Repository: nats-io/nack Branch: main Commit: f6dad096d5d4 Files: 177 Total size: 926.2 KB Directory structure: gitextract_mbhvqodb/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── defect.yml │ │ └── proposal.yml │ ├── dependabot.yml │ └── workflows/ │ ├── claude.yml │ ├── deps-release-detect.yaml │ ├── deps-release-tag.yaml │ ├── e2e.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .goreleaser.yml ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cicd/ │ ├── Dockerfile │ ├── Dockerfile_goreleaser │ ├── assets/ │ │ └── entrypoint.sh │ └── tag-deps-version.txt ├── cmd/ │ ├── jetstream-controller/ │ │ └── main.go │ ├── nats-boot-config/ │ │ └── main.go │ └── nats-server-config-reloader/ │ └── main.go ├── controllers/ │ └── jetstream/ │ ├── conn_pool.go │ ├── conn_pool_test.go │ ├── consumer.go │ ├── consumer_test.go │ ├── controller.go │ ├── controller_test.go │ ├── jsmclient.go │ ├── jsmclient_test.go │ ├── stream.go │ └── stream_test.go ├── dependencies.md ├── deploy/ │ ├── crds.yml │ ├── examples/ │ │ ├── consumer_pull.yml │ │ ├── consumer_push.yml │ │ ├── stream.yml │ │ ├── stream_mirror.yml │ │ ├── stream_placement.yml │ │ ├── stream_servers.yml │ │ └── stream_sources.yml │ └── rbac.yml ├── docker-bake.hcl ├── docs/ │ └── api.md ├── examples/ │ └── secure/ │ ├── client-tls.yaml │ ├── issuer.yaml │ ├── nack/ │ │ ├── account-foo.yaml │ │ ├── nats-account-a.yaml │ │ ├── nats-consumer-bar-a.yaml │ │ ├── nats-stream-foo-a.yaml │ │ └── stream-foo.yaml │ ├── nack-a-client-tls.yaml │ ├── nack-b-client-tls.yaml │ ├── nats-helm.yaml │ └── server-tls.yaml ├── go.mod ├── go.sum ├── internal/ │ └── controller/ │ ├── account_controller.go │ ├── account_controller_test.go │ ├── client.go │ ├── connection_pool.go │ ├── connection_pool_test.go │ ├── consumer_controller.go │ ├── consumer_controller_test.go │ ├── helpers_test.go │ ├── jetstream_controller.go │ ├── jetstream_controller_test.go │ ├── keyvalue_controller.go │ ├── keyvalue_controller_test.go │ ├── objectstore_controller.go │ ├── objectstore_controller_test.go │ ├── register.go │ ├── stream_controller.go │ ├── stream_controller_test.go │ ├── suite_test.go │ └── types.go ├── kuttl-test.yaml ├── pkg/ │ ├── bootconfig/ │ │ └── bootconfig.go │ ├── jetstream/ │ │ ├── apis/ │ │ │ └── jetstream/ │ │ │ ├── register.go │ │ │ ├── v1beta1/ │ │ │ │ ├── consumertypes.go │ │ │ │ ├── doc.go │ │ │ │ ├── register.go │ │ │ │ ├── streamtemplatetypes.go │ │ │ │ ├── streamtypes.go │ │ │ │ ├── types.go │ │ │ │ └── zz_generated.deepcopy.go │ │ │ └── v1beta2/ │ │ │ ├── accounttypes.go │ │ │ ├── consumertypes.go │ │ │ ├── doc.go │ │ │ ├── keyvaluetypes.go │ │ │ ├── objectstoretypes.go │ │ │ ├── register.go │ │ │ ├── streamtypes.go │ │ │ ├── types.go │ │ │ └── zz_generated.deepcopy.go │ │ └── generated/ │ │ ├── applyconfiguration/ │ │ │ ├── internal/ │ │ │ │ └── internal.go │ │ │ ├── jetstream/ │ │ │ │ └── v1beta2/ │ │ │ │ ├── account.go │ │ │ │ ├── accountspec.go │ │ │ │ ├── basestreamconfig.go │ │ │ │ ├── condition.go │ │ │ │ ├── connectionopts.go │ │ │ │ ├── consumer.go │ │ │ │ ├── consumerlimits.go │ │ │ │ ├── consumerspec.go │ │ │ │ ├── credssecret.go │ │ │ │ ├── keyvalue.go │ │ │ │ ├── keyvaluespec.go │ │ │ │ ├── nkeysecret.go │ │ │ │ ├── objectstore.go │ │ │ │ ├── objectstorespec.go │ │ │ │ ├── republish.go │ │ │ │ ├── secretref.go │ │ │ │ ├── status.go │ │ │ │ ├── stream.go │ │ │ │ ├── streamplacement.go │ │ │ │ ├── streamsource.go │ │ │ │ ├── streamspec.go │ │ │ │ ├── subjecttransform.go │ │ │ │ ├── tls.go │ │ │ │ ├── tlssecret.go │ │ │ │ ├── tokensecret.go │ │ │ │ └── user.go │ │ │ └── utils.go │ │ ├── clientset/ │ │ │ └── versioned/ │ │ │ ├── clientset.go │ │ │ ├── fake/ │ │ │ │ ├── clientset_generated.go │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ ├── scheme/ │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ └── typed/ │ │ │ └── jetstream/ │ │ │ └── v1beta2/ │ │ │ ├── account.go │ │ │ ├── consumer.go │ │ │ ├── doc.go │ │ │ ├── fake/ │ │ │ │ ├── doc.go │ │ │ │ ├── fake_account.go │ │ │ │ ├── fake_consumer.go │ │ │ │ ├── fake_jetstream_client.go │ │ │ │ ├── fake_keyvalue.go │ │ │ │ ├── fake_objectstore.go │ │ │ │ └── fake_stream.go │ │ │ ├── generated_expansion.go │ │ │ ├── jetstream_client.go │ │ │ ├── keyvalue.go │ │ │ ├── objectstore.go │ │ │ └── stream.go │ │ ├── informers/ │ │ │ └── externalversions/ │ │ │ ├── factory.go │ │ │ ├── generic.go │ │ │ ├── internalinterfaces/ │ │ │ │ └── factory_interfaces.go │ │ │ └── jetstream/ │ │ │ ├── interface.go │ │ │ └── v1beta2/ │ │ │ ├── account.go │ │ │ ├── consumer.go │ │ │ ├── interface.go │ │ │ ├── keyvalue.go │ │ │ ├── objectstore.go │ │ │ └── stream.go │ │ └── listers/ │ │ └── jetstream/ │ │ └── v1beta2/ │ │ ├── account.go │ │ ├── consumer.go │ │ ├── expansion_generated.go │ │ ├── keyvalue.go │ │ ├── objectstore.go │ │ └── stream.go │ ├── k8scodegen/ │ │ ├── file-header.txt │ │ └── k8scodegen.go │ └── natsreloader/ │ ├── natsreloader.go │ └── natsreloader_test.go └── tests/ ├── Dockerfile ├── nack-control-loop.yaml ├── nack-legacy.yaml ├── nats.yaml └── stream-creation/ ├── 00-nack.yaml ├── 01-stream.yaml ├── 02-natscli-stream.yaml ├── asserted-natscli.yaml ├── asserted-rides-stream.yaml ├── natscli.yaml └── rides-stream.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Discussion url: https://github.com/nats-io/nack/discussions about: Ideal for ideas, feedback, or longer form questions. - name: Chat url: https://slack.nats.io about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! ================================================ FILE: .github/ISSUE_TEMPLATE/defect.yml ================================================ --- name: Defect description: Report a defect, such as a bug or regression. labels: - defect body: - type: textarea id: versions attributes: label: What version were you using? description: Include the server version (`nats-server --version`) and any client versions when observing the issue. validations: required: true - type: textarea id: environment attributes: label: What environment was the server running in? description: This pertains to the operating system, CPU architecture, and/or Docker image that was used. validations: required: true - type: textarea id: steps attributes: label: Is this defect reproducible? description: Provide best-effort steps to showcase the defect. validations: required: true - type: textarea id: expected attributes: label: Given the capability you are leveraging, describe your expectation? description: This may be the expected behavior or performance characteristics. validations: required: true - type: textarea id: actual attributes: label: Given the expectation, what is the defect you are observing? description: This may be an unexpected behavior or regression in performance. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/proposal.yml ================================================ --- name: Proposal description: Propose an enhancement or new feature. labels: - proposal body: - type: textarea id: usecase attributes: label: What motivated this proposal? description: Describe the use case justifying this request. validations: required: true - type: textarea id: change attributes: label: What is the proposed change? description: This could be a behavior change, enhanced API, or a branch new feature. validations: required: true - type: textarea id: benefits attributes: label: Who benefits from this change? description: Describe how this not only benefits you. validations: required: false - type: textarea id: alternates attributes: label: What alternatives have you evaluated? description: This could be using existing features or relying on an external dependency. validations: required: false ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # version updates: enabled # security updates: enabled - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cooldown: default-days: 7 - package-ecosystem: docker directory: /cicd schedule: interval: daily cooldown: default-days: 7 # version updates: disabled # security updates: enabled # https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates#overriding-the-default-behavior-with-a-configuration-file - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 0 cooldown: default-days: 7 ================================================ FILE: .github/workflows/claude.yml ================================================ name: Claude Code # GITHUB_TOKEN needs contents:read and actions:read — required by # claude-code-action for restoring trusted config files from the base branch. # All other GitHub API access uses the App token. permissions: contents: read actions: read on: issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request_target: types: [opened, reopened] jobs: claude: uses: synadia-io/ai-workflows/.github/workflows/claude.yml@v2 with: gh_app_id: ${{ vars.CLAUDE_GH_APP_ID }} checkout_mode: 'base' review_focus: | Additionally focus on: - Kubernetes controller reconciliation correctness (idempotency, status updates, error handling) - Proper use of controller-runtime patterns (watches, predicates, ownership references) - Go error handling (wrapped errors, sentinel errors, no swallowed errors) secrets: claude_oauth_token: ${{ secrets.CLAUDE_OAUTH_TOKEN }} gh_app_private_key: ${{ secrets.CLAUDE_GH_APP_PRIVATE_KEY }} ================================================ FILE: .github/workflows/deps-release-detect.yaml ================================================ name: Deps Release on: 'pull_request' permissions: contents: write jobs: detect: name: Detect runs-on: ubuntu-latest if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" git checkout -b "$GITHUB_HEAD_REF" - name: Dependabot metadata id: dependabot-metadata uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 - name: Install node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 18 - name: Install semver run: |- npm install -g semver - name: Bump run: |- set -e push=0 config='[ { "directory": "cicd", "dependencyName": "alpine" } ]' deps_file="./cicd/tag-deps-version.txt" deps="${STEPS_DEPENDABOT_METADATA_OUTPUTS_UPDATED_DEPENDENCIES_JSON}" for i in $(seq 0 "$(("$(echo "$config" | jq length) - 1"))"); do directory="$(echo "$config" | jq -r ".[$i].directory")" dependencyName="$(echo "$config" | jq -r ".[$i].dependencyName")" match="$(echo "$deps" | jq ".[] | select(.directory == \"/$directory\" and .dependencyName == \"$dependencyName\")")" if [ -z "$match" ]; then continue fi updateType="$(echo "$match" | jq -r ".updateType")" prevVersion="$(echo "$match" | jq -r ".prevVersion")" newVersion="$(echo "$match" | jq -r ".newVersion")" echo "directory : $directory" echo "dependencyName : $dependencyName" echo "updateType : $updateType" echo "prevVersion : $prevVersion" echo "newVersion : $newVersion" tagPrevVersion="$(git ls-remote 2>/dev/null \ | grep -oE 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+' \ | cut -d'/' -f3 \ | xargs semver \ | tail -n 1)" tagNewVersion="$(semver -i patch "$tagPrevVersion")" echo "$tagPrevVersion" > "$deps_file" echo "$tagNewVersion" >> "$deps_file" git add "$deps_file" if git commit -m "bump dependency release to $tagNewVersion"; then push=1 fi done if [ "$push" = "1" ]; then git push -u origin "$GITHUB_HEAD_REF" fi env: STEPS_DEPENDABOT_METADATA_OUTPUTS_UPDATED_DEPENDENCIES_JSON: ${{ steps.dependabot-metadata.outputs.updated-dependencies-json }} ================================================ FILE: .github/workflows/deps-release-tag.yaml ================================================ name: Deps Release on: push: branches: - main permissions: actions: write contents: write jobs: tag: name: Tag runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - id: tag name: Determine tag run: | deps_file="./cicd/tag-deps-version.txt" old_version="$(head -n 1 "$deps_file")" old_ref_name="v$old_version" new_version="$(tail -n 1 "$deps_file")" new_ref_name="v$new_version" create=true if [ "$(git ls-remote origin "refs/tags/$new_ref_name" | wc -l)" = "1" ]; then create=false fi echo "old-version=$old_version" | tee -a "$GITHUB_OUTPUT" echo "old-ref-name=$old_ref_name" | tee -a "$GITHUB_OUTPUT" echo "new-version=$new_version" | tee -a "$GITHUB_OUTPUT" echo "new-ref-name=$new_ref_name" | tee -a "$GITHUB_OUTPUT" echo "create=$create" | tee -a "$GITHUB_OUTPUT" - if: ${{ fromJSON(steps.tag.outputs.create) }} name: Tag run: | commit="$(git rev-parse HEAD)" git fetch origin refs/tags/"${STEPS_TAG_OUTPUTS_OLD_REF_NAME}" git checkout -b deps "${STEPS_TAG_OUTPUTS_OLD_REF_NAME}" git restore --source="$commit" ./cicd ./.github/workflows/release.yaml git add ./cicd ./.github/workflows/release.yaml if git commit -m "bump dependency release to ${STEPS_TAG_OUTPUTS_NEW_VERSION}"; then git tag "${STEPS_TAG_OUTPUTS_NEW_REF_NAME}" git push origin "${STEPS_TAG_OUTPUTS_NEW_REF_NAME}" fi env: STEPS_TAG_OUTPUTS_OLD_REF_NAME: ${{ steps.tag.outputs.old-ref-name }} STEPS_TAG_OUTPUTS_NEW_VERSION: ${{ steps.tag.outputs.new-version }} STEPS_TAG_OUTPUTS_NEW_REF_NAME: ${{ steps.tag.outputs.new-ref-name }} - if: ${{ fromJSON(steps.tag.outputs.create) }} name: Trigger Release run: gh workflow run release.yaml --ref "${STEPS_TAG_OUTPUTS_NEW_REF_NAME}" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} STEPS_TAG_OUTPUTS_NEW_REF_NAME: ${{ steps.tag.outputs.new-ref-name }} ================================================ FILE: .github/workflows/e2e.yaml ================================================ name: e2e on: push: branches: - main pull_request: jobs: e2e: name: e2e runs-on: ubuntu-latest permissions: contents: read steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: install kuttl run: | curl -L https://github.com/kudobuilder/kuttl/releases/download/v0.24.0/kubectl-kuttl_0.24.0_linux_x86_64 -o /usr/local/bin/kubectl-kuttl chmod +x /usr/local/bin/kubectl-kuttl - name: create kind cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: install_only: true - name: set up helm uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 - name: run e2e test run: make test-e2e ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: workflow_dispatch: push: tags: - v[0-9]+.[0-9]+.[0-9]+ jobs: release: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: false - name: Setup QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Setup Docker Buildx id: buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Setup Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_CLI_TOKEN }} - name: Get Image Tags id: tags run: | version=$(sed 's/^v//' <<< ${GITHUB_REF_NAME}) echo tags="latest,${version}" >> $GITHUB_OUTPUT - name: Build and Push uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0 with: source: . files: docker-bake.hcl push: true set: goreleaser.args.GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} env: TAGS: "${{ steps.tags.outputs.tags }}" REGISTRY: "natsio" - name: Attach Release Files run: gh release upload ${GITHUB_REF_NAME} deploy/crds.yml deploy/rbac.yml env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: push: paths-ignore: - '**.md' pull_request: paths-ignore: - '**.md' jobs: test: name: Test runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Go id: setup-go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Build run: make build - name: Test run: make test ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out /jetstream-controller.docker /jetstream-controller /nats-server-config-reloader /nats-server-config-reloader.docker /nats-boot-config /nats-boot-config.docker /tools /bin /.idea /kubeconfig # E2E test generated config /tests/nack.yaml ================================================ FILE: .goreleaser.yml ================================================ version: 2 project_name: nack release: name_template: 'Release {{.Tag}}' draft: true skip_upload: true github: owner: nats-io name: nack builds: - id: jetstream-controller main: ./cmd/jetstream-controller binary: jetstream-controller ldflags: &common_ldflags - -s -w -X main.Version={{ if index .Env "VERSION" }}{{ .Env.VERSION }}{{ else }}{{ .Version }}{{ end }} -X main.GitInfo={{.ShortCommit}} -X main.BuildTime={{.Date}} tags: - timetzdata env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 - arm64 - arm goarm: - 6 - 7 - id: nats-boot-config main: ./cmd/nats-boot-config binary: nats-boot-config ldflags: *common_ldflags tags: - timetzdata env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 - arm64 - arm goarm: - 6 - 7 - id: nats-server-config-reloader main: ./cmd/nats-server-config-reloader binary: nats-server-config-reloader ldflags: *common_ldflags tags: - timetzdata env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 - arm64 - arm goarm: - 6 - 7 ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview NACK (NATS Controllers for Kubernetes) is a Go-based Kubernetes operator that manages NATS JetStream resources (Streams, Consumers, Accounts, KeyValue, ObjectStore) via CRDs. It also includes a NATS server config reloader sidecar and a boot config init container. ## Build & Test Commands ```bash make build # Build all binaries make jetstream-controller # Build main controller (with race detector) make nats-server-config-reloader # Build config reloader sidecar make nats-boot-config # Build boot config utility make test # Run unit tests (go vet + envtest + go test) make test-e2e # Run E2E tests with kuttl (requires kind) make generate # Regenerate K8s clientset and deepcopy code make clean # Remove built binaries ``` Run a single test package: ```bash go test -race -cover -count=1 -timeout 30s ./internal/controller/... go test -race -cover -count=1 -timeout 30s ./controllers/jetstream/... ``` Run a single test: ```bash go test -race -count=1 -timeout 30s -run TestMyFunction ./internal/controller/... ``` Format code: `go fmt ./...` ## Architecture ### Two Controller Modes The `jetstream-controller` binary runs in one of two modes: - **Legacy mode** (default): Event-driven queue processing using custom informer factories. Supports only Stream and Consumer. Code in `controllers/jetstream/`. - **Control-loop mode** (`--control-loop`): Full controller-runtime reconciliation loop. Supports all resource types (Stream, Consumer, Account, KeyValue, ObjectStore). Code in `internal/controller/`. Entry point: `cmd/jetstream-controller/main.go` — the `--control-loop` flag selects which mode to run. ### CRD Types All CRDs are API version `jetstream.nats.io/v1beta2`, defined in `pkg/jetstream/apis/jetstream/v1beta2/`: - `streamtypes.go`, `consumertypes.go`, `accounttypes.go`, `keyvaluetypes.go`, `objectstoretypes.go` Generated client code lives in `pkg/jetstream/generated/` — do not edit manually, run `make generate`. ### Controller Patterns Controllers follow standard Kubernetes operator patterns: - **Finalizers** for safe deletion cleanup (defined in `internal/controller/types.go`) - **Status conditions** (Ready/Errored) tracked on each resource - **State annotations** for reconciliation state tracking (Ready, Reconciling, Errored, Finalizing) - **Idempotent reconciliation** — operations must be safe to retry - **Owner references** for parent-child relationships (e.g., Consumer → Stream) ### Other Components - `pkg/natsreloader/` — watches config files and sends SIGHUP to reload NATS server - `pkg/bootconfig/` — init container for node-level network config ## Key Dependencies - `sigs.k8s.io/controller-runtime` — Kubernetes controller framework (control-loop mode) - `k8s.io/client-go` — Kubernetes client (legacy mode) - `github.com/nats-io/nats.go` — NATS client - `github.com/nats-io/jsm.go` — JetStream management ## Review Focus Areas When reviewing changes, pay attention to: - Kubernetes controller reconciliation correctness (idempotency, status updates, error handling) - Proper use of controller-runtime patterns (watches, predicates, ownership references) - Go error handling (wrapped errors, sentinel errors, no swallowed errors) ## Local Development ```bash # Build and run against a local kubeconfig make jetstream-controller ./jetstream-controller -kubeconfig ~/.kube/config -s nats://localhost:4222 # Start a local JetStream-enabled NATS server nats-server -DV -js # Increase log verbosity (klog flags) ./jetstream-controller -kubeconfig ~/.kube/config -s nats://localhost:4222 -v=10 ``` ================================================ FILE: CONTRIBUTING.md ================================================ > [!WARNING] > This contribution guide is work in progress and is meant to be a location where more developers can contribute. # Development The codebase is currently fragmented into the refactored solution using today's standards for creating controllers (when using the `--control-loop` argument) and the old variant. The old variant is found in `controllers` directory, while the new code is found in `internal`. ## E2E testing You may run the entire e2e suite with the accompanying updated image using: ```bash make test-e2e ``` This command will: 1. Build a local Docker image with your changes 2. Run the full test suite in **legacy controller mode** (using `controllers/` implementation) 3. Run the full test suite in **control-loop mode** (using `internal/controller/` implementation with `--control-loop` flag) This ensures both controller implementations are tested with your changes. **Requirements:** - `kind` must be installed (install via `make install-kind`) - `kubectl-kuttl` must be installed (install via `kubectl krew install kuttl`) # CRD Updates ## Generating types ```bash make generate ``` will update the generated go structs after having updated the types. ## CRD and docs CRD updates & accompanying documentation is currently updated manually. TODO to automate this. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ export GO111MODULE := on SHELL=/usr/bin/env bash ENVTEST_K8S_VERSION = 1.32.0 now := $(shell date -u +%Y-%m-%dT%H:%M:%S%z) gitBranch := $(shell git rev-parse --abbrev-ref HEAD) gitCommit := $(shell git rev-parse --short HEAD) repoDirty := $(shell git diff --quiet || echo "-dirty") VERSION ?= version-not-set linkerVars := -X main.BuildTime=$(now) -X main.GitInfo=$(gitBranch)-$(gitCommit)$(repoDirty) -X main.Version=$(VERSION) drepo ?= natsio jetstreamSrc := $(shell find cmd/jetstream-controller pkg/jetstream internal/controller controllers/jetstream -name "*.go") pkg/jetstream/apis/jetstream/v1beta2/zz_generated.deepcopy.go configReloaderSrc := $(shell find cmd/nats-server-config-reloader/ pkg/natsreloader/ -name "*.go") bootConfigSrc := $(shell find cmd/nats-boot-config/ pkg/bootconfig/ -name "*.go") # You might override this so as to use a more recent version, to update old # generated imports, and so migrate away from old import paths and get back to # a consistent import tree. codeGeneratorDir ?= default: # Try these (read Makefile for more recipes): # make jetstream-controller # make nats-server-config-reloader # make nats-boot-config generate: fetch-modules pkg/k8scodegen/file-header.txt rm -rf pkg/jetstream/generated D="$(codeGeneratorDir)"; : "$${D:=`go list -m -f '{{.Dir}}' k8s.io/code-generator`}"; \ source "$$D/kube_codegen.sh" ; \ kube::codegen::gen_helpers \ --boilerplate pkg/k8scodegen/file-header.txt \ pkg/jetstream/apis; \ kube::codegen::gen_client \ --with-watch \ --with-applyconfig \ --boilerplate pkg/k8scodegen/file-header.txt \ --output-dir pkg/jetstream/generated \ --output-pkg github.com/nats-io/nack/pkg/jetstream/generated \ --one-input-api jetstream/v1beta2 \ pkg/jetstream/apis jetstream-controller: $(jetstreamSrc) go build -race -o $@ \ -ldflags "$(linkerVars)" \ github.com/nats-io/nack/cmd/jetstream-controller jetstream-controller.docker: $(jetstreamSrc) CGO_ENABLED=0 GOOS=linux go build -o $@ \ -ldflags "-s -w $(linkerVars)" \ -tags timetzdata \ github.com/nats-io/nack/cmd/jetstream-controller .PHONY: jetstream-controller-docker jetstream-controller-docker: ifneq ($(ver),) REGISTRY="$(drepo)" \ TAGS="$(ver)" \ docker buildx bake --load \ --set goreleaser.args.VERSION=$(ver) \ jetstream-controller else # Missing version, try this. # make jetstream-controller-docker ver=1.2.3 exit 1 endif .PHONY: jetstream-controller-dockerx jetstream-controller-dockerx: ifneq ($(ver),) # Ensure 'docker buildx ls' shows correct platforms. REGISTRY="$(drepo)" \ TAGS="$(ver)" \ PUSH=true \ docker buildx bake --push \ --set goreleaser.args.VERSION=$(ver) \ jetstream-controller else # Missing version, try this. # make jetstream-controller-dockerx ver=1.2.3 exit 1 endif nats-server-config-reloader: $(configReloaderSrc) go build -race -o $@ \ -ldflags "$(linkerVars)" \ github.com/nats-io/nack/cmd/nats-server-config-reloader nats-server-config-reloader.docker: $(configReloaderSrc) CGO_ENABLED=0 GOOS=linux go build -o $@ \ -ldflags "-s -w $(linkerVars)" \ -tags timetzdata \ github.com/nats-io/nack/cmd/nats-server-config-reloader .PHONY: nats-server-config-reloader-docker nats-server-config-reloader-docker: ifneq ($(ver),) REGISTRY="$(drepo)" \ TAGS="$(ver)" \ docker buildx bake --load \ --set goreleaser.args.VERSION=$(ver) \ nats-server-config-reloader else # Missing version, try this. # make nats-server-config-reloader-docker ver=1.2.3 exit 1 endif .PHONY: nats-server-config-reloader-dockerx nats-server-config-reloader-dockerx: ifneq ($(ver),) # Ensure 'docker buildx ls' shows correct platforms. REGISTRY="$(drepo)" \ TAGS="$(ver)" \ PUSH=true \ docker buildx bake --push \ --set goreleaser.args.VERSION=$(ver) \ nats-server-config-reloader else # Missing version, try this. # make nats-server-config-reloader-dockerx ver=1.2.3 exit 1 endif nats-boot-config: $(bootConfigSrc) go build -race -o $@ \ -ldflags "$(linkerVars)" \ github.com/nats-io/nack/cmd/nats-boot-config nats-boot-config.docker: $(bootConfigSrc) CGO_ENABLED=0 GOOS=linux go build -o $@ \ -ldflags "-s -w $(linkerVars)" \ -tags timetzdata \ github.com/nats-io/nack/cmd/nats-boot-config .PHONY: nats-boot-config-docker nats-boot-config-docker: ifneq ($(ver),) REGISTRY="$(drepo)" \ TAGS="$(ver)" \ docker buildx bake --load \ --set goreleaser.args.VERSION=$(ver) \ nats-boot-config else # Missing version, try this. # make nats-boot-config-docker ver=1.2.3 exit 1 endif .PHONY: nats-boot-config-dockerx nats-boot-config-dockerx: ifneq ($(ver),) # Ensure 'docker buildx ls' shows correct platforms. REGISTRY="$(drepo)" \ TAGS="$(ver)" \ PUSH=true \ docker buildx bake --push \ --set goreleaser.args.VERSION=$(ver) \ nats-boot-config else # Missing version, try this. # make nats-boot-config-dockerx ver=1.2.3 exit 1 endif .PHONY: fetch-modules # This will error if we have removed some code to be regenerated, so we instead silence it and force success fetch-modules: go list -f '{{with .Module}}{{end}}' all >/dev/null 2>&1 || true .PHONY: build build: jetstream-controller nats-server-config-reloader nats-boot-config # Setup envtest tools based on a operator-sdk project makefile LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary (ideally with version) # $2 - package url which can be installed # $3 - specific version of package define go-install-tool @[ -f $(1) ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ GOBIN=$(LOCALBIN) go install $${package} ;\ mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ } endef ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) ENVTEST_VERSION ?= release-0.20 .PHONY: envtest envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) .PHONY: test test: envtest go vet ./controllers/... ./pkg/natsreloader/... ./internal/controller/... $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path ## Get k8s binaries go test -race -cover -count=1 -timeout 30s ./controllers/... ./pkg/natsreloader/... ./internal/controller/... .PHONY: test-e2e test-e2e: @echo "Running e2e tests with kuttl..." @command -v kubectl-kuttl >/dev/null 2>&1 || { echo "kuttl not installed. Install: kubectl krew install kuttl"; exit 1; } kind delete cluster || true kind create cluster docker build -t nack:test -f tests/Dockerfile . kind load docker-image nack:test @echo "\n=== Testing LEGACY controller mode ===" cp tests/nack-legacy.yaml tests/nack.yaml && kubectl kuttl test @echo "\n=== Testing CONTROL-LOOP controller mode ===" cp tests/nack-control-loop.yaml tests/nack.yaml && kubectl kuttl test rm -f tests/nack.yaml .PHONY: clean clean: rm -f jetstream-controller jetstream-controller.docker \ nats-server-config-reloader nats-server-config-reloader.docker \ nats-boot-config nats-boot-config.docker .PHONY: install-kind install-kind: go install sigs.k8s.io/kind@v0.30.0 ================================================ FILE: README.md ================================================ nack-large [![License Apache 2](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Release Badge](https://github.com/nats-io/nack/actions/workflows/release.yaml/badge.svg)](https://github.com/nats-io/nack/actions/workflows/release.yaml) [![E2E Badge](https://github.com/nats-io/nack/actions/workflows/e2e.yaml/badge.svg)](https://github.com/nats-io/nack/actions/workflows/e2e.yaml) [![TEST Badge](https://github.com/nats-io/nack/actions/workflows/test.yaml/badge.svg)](https://github.com/nats-io/nack/actions/workflows/test.yaml) [NATS](https://nats.io) Controllers for Kubernetes (NACK) ## Table of Contents - [JetStream Controller](#jetstream-controller) - [Controller Modes](#controller-modes) - [Getting Started](#getting-started) - [Managing Multiple NATS Systems and Accounts](#managing-multiple-nats-systems-and-accounts) - [Creating NATS Resources](#creating-nats-resources) - [Getting Started with Accounts](#getting-started-with-accounts) - [Local Development](#local-development) - [NATS Server Config Reloader](#nats-server-config-reloader) - [NATS Boot Config](#nats-boot-config) ## JetStream Controller The JetStream controllers allows you to manage [NATS JetStream](https://docs.nats.io/nats-concepts/jetstream) resources via Kubernetes CRDs. ### Controller Modes NACK supports two controller modes with different capabilities: | Mode | Streams | Consumers | Key/Value | Object Store | Accounts | |------|---------|-----------|-----------|--------------|----------| | **Legacy (default)** | ✅ | ✅ | ❌ | ❌ | ❌ | | **Control-loop** (`--control-loop`) | ✅ | ✅ | ✅ | ✅ | ✅ | > **Important**: Key/Value stores and Object stores are **only supported in control-loop mode**. If you create KeyValue or ObjectStore resources without enabling control-loop mode, they will not be reconciled. Resources managed by NACK controllers are expected to _exclusively_ be managed by NACK, and configuration state will be enforced if mutated by an external client. ## [API Reference](docs/api.md) The API reference documents all available CRD fields for Streams, Consumers, KeyValue, ObjectStore, and Account resources. ### Getting started Install with Helm: ```sh helm repo add nats https://nats-io.github.io/k8s/helm/charts/ helm repo update helm upgrade --install nats nats/nats \ --set config.jetstream.enabled=true \ --set config.jetstream.memoryStore.enabled=true \ --set config.cluster.enabled=true --wait helm upgrade --install nack nats/nack \ --set jetstream.nats.url=nats://nats.default.svc.cluster.local:4222 --wait ``` #### (Optional) Enable Experimental `controller-runtime` Controllers > **Note**: The updated controllers will more reliably enforce resource state. If migrating from an older version of NACK, as long as all NATS resources are in-sync with NACK resources no modifications are expected. > > The `jetstream-controller` logs will contain a diff of any changes the controller has made. ```sh helm upgrade nack nats/nack \ --set jetstream.nats.url=nats://nats.default.svc.cluster.local:4222 \ --set jetstream.additionalArgs={--control-loop} --wait ``` ### Managing Multiple NATS Systems and Accounts There are several approaches for managing multiple NATS Systems with NACK within one Kubernetes cluster. These options are not mutually exclusive. #### 1. Run Multiple Namespaced Controllers You can run multiple NACK controllers on the same Kubernetes cluster. Add `--set config.namespaced=true` to your install flags or set `namespaced: true` in your `values.yaml`. When set, the controller will only reconcile resources within its own namespace. ```sh helm upgrade --install nack nats/nack \ --create-namespace --namespace nats \ --set namespaced=true \ --set jetstream.nats.url=nats://nats.nats.svc.cluster.local:4222 --wait ``` #### 2. Use the Accounts Resource The Accounts resource acts as a connection config for other resources. You may define multiple accounts for the same, or for distinct, NATS Systems. ```yaml --- apiVersion: jetstream.nats.io/v1beta2 kind: Account metadata: name: a spec: name: a creds: secret: name: account-a-creds servers: - nats://nats.nats-a.svc.cluster.local --- apiVersion: jetstream.nats.io/v1beta2 kind: Account metadata: name: b spec: name: b creds: secret: name: account-b-creds servers: - nats://nats.nats-b.svc.cluster.local --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: foo-a spec: name: foo subjects: ["foo", "foo.>"] storage: file replicas: 3 maxAge: 1h account: a --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: foo-b spec: name: foo subjects: ["foo", "foo.>"] storage: file replicas: 3 maxAge: 1h account: b ``` The above manifests will define two Account resources, each pulling credentials from a Kubernetes secret. Account `a` is configured to use the NATS Cluster in namespace `nats-a` and Account `b` is configured to use the NATS Cluster in namespace `nats-b`. The NATS clusters do not need to be in Kubernetes, this is just an example. This will also create an identical stream, `foo`, in each cluster. **Note:** The resource names, `foo-a` and `foo-b`, must be distinct to not conflict as Kubernetes resources, but the stream names themselves are both `foo`. See more details in the [Getting Started with Accounts](#getting-started-with-accounts) section. #### 3. Define Connection Config in the CRD Manifest You may define some connection options within the resource manifests directly. If not running in the newer `--control-loop` mode, set `--crd-connect`. If running with `--control-loop`, resource-level connection config will always override any global config. > **Note**: The `--crd-connect` flag is not required if running with `--control-loop`. ```sh helm upgrade nack nats/nack \ --set jetstream.additionalArgs={--crd-connect} --wait ``` #### Example Stream: ```yaml apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: bar spec: name: bar subjects: ["bar", "bar.>"] storage: file replicas: 3 maxAge: 1h servers: - nats://nats.nats.svc.cluster.local:4222 ``` ### Creating NATS Resources Let's create a stream and a couple of consumers: ```yaml --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream spec: name: mystream subjects: ["orders.*"] storage: memory maxAge: 1h --- apiVersion: jetstream.nats.io/v1beta2 kind: Consumer metadata: name: my-push-consumer spec: streamName: mystream durableName: my-push-consumer deliverSubject: my-push-consumer.orders deliverPolicy: last ackPolicy: none replayPolicy: instant --- apiVersion: jetstream.nats.io/v1beta2 kind: Consumer metadata: name: my-pull-consumer spec: streamName: mystream durableName: my-pull-consumer deliverPolicy: all filterSubject: orders.received maxDeliver: 20 ackPolicy: explicit --- # Note: KeyValue requires control-loop mode to be enabled apiVersion: jetstream.nats.io/v1beta2 kind: KeyValue metadata: name: my-key-value spec: bucket: my-key-value history: 20 storage: file maxBytes: 2048 compression: true --- # Note: ObjectStore requires control-loop mode to be enabled apiVersion: jetstream.nats.io/v1beta2 kind: ObjectStore metadata: name: my-object-store spec: bucket: my-object-store storage: file replicas: 1 maxBytes: 536870912 # 512 MB compression: true ``` ```sh # Create a stream. $ kubectl apply -f https://raw.githubusercontent.com/nats-io/nack/main/deploy/examples/stream.yml # Check if it was successfully created. $ kubectl get streams NAME STATE STREAM NAME SUBJECTS mystream Ready mystream [orders.*] # Create a push-based consumer $ kubectl apply -f https://raw.githubusercontent.com/nats-io/nack/main/deploy/examples/consumer_push.yml # Create a pull based consumer $ kubectl apply -f https://raw.githubusercontent.com/nats-io/nack/main/deploy/examples/consumer_pull.yml # Check if they were successfully created. $ kubectl get consumers NAME STATE STREAM CONSUMER ACK POLICY my-pull-consumer Ready mystream my-pull-consumer explicit my-push-consumer Ready mystream my-push-consumer none # If you end up in an Errored state, run kubectl describe for more info. # kubectl describe streams mystream # kubectl describe consumers my-pull-consumer ``` Now we're ready to use Streams and Consumers. Let's start off with writing some data into `mystream`. ```sh # Run nats-box that includes the NATS management utilities, and exec into it. $ kubectl exec -it deployment/nats-box -- /bin/sh -l # Publish a couple of messages from nats-box nats-box:~$ nats pub orders.received "order 1" nats-box:~$ nats pub orders.received "order 2" ``` First, we'll read the data using a pull-based consumer. From the above `my-pull-consumer` Consumer CRD, we have set the filterSubject of `orders.received`. You can double check with the following command: ```sh $ kubectl get consumer my-pull-consumer -o jsonpath={.spec.filterSubject} orders.received ``` So that's the subject my-pull-consumer will pull messages from. ```sh # Pull first message. nats-box:~$ nats consumer next mystream my-pull-consumer --- subject: orders.received / delivered: 1 / stream seq: 1 / consumer seq: 1 order 1 Acknowledged message # Pull next message. nats-box:~$ nats consumer next mystream my-pull-consumer --- subject: orders.received / delivered: 1 / stream seq: 2 / consumer seq: 2 order 2 Acknowledged message ``` Next, let's read data using a push-based consumer. From the above `my-push-consumer` Consumer CRD, we have set the deliverSubject of `my-push-consumer.orders`, as you can confirm with the following command: ```sh $ kubectl get consumer my-push-consumer -o jsonpath={.spec.deliverSubject} my-push-consumer.orders ``` So pushed messages will arrive on that subject. This time all messages arrive automatically. ```sh nats-box:~$ nats sub my-push-consumer.orders 17:57:24 Subscribing on my-push-consumer.orders [#1] Received JetStream message: consumer: mystream > my-push-consumer / subject: orders.received / delivered: 1 / consumer seq: 1 / stream seq: 1 / ack: false order 1 [#2] Received JetStream message: consumer: mystream > my-push-consumer / subject: orders.received / delivered: 1 / consumer seq: 2 / stream seq: 2 / ack: false order 2 ``` ### Getting Started with Accounts You can create an Account resource with the following CRD. The Account resource can be used to specify server and TLS information. > **Note** The `Account` resource does not create or manage NATS accounts. It functions as a connection and authentication config for the managed resources. The [nsc](https://docs.nats.io/using-nats/nats-tools/nsc/basics#creating-an-operator-account-and-user) tool can be used to manage your NATS account configuration on the server-side. You can find more details about NATS decentralized auth in the [docs](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt). ```yaml --- apiVersion: jetstream.nats.io/v1beta2 kind: Account metadata: name: a spec: name: a servers: - nats://nats:4222 tls: secret: name: nack-a-tls ca: "ca.crt" cert: "tls.crt" key: "tls.key" ``` You can then link an Account to a Stream so that the Stream uses the Account information for its creation. ```yaml --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: foo spec: name: foo subjects: ["foo", "foo.>"] storage: file replicas: 1 account: a # <-- Create stream using account A information ``` The following is an example of how to get Accounts working with a custom NATS Server URL and TLS certificates. ```sh # Install cert-manager kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.0/cert-manager.yaml # Install TLS certs cd examples/secure # Install certificate issuer kubectl apply -f issuer.yaml # Install account A cert kubectl apply -f nack-a-client-tls.yaml # Install server cert kubectl apply -f server-tls.yaml # Install nats-box cert kubectl apply -f client-tls.yaml # Install NATS cluster helm upgrade --install -f nats-helm.yaml nats nats/nats # Verify pods are healthy kubectl get pods # Install JetStream Controller from nack helm upgrade --install nack nats/nack --set jetstream.enabled=true # Verify pods are healthy kubectl get pods # Create account A resource kubectl apply -f nack/nats-account-a.yaml # Create stream using account A kubectl apply -f nack/nats-stream-foo-a.yaml # Create consumer using account A kubectl apply -f nack/nats-consumer-bar-a.yaml ``` After Accounts, Streams, and Consumers are created, let's log into the nats-box container to run the management CLI. ```sh # Get container shell kubectl exec -it deployment/nats-box -- /bin/sh -l ``` There should now be some Streams available, verify with `nats` command. ```sh # List streams nats stream ls ``` You can now publish messages on a Stream. ```sh # Push message nats pub foo hi ``` And pull messages from a Consumer. ```sh # Pull message nats consumer next foo bar ``` ### Local Development ```sh # First, build the jetstream controller. make jetstream-controller # Next, run the controller like this ./jetstream-controller -kubeconfig ~/.kube/config -s nats://localhost:4222 # Pro tip: jetstream-controller uses klog just like kubectl or kube-apiserver. # This means you can change the verbosity of logs with the -v flag. # # For example, this prints raw HTTP requests and responses. # ./jetstream-controller -v=10 # You'll probably want to start a local Jetstream-enabled NATS server, unless # you use a public one. nats-server -DV -js ``` Build Docker image ```sh make jetstream-controller-docker ver=1.2.3 ``` ## NATS Server Config Reloader This is a sidecar that you can use to automatically reload your NATS Server configuration file. ### Installing with Helm For more information see the [Chart repo](https://github.com/nats-io/k8s/tree/main/helm/charts/nats). ```sh helm repo add nats https://nats-io.github.io/k8s/helm/charts/ helm upgrade --install nats nats/nats ``` ### Configuring ```yaml reloader: enabled: true image: natsio/nats-server-config-reloader:0.16.1 pullPolicy: IfNotPresent ``` ### Local Development ```sh # First, build the config reloader. make nats-server-config-reloader # Next, run the reloader like this ./nats-server-config-reloader ``` Build Docker image ```sh make nats-server-config-reloader-docker ver=1.2.3 ``` ## NATS Boot Config A helper utility used during NATS server pod initialization to generate and manage boot-time configuration. ### Installing with Helm For more information see the [Chart repo](https://github.com/nats-io/k8s/tree/main/helm/charts/nats). ```sh helm repo add nats https://nats-io.github.io/k8s/helm/charts/ helm upgrade --install nats nats/nats ``` ### Configuring ```yaml bootconfig: image: natsio/nats-boot-config:0.16.1 pullPolicy: IfNotPresent ``` ### Local Development ```sh # First, build the project. make nats-boot-config # Next, run the project like this ./nats-boot-config ``` Build Docker image ```sh make nats-boot-config-docker ver=1.2.3 ``` ================================================ FILE: cicd/Dockerfile ================================================ #syntax=docker/dockerfile:1.13 ARG GO_APP FROM alpine:3.23.3 AS deps ARG GO_APP ARG GORELEASER_DIST_DIR=/go/src/dist ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT RUN mkdir -p /go/bin /go/src ${GORELEASER_DIST_DIR} COPY --from=build ${GORELEASER_DIST_DIR}/ ${GORELEASER_DIST_DIR} RUN < /etc/apt/sources.list.d/goreleaser.list apt-get update apt-get install -y goreleaser rm -rf /var/lib/apt/lists/* EOT FROM build ARG CI ARG PUSH ARG GITHUB_TOKEN ARG TAGS ARG VERSION COPY --from=src . /go/src RUN < 0 { for _, cert := range c.TLSCAs { fb, err := os.ReadFile(cert) if err != nil { return "", fmt.Errorf("error opening ca file %s: %v", cert, err) } b = append(b, fb...) } } if c.TLSCert != "" { fb, err := os.ReadFile(c.TLSCert) if err != nil { return "", fmt.Errorf("error opening cert file %s: %v", c.TLSCert, err) } b = append(b, fb...) } if c.TLSKey != "" { fb, err := os.ReadFile(c.TLSKey) if err != nil { return "", fmt.Errorf("error opening key file %s: %v", c.TLSKey, err) } b = append(b, fb...) } hash := sha256.New() hash.Write(b) return fmt.Sprintf("%x", hash.Sum(nil)), nil } type natsContextDefaults struct { Name string URL string TLSCAs []string TLSCert string TLSKey string TLSConfig *tls.Config } type pooledNatsConn struct { nc *nats.Conn cp *natsConnPool key string count uint64 closed bool } func (pc *pooledNatsConn) ReturnToPool() { pc.cp.Lock() pc.count-- if pc.count == 0 { if pooledConn, ok := pc.cp.cache[pc.key]; ok && pc == pooledConn { delete(pc.cp.cache, pc.key) } pc.closed = true pc.cp.Unlock() pc.nc.Close() return } pc.cp.Unlock() } type natsConnPool struct { sync.Mutex cache map[string]*pooledNatsConn logger *logrus.Logger group *singleflight.Group natsDefaults *natsContextDefaults natsOpts []nats.Option } func newNatsConnPool(logger *logrus.Logger, natsDefaults *natsContextDefaults, natsOpts []nats.Option) *natsConnPool { return &natsConnPool{ cache: map[string]*pooledNatsConn{}, group: &singleflight.Group{}, logger: logger, natsDefaults: natsDefaults, natsOpts: natsOpts, } } const getPooledConnMaxTries = 10 // Get returns a *pooledNatsConn func (cp *natsConnPool) Get(cfg *natsContext) (*pooledNatsConn, error) { if cfg == nil { return nil, fmt.Errorf("nats context must not be nil") } // copy cfg cfg = cfg.copy() // set defaults if cfg.Name == "" { cfg.Name = cp.natsDefaults.Name } if cfg.URL == "" { cfg.URL = cp.natsDefaults.URL } if len(cfg.TLSCAs) == 0 { cfg.TLSCAs = cp.natsDefaults.TLSCAs } if cfg.TLSCert == "" { cfg.TLSCert = cp.natsDefaults.TLSCert } if cfg.TLSKey == "" { cfg.TLSKey = cp.natsDefaults.TLSKey } // get hash key, err := cfg.hash() if err != nil { return nil, err } for i := 0; i < getPooledConnMaxTries; i++ { connection, err := cp.getPooledConn(key, cfg) if err != nil { return nil, err } cp.Lock() if connection.closed { // ReturnToPool closed this while lock not held, try again cp.Unlock() continue } // increment count out of the pool connection.count++ cp.Unlock() return connection, nil } return nil, fmt.Errorf("failed to get pooled connection after %d attempts", getPooledConnMaxTries) } // getPooledConn gets or establishes a *pooledNatsConn in a singleflight group, but does not increment its count func (cp *natsConnPool) getPooledConn(key string, cfg *natsContext) (*pooledNatsConn, error) { conn, err, _ := cp.group.Do(key, func() (interface{}, error) { cp.Lock() pooledConn, ok := cp.cache[key] if ok && pooledConn.nc.IsConnected() { cp.Unlock() return pooledConn, nil } cp.Unlock() opts := cp.natsOpts opts = append(opts, func(options *nats.Options) error { if cfg.Name != "" { options.Name = cfg.Name } if cfg.Token != "" { options.Token = cfg.Token } if cfg.Username != "" { options.User = cfg.Username } if cfg.Password != "" { options.Password = cfg.Password } return nil }) if cfg.JWT != "" && cfg.Seed != "" { opts = append(opts, nats.UserJWTAndSeed(cfg.JWT, cfg.Seed)) } if cfg.Nkey != "" { opt, err := nats.NkeyOptionFromSeed(cfg.Nkey) if err != nil { return nil, fmt.Errorf("unable to load nkey: %v", err) } opts = append(opts, opt) } if cfg.Credentials != "" { opts = append(opts, nats.UserCredentials(cfg.Credentials)) } if len(cfg.TLSCAs) > 0 { opts = append(opts, nats.RootCAs(cfg.TLSCAs...)) } if cfg.TLSCert != "" && cfg.TLSKey != "" { opts = append(opts, nats.ClientCert(cfg.TLSCert, cfg.TLSKey)) } nc, err := nats.Connect(cfg.URL, opts...) if err != nil { return nil, err } cp.logger.Infof("%s connected to NATS Deployment: %s", cfg.Name, nc.ConnectedAddr()) connection := &pooledNatsConn{ nc: nc, cp: cp, key: key, } cp.Lock() cp.cache[key] = connection cp.Unlock() return connection, err }) if err != nil { return nil, err } connection, ok := conn.(*pooledNatsConn) if !ok { return nil, fmt.Errorf("not a pooledNatsConn") } return connection, nil } ================================================ FILE: controllers/jetstream/conn_pool_test.go ================================================ package jetstream import ( "sync" "testing" "time" "github.com/nats-io/nats.go" natsservertest "github.com/nats-io/nats-server/v2/test" "github.com/sirupsen/logrus" testifyAssert "github.com/stretchr/testify/assert" ) func TestConnPool(t *testing.T) { t.Parallel() s := natsservertest.RunRandClientPortServer() defer s.Shutdown() o1 := &natsContext{ Name: "Client 1", } o2 := &natsContext{ Name: "Client 1", } o3 := &natsContext{ Name: "Client 2", } natsDefaults := &natsContextDefaults{ URL: s.ClientURL(), } natsOptions := []nats.Option{ nats.MaxReconnects(10240), } cp := newNatsConnPool(logrus.New(), natsDefaults, natsOptions) var c1, c2, c3 *pooledNatsConn var c1e, c2e, c3e error wg := &sync.WaitGroup{} wg.Add(3) go func() { c1, c1e = cp.Get(o1) wg.Done() }() go func() { c2, c2e = cp.Get(o2) wg.Done() }() go func() { c3, c3e = cp.Get(o3) wg.Done() }() wg.Wait() assert := testifyAssert.New(t) if assert.NoError(c1e) && assert.NoError(c2e) { assert.Same(c1, c2) } if assert.NoError(c3e) { assert.NotSame(c1, c3) assert.NotSame(c2, c3) } c1.ReturnToPool() c3.ReturnToPool() time.Sleep(1 * time.Second) assert.False(c1.nc.IsClosed()) assert.False(c2.nc.IsClosed()) assert.True(c3.nc.IsClosed()) c4, c4e := cp.Get(o1) if assert.NoError(c4e) { assert.Same(c2, c4) } c2.ReturnToPool() c4.ReturnToPool() time.Sleep(1 * time.Second) assert.True(c1.nc.IsClosed()) assert.True(c2.nc.IsClosed()) assert.True(c4.nc.IsClosed()) c5, c5e := cp.Get(o1) if assert.NoError(c5e) { assert.NotSame(c1, c5) } c5.ReturnToPool() time.Sleep(1 * time.Second) assert.True(c5.nc.IsClosed()) } ================================================ FILE: controllers/jetstream/consumer.go ================================================ package jetstream import ( "context" "errors" "fmt" "strconv" "time" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" typed "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" k8sapi "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" klog "k8s.io/klog/v2" ) func (c *Controller) runConsumerQueue() { for { processQueueNext(c.cnsQueue, c.RealJSMC, c.processConsumer) } } func (c *Controller) processConsumer(ns, name string, jsmClient jsmClientFunc) (err error) { cns, err := c.cnsLister.Consumers(ns).Get(name) if err != nil && k8serrors.IsNotFound(err) { return nil } else if err != nil { return err } return c.processConsumerObject(cns, jsmClient) } func (c *Controller) processConsumerObject(cns *apis.Consumer, jsm jsmClientFunc) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to process consumer: %w", err) } }() ns := cns.Namespace spec := cns.Spec ifc := c.ji.Consumers(ns) acc, err := c.getAccountOverrides(spec.Account, ns) if err != nil { return err } defer func() { if err == nil { return } if _, serr := setConsumerErrored(c.ctx, cns, ifc, err); serr != nil { err = fmt.Errorf("%s: %w", err, serr) } }() type operator func(ctx context.Context, c jsmClient, spec apis.ConsumerSpec) (err error) natsClientUtil := func(op operator) error { return c.runWithJsmc(jsm, acc, &jsmcSpecOverrides{ servers: spec.Servers, tls: spec.TLS, creds: spec.Creds, nkey: spec.Nkey, }, cns, func(jsmc jsmClient) error { return op(c.ctx, jsmc, spec) }) } deleteOK := cns.GetDeletionTimestamp() != nil newGeneration := cns.Generation != cns.Status.ObservedGeneration consumerOK := true err = natsClientUtil(consumerExists) var apierr jsmapi.ApiError if errors.As(err, &apierr) && apierr.NotFoundError() { consumerOK = false } else if err != nil { return err } updateOK := (consumerOK && !deleteOK && newGeneration) createOK := (!consumerOK && !deleteOK) || (!updateOK && !deleteOK && newGeneration) switch { case createOK: c.normalEvent(cns, "Creating", fmt.Sprintf("Creating consumer %q on stream %q", spec.DurableName, spec.StreamName)) if err := natsClientUtil(createConsumer); err != nil { return err } if _, err := setConsumerOK(c.ctx, cns, ifc); err != nil { return err } c.normalEvent(cns, "Created", fmt.Sprintf("Created consumer %q on stream %q", spec.DurableName, spec.StreamName)) case updateOK: if cns.Spec.PreventUpdate { c.normalEvent(cns, "SkipUpdate", fmt.Sprintf("Skip updating consumer %q on stream %q", spec.DurableName, spec.StreamName)) if _, err := setConsumerOK(c.ctx, cns, ifc); err != nil { return err } return nil } c.normalEvent(cns, "Updating", fmt.Sprintf("Updating consumer %q on stream %q", spec.DurableName, spec.StreamName)) if err := natsClientUtil(updateConsumer); err != nil { return err } if _, err := setConsumerOK(c.ctx, cns, ifc); err != nil { return err } c.normalEvent(cns, "Updated", fmt.Sprintf("Updated consumer %q on stream %q", spec.DurableName, spec.StreamName)) case deleteOK: if cns.Spec.PreventDelete { c.normalEvent(cns, "SkipDelete", fmt.Sprintf("Skip deleting consumer %q on stream %q", spec.DurableName, spec.StreamName)) if _, err := setConsumerOK(c.ctx, cns, ifc); err != nil { return err } return nil } c.normalEvent(cns, "Deleting", fmt.Sprintf("Deleting consumer %q on stream %q", spec.DurableName, spec.StreamName)) if err := natsClientUtil(deleteConsumer); err != nil { return err } default: c.normalEvent(cns, "Noop", fmt.Sprintf("Nothing done for consumer %q (prevent-delete=%v, prevent-update=%v)", spec.DurableName, spec.PreventDelete, spec.PreventUpdate, )) if _, err := setConsumerOK(c.ctx, cns, ifc); err != nil { return err } } return nil } func consumerExists(ctx context.Context, c jsmClient, spec apis.ConsumerSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to check if consumer exists: %w", err) } }() _, err = c.LoadConsumer(ctx, spec.StreamName, spec.DurableName) return err } func createConsumer(ctx context.Context, c jsmClient, spec apis.ConsumerSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to create consumer %q on stream %q: %w", spec.DurableName, spec.StreamName, err) } }() opts, err := consumerSpecToOpts(spec) if err != nil { return } _, err = c.NewConsumer(ctx, spec.StreamName, opts) return } func updateConsumer(ctx context.Context, c jsmClient, spec apis.ConsumerSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to update consumer %q on stream %q: %w", spec.DurableName, spec.StreamName, err) } }() js, err := c.LoadConsumer(ctx, spec.StreamName, spec.DurableName) if err != nil { return } opts, err := consumerSpecToOpts(spec) if err != nil { return } err = js.UpdateConfiguration(opts...) return } func consumerSpecToOpts(spec apis.ConsumerSpec) ([]jsm.ConsumerOption, error) { opts := []jsm.ConsumerOption{ jsm.DurableName(spec.DurableName), jsm.DeliverySubject(spec.DeliverSubject), jsm.RateLimitBitsPerSecond(uint64(spec.RateLimitBps)), jsm.MaxAckPending(uint(spec.MaxAckPending)), jsm.ConsumerDescription(spec.Description), jsm.DeliverGroup(spec.DeliverGroup), jsm.MaxWaiting(uint(spec.MaxWaiting)), jsm.MaxRequestBatch(uint(spec.MaxRequestBatch)), jsm.MaxRequestMaxBytes(spec.MaxRequestMaxBytes), jsm.ConsumerOverrideReplicas(spec.Replicas), } if spec.FilterSubject != "" && len(spec.FilterSubjects) > 0 { return nil, fmt.Errorf("cannot specify both FilterSubject and FilterSubjects") } if spec.FilterSubject != "" { opts = append(opts, jsm.FilterStreamBySubject(spec.FilterSubject)) } else if len(spec.FilterSubjects) > 0 { opts = append(opts, jsm.FilterStreamBySubject(spec.FilterSubjects...)) } switch spec.DeliverPolicy { case "all": opts = append(opts, jsm.DeliverAllAvailable()) case "last": opts = append(opts, jsm.StartWithLastReceived()) case "new": opts = append(opts, jsm.StartWithNextReceived()) case "byStartSequence": opts = append(opts, jsm.StartAtSequence(uint64(spec.OptStartSeq))) case "byStartTime": if spec.OptStartTime == "" { return nil, fmt.Errorf("'optStartTime' is required for deliver policy 'byStartTime'") } t, err := time.Parse(time.RFC3339, spec.OptStartTime) if err != nil { return nil, err } opts = append(opts, jsm.StartAtTime(t)) case "lastPerSubject": opts = append(opts, jsm.DeliverLastPerSubject()) case "": default: return nil, fmt.Errorf("invalid value for 'deliverPolicy': '%s'. Must be one of 'all', 'last', 'new', 'lastPerSubject', 'byStartSequence', 'byStartTime'", spec.DeliverPolicy) } switch spec.AckPolicy { case "none": opts = append(opts, jsm.AcknowledgeNone()) case "all": opts = append(opts, jsm.AcknowledgeAll()) case "explicit": opts = append(opts, jsm.AcknowledgeExplicit()) case "": default: return nil, fmt.Errorf("invalid value for 'ackPolicy': '%s'. Must be one of 'none', 'all', 'explicit'", spec.AckPolicy) } if spec.AckWait != "" { d, err := time.ParseDuration(spec.AckWait) if err != nil { return nil, err } opts = append(opts, jsm.AckWait(d)) } switch spec.ReplayPolicy { case "instant": opts = append(opts, jsm.ReplayInstantly()) case "original": opts = append(opts, jsm.ReplayAsReceived()) case "": default: return nil, fmt.Errorf("invalid value for 'replayPolicy': '%s'. Must be one of 'instant', 'original'", spec.ReplayPolicy) } if spec.SampleFreq != "" { n, err := strconv.Atoi(spec.SampleFreq) if err != nil { return nil, err } opts = append(opts, jsm.SamplePercent(n)) } if spec.FlowControl { opts = append(opts, jsm.PushFlowControl()) } if spec.HeartbeatInterval != "" { d, err := time.ParseDuration(spec.HeartbeatInterval) if err != nil { return nil, err } opts = append(opts, jsm.IdleHeartbeat(d)) } if len(spec.BackOff) > 0 { backoffs := make([]time.Duration, 0) for _, backoff := range spec.BackOff { dur, err := time.ParseDuration(backoff) if err != nil { return nil, err } backoffs = append(backoffs, dur) } opts = append(opts, jsm.BackoffIntervals(backoffs...)) } if spec.HeadersOnly { opts = append(opts, jsm.DeliverHeadersOnly()) } else { opts = append(opts, jsm.DeliverBodies()) } if spec.MaxRequestExpires != "" { dur, err := time.ParseDuration(spec.MaxRequestExpires) if err != nil { return nil, err } opts = append(opts, jsm.MaxRequestExpires(dur)) } if spec.MemStorage { opts = append(opts, jsm.ConsumerOverrideMemoryStorage()) } if spec.MaxDeliver != 0 { opts = append(opts, jsm.MaxDeliveryAttempts(spec.MaxDeliver)) } if spec.Metadata != nil { opts = append(opts, jsm.ConsumerMetadata(spec.Metadata)) } if spec.InactiveThreshold != "" { dur, err := time.ParseDuration(spec.InactiveThreshold) if err != nil { return nil, err } opts = append(opts, jsm.InactiveThreshold(dur)) } // Handle PauseUntil for pausing consumer if spec.PauseUntil != "" { t, err := time.Parse(time.RFC3339, spec.PauseUntil) if err != nil { return nil, fmt.Errorf("invalid pauseUntil time: %w", err) } opts = append(opts, jsm.PauseUntil(t)) } // Handle PriorityPolicy with PriorityGroups and PinnedTTL switch spec.PriorityPolicy { case "", "none": // Default is none, no need to set case "pinned_client": if spec.PinnedTTL != "" { dur, err := time.ParseDuration(spec.PinnedTTL) if err != nil { return nil, fmt.Errorf("invalid pinnedTTL duration: %w", err) } opts = append(opts, jsm.PinnedClientPriorityGroups(dur, spec.PriorityGroups...)) } case "overflow": opts = append(opts, jsm.OverflowPriorityGroups(spec.PriorityGroups...)) case "prioritized": opts = append(opts, jsm.PrioritizedPriorityGroups(spec.PriorityGroups...)) default: return nil, fmt.Errorf("invalid priority policy: %s", spec.PriorityPolicy) } return opts, nil } func deleteConsumer(ctx context.Context, c jsmClient, spec apis.ConsumerSpec) (err error) { stream, consumer := spec.StreamName, spec.DurableName defer func() { if err != nil { err = fmt.Errorf("failed to delete consumer %q on stream %q: %w", consumer, stream, err) } }() if spec.PreventDelete { klog.Infof("Consumer %q is configured to preventDelete on stream %q:", stream, consumer) return nil } var apierr jsmapi.ApiError cn, err := c.LoadConsumer(ctx, stream, consumer) if errors.As(err, &apierr) && apierr.NotFoundError() { return nil } else if err != nil { return err } return cn.Delete() } func setConsumerOK(ctx context.Context, s *apis.Consumer, i typed.ConsumerInterface) (*apis.Consumer, error) { sc := s.DeepCopy() sc.Status.ObservedGeneration = s.Generation sc.Status.Conditions = UpsertCondition(sc.Status.Conditions, apis.Condition{ Type: readyCondType, Status: k8sapi.ConditionTrue, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Created", Message: "Consumer successfully created", }) var res *apis.Consumer err := retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res, err = i.UpdateStatus(ctx, sc, k8smeta.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to set consumer %q status: %w", s.Spec.DurableName, err) } return nil }) return res, err } func setConsumerErrored(ctx context.Context, s *apis.Consumer, sif typed.ConsumerInterface, err error) (*apis.Consumer, error) { if err == nil { return s, nil } sc := s.DeepCopy() sc.Status.Conditions = UpsertCondition(sc.Status.Conditions, apis.Condition{ Type: readyCondType, Status: k8sapi.ConditionFalse, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Errored", Message: err.Error(), }) var res *apis.Consumer err = retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res, err = sif.UpdateStatus(ctx, sc, k8smeta.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to set consumer errored status: %w", err) } return nil }) return res, err } ================================================ FILE: controllers/jetstream/consumer_test.go ================================================ package jetstream import ( "context" "errors" "strings" "testing" "time" jsmapi "github.com/nats-io/jsm.go/api" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" clientsetfake "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/fake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8sclientsetfake "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/record" ) func TestProcessConsumer(t *testing.T) { t.Parallel() updateObject := func(a k8stesting.Action) (handled bool, o runtime.Object, err error) { ua, ok := a.(k8stesting.UpdateAction) if !ok { return false, nil, nil } return true, ua.GetObject(), nil } t.Run("create consumer", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 2 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-consumer" informer := ctrl.informerFactory.Jetstream().V1beta2().Consumers() err := informer.Informer().GetStore().Add(&apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 1, }, Spec: apis.ConsumerSpec{ DurableName: name, DeliverPolicy: "byStartTime", OptStartTime: time.Now().Format(time.RFC3339), AckPolicy: "explicit", AckWait: "1m", ReplayPolicy: "original", SampleFreq: "50", HeartbeatInterval: "30s", BackOff: []string{"500ms", "1s"}, HeadersOnly: true, MaxRequestExpires: "5m", MemStorage: true, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "consumers", updateObject) notFoundErr := jsmapi.ApiError{Code: 404} jsmc := &mockJsmClient{ loadConsumerErr: notFoundErr, newConsumerErr: nil, newConsumer: &mockConsumer{}, } if err := ctrl.processConsumer(ns, name, func(n *natsContext) (jsmClient, error) { return jsmc, nil }); err != nil { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } for i := 0; i < len(rec.Events); i++ { gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Creat") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Creating/Created...") } } }) t.Run("create consumer, invalid configuration", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 1 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-consumer" informer := ctrl.informerFactory.Jetstream().V1beta2().Consumers() err := informer.Informer().GetStore().Add(&apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 1, }, Spec: apis.ConsumerSpec{ DurableName: name, DeliverPolicy: "invalid", }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "consumers", updateObject) notFoundErr := jsmapi.ApiError{Code: 404} jsmc := &mockJsmClient{ loadConsumerErr: notFoundErr, newConsumerErr: nil, newConsumer: &mockConsumer{}, } if err := ctrl.processConsumer(ns, name, testWrapJSMC(jsmc)); err == nil || !strings.Contains(err.Error(), `failed to create consumer "my-consumer" on stream `) { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Creating") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Creating...") } }) t.Run("update consumer", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 2 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-consumer" informer := ctrl.informerFactory.Jetstream().V1beta2().Consumers() err := informer.Informer().GetStore().Add(&apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 2, }, Spec: apis.ConsumerSpec{ DurableName: name, }, Status: apis.Status{ ObservedGeneration: 1, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "consumers", updateObject) jsmc := &mockJsmClient{ loadConsumerErr: nil, loadConsumer: &mockConsumer{}, } if err := ctrl.processConsumer(ns, name, testWrapJSMC(jsmc)); err != nil { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } for i := 0; i < len(rec.Events); i++ { gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Updat") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Updating/Updated...") } } }) t.Run("delete consumer", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 1 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ts := k8smeta.Unix(1600216923, 0) ns, name := "default", "my-consumer" informer := ctrl.informerFactory.Jetstream().V1beta2().Consumers() err := informer.Informer().GetStore().Add(&apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, DeletionTimestamp: &ts, }, Spec: apis.ConsumerSpec{ DurableName: name, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "consumers", updateObject) jsmc := &mockJsmClient{ loadConsumerErr: nil, loadConsumer: &mockConsumer{}, } if err := ctrl.processConsumer(ns, name, testWrapJSMC(jsmc)); err != nil { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Deleting") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Deleting...") } }) t.Run("process error", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 1 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-consumer" informer := ctrl.informerFactory.Jetstream().V1beta2().Consumers() err := informer.Informer().GetStore().Add(&apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 1, }, Spec: apis.ConsumerSpec{ DurableName: name, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "consumers", func(a k8stesting.Action) (handled bool, o runtime.Object, err error) { ua, ok := a.(k8stesting.UpdateAction) if !ok { return false, nil, nil } obj := ua.GetObject() str, ok := obj.(*apis.Consumer) if !ok { t.Error("unexpected object type") t.Fatalf("got=%T; want=%T", obj, &apis.Consumer{}) } if got, want := len(str.Status.Conditions), 1; got != want { t.Error("unexpected number of conditions") t.Fatalf("got=%d; want=%d", got, want) } if got, want := str.Status.Conditions[0].Reason, "Errored"; got != want { t.Error("unexpected condition reason") t.Fatalf("got=%s; want=%s", got, want) } return true, obj, nil }) jsmc := &mockJsmClient{ loadConsumerErr: errors.New("failed to load consumer"), } if err := ctrl.processConsumer(ns, name, testWrapJSMC(jsmc)); err == nil { t.Fatal("unexpected success") } }) } func TestConsumerSpecToOpts(t *testing.T) { tests := map[string]struct { name string given apis.ConsumerSpec expected jsmapi.ConsumerConfig errCheck func(t *testing.T, err error) }{ "valid consumer spec": { given: apis.ConsumerSpec{ DurableName: "my-consumer", DeliverPolicy: "byStartSequence", OptStartSeq: 10, AckPolicy: "explicit", AckWait: "1m", ReplayPolicy: "original", SampleFreq: "50", HeartbeatInterval: "30s", BackOff: []string{"500ms", "1s"}, HeadersOnly: true, MaxRequestExpires: "5m", MemStorage: true, }, expected: jsmapi.ConsumerConfig{ AckPolicy: jsmapi.AckExplicit, AckWait: 1 * time.Minute, DeliverPolicy: jsmapi.DeliverByStartSequence, Durable: "my-consumer", Heartbeat: 30 * time.Second, BackOff: []time.Duration{500 * time.Millisecond, 1 * time.Second}, OptStartSeq: 10, ReplayPolicy: jsmapi.ReplayOriginal, SampleFrequency: "50%", HeadersOnly: true, MaxRequestExpires: 5 * time.Minute, MemoryStorage: true, }, }, "valid consumer spec, defaults only": { given: apis.ConsumerSpec{ DurableName: "my-consumer", }, expected: jsmapi.ConsumerConfig{ Durable: "my-consumer", }, }, "invalid deliver policy value": { given: apis.ConsumerSpec{ DurableName: "my-consumer", DeliverPolicy: "invalid", }, errCheck: func(t *testing.T, err error) { require.Error(t, err) require.Contains(t, err.Error(), "invalid value for 'deliverPolicy': 'invalid'") }, }, "missing start time for deliver policy byStartTime": { given: apis.ConsumerSpec{ DurableName: "my-consumer", DeliverPolicy: "byStartTime", }, errCheck: func(t *testing.T, err error) { require.Error(t, err) require.Contains(t, err.Error(), "'optStartTime' is required for deliver policy 'byStartTime'") }, }, "deliver policy lastPerSubject": { given: apis.ConsumerSpec{ DurableName: "my-consumer", DeliverPolicy: "lastPerSubject", }, expected: jsmapi.ConsumerConfig{ Durable: "my-consumer", DeliverPolicy: jsmapi.DeliverLastPerSubject, }, }, "invalid ack policy": { given: apis.ConsumerSpec{ DurableName: "my-consumer", AckPolicy: "invalid", }, errCheck: func(t *testing.T, err error) { require.Error(t, err) require.Contains(t, err.Error(), "invalid value for 'ackPolicy': 'invalid'") }, }, "invalid replay policy": { given: apis.ConsumerSpec{ DurableName: "my-consumer", ReplayPolicy: "invalid", }, errCheck: func(t *testing.T, err error) { require.Error(t, err) require.Contains(t, err.Error(), "invalid value for 'replayPolicy': 'invalid'") }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { res, err := consumerSpecToOpts(test.given) if test.errCheck != nil { test.errCheck(t, err) return } require.NoError(t, err) var config jsmapi.ConsumerConfig for _, opt := range res { err := opt(&config) require.NoError(t, err) } assert.Equal(t, test.expected, config) }) } } func testWrapJSMC(jsm jsmClient) jsmClientFunc { return func(n *natsContext) (jsmClient, error) { return jsm, nil } } ================================================ FILE: controllers/jetstream/controller.go ================================================ // Copyright 2020-2022 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jetstream import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" "github.com/nats-io/nats.go" "github.com/sirupsen/logrus" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" clientset "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" typed "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" informers "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions" listers "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" k8sapi "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" k8serrors "k8s.io/apimachinery/pkg/api/errors" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8styped "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" klog "k8s.io/klog/v2" ) const ( // maxQueueRetries is the max times an item will be retried. An item will // be pulled maxQueueRetries+1 times from the queue. On pull number // maxQueueRetries+1, if it fails again, it won't be retried. maxQueueRetries = 10 // readyCondType is the Ready condition type. readyCondType = "Ready" ) type Options struct { Ctx context.Context KubeIface kubernetes.Interface JetstreamIface clientset.Interface NATSClientName string NATSCredentials string NATSNKey string NATSServerURL string NATSCA string NATSCertificate string NATSKey string NATSTLSFirst bool Namespace string CRDConnect bool CleanupPeriod time.Duration ReadOnly bool Recorder record.EventRecorder } type Controller struct { ctx context.Context opts Options connPool *natsConnPool ki k8styped.CoreV1Interface ji typed.JetstreamV1beta2Interface informerFactory informers.SharedInformerFactory rec record.EventRecorder strLister listers.StreamLister strSynced cache.InformerSynced strQueue workqueue.TypedRateLimitingInterface[any] cnsLister listers.ConsumerLister cnsSynced cache.InformerSynced cnsQueue workqueue.TypedRateLimitingInterface[any] accLister listers.AccountLister // Informers for unsupported resources (KeyValue and ObjectStore) // These are only used to emit warnings in legacy mode kvLister listers.KeyValueLister kvSynced cache.InformerSynced osLister listers.ObjectStoreLister osSynced cache.InformerSynced // cacheDir is where the downloaded TLS certs from the server // will be stored temporarily. cacheDir string } func NewController(opt Options) *Controller { resyncPeriod := 30 * time.Second informerFactory := informers.NewSharedInformerFactoryWithOptions(opt.JetstreamIface, resyncPeriod, informers.WithNamespace(opt.Namespace)) streamInformer := informerFactory.Jetstream().V1beta2().Streams() consumerInformer := informerFactory.Jetstream().V1beta2().Consumers() accountInformer := informerFactory.Jetstream().V1beta2().Accounts() keyValueInformer := informerFactory.Jetstream().V1beta2().KeyValues() objectStoreInformer := informerFactory.Jetstream().V1beta2().ObjectStores() if opt.Recorder == nil { utilruntime.Must(scheme.AddToScheme(k8sscheme.Scheme)) eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(klog.Infof) eventBroadcaster.StartRecordingToSink(&k8styped.EventSinkImpl{ Interface: opt.KubeIface.CoreV1().Events(""), }) opt.Recorder = eventBroadcaster.NewRecorder(k8sscheme.Scheme, k8sapi.EventSource{ Component: "jetstream-controller", }) } if opt.NATSClientName == "" { opt.NATSClientName = "jetstream-controller" } ji := opt.JetstreamIface.JetstreamV1beta2() streamQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any](), "Streams") consumerQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any](), "Consumers") streamInformer.Informer().AddEventHandler( eventHandlers( streamQueue, ), ) consumerInformer.Informer().AddEventHandler( eventHandlers( consumerQueue, ), ) // Add warning handlers for unsupported resources in legacy mode keyValueInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if kv, ok := obj.(*apis.KeyValue); ok { klog.Warningf("KeyValue resource %s/%s detected but not supported in legacy mode. The NATS KV bucket will NOT be created. Enable --control-loop mode to use KeyValue resources.", kv.Namespace, kv.Name) opt.Recorder.Event(kv, k8sapi.EventTypeWarning, "NotSupported", "KeyValue resources require --control-loop mode. The NATS KV bucket will NOT be created.") } }, UpdateFunc: func(oldObj, newObj interface{}) { if kv, ok := newObj.(*apis.KeyValue); ok { klog.Warningf("KeyValue resource %s/%s updated but not supported in legacy mode. Changes will NOT be applied to NATS. Enable --control-loop mode to use KeyValue resources.", kv.Namespace, kv.Name) opt.Recorder.Event(kv, k8sapi.EventTypeWarning, "NotSupported", "KeyValue resources require --control-loop mode. Updates will NOT be applied.") } }, }, ) objectStoreInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if os, ok := obj.(*apis.ObjectStore); ok { klog.Warningf("ObjectStore resource %s/%s detected but not supported in legacy mode. The NATS object store will NOT be created. Enable --control-loop mode to use ObjectStore resources.", os.Namespace, os.Name) opt.Recorder.Event(os, k8sapi.EventTypeWarning, "NotSupported", "ObjectStore resources require --control-loop mode. The NATS object store will NOT be created.") } }, UpdateFunc: func(oldObj, newObj interface{}) { if os, ok := newObj.(*apis.ObjectStore); ok { klog.Warningf("ObjectStore resource %s/%s updated but not supported in legacy mode. Changes will NOT be applied to NATS. Enable --control-loop mode to use ObjectStore resources.", os.Namespace, os.Name) opt.Recorder.Event(os, k8sapi.EventTypeWarning, "NotSupported", "ObjectStore resources require --control-loop mode. Updates will NOT be applied.") } }, }, ) cacheDir, err := os.MkdirTemp(".", "nack") if err != nil { panic(err) } defer os.RemoveAll(cacheDir) return &Controller{ ctx: opt.Ctx, opts: opt, ki: opt.KubeIface.CoreV1(), ji: ji, informerFactory: informerFactory, rec: opt.Recorder, strLister: streamInformer.Lister(), strSynced: streamInformer.Informer().HasSynced, strQueue: streamQueue, cnsLister: consumerInformer.Lister(), cnsSynced: consumerInformer.Informer().HasSynced, cnsQueue: consumerQueue, accLister: accountInformer.Lister(), kvLister: keyValueInformer.Lister(), kvSynced: keyValueInformer.Informer().HasSynced, osLister: objectStoreInformer.Lister(), osSynced: objectStoreInformer.Informer().HasSynced, cacheDir: cacheDir, } } func (c *Controller) Run() error { // Connect to NATS. opts := make([]nats.Option, 0) // Always attempt to have a connection to NATS. opts = append(opts, nats.MaxReconnects(-1)) if c.opts.NATSTLSFirst { opts = append(opts, nats.TLSHandshakeFirst()) } natsCtxDefaults := &natsContextDefaults{Name: c.opts.NATSClientName} if !c.opts.CRDConnect { // Use JWT/NKEYS based credentials if present. if c.opts.NATSCredentials != "" { opts = append(opts, nats.UserCredentials(c.opts.NATSCredentials)) } else if c.opts.NATSNKey != "" { opt, err := nats.NkeyOptionFromSeed(c.opts.NATSNKey) if err != nil { return nil } opts = append(opts, opt) } if c.opts.NATSCertificate != "" && c.opts.NATSKey != "" { natsCtxDefaults.TLSCert = c.opts.NATSCertificate natsCtxDefaults.TLSKey = c.opts.NATSKey } if c.opts.NATSCA != "" { natsCtxDefaults.TLSCAs = []string{c.opts.NATSCA} } natsCtxDefaults.URL = c.opts.NATSServerURL ncp := newNatsConnPool(logrus.New(), natsCtxDefaults, opts) pooledNc, err := ncp.Get(&natsContext{}) if err != nil { return fmt.Errorf("failed to connect to nats: %w", err) } pooledNc.ReturnToPool() c.connPool = ncp } else { c.connPool = newNatsConnPool(logrus.New(), natsCtxDefaults, opts) } defer utilruntime.HandleCrash() defer c.strQueue.ShutDown() defer c.cnsQueue.ShutDown() c.informerFactory.Start(c.ctx.Done()) if !cache.WaitForCacheSync(c.ctx.Done(), c.strSynced) { return fmt.Errorf("failed to wait for stream cache sync") } if !cache.WaitForCacheSync(c.ctx.Done(), c.cnsSynced) { return fmt.Errorf("failed to wait for consumer cache sync") } // Also wait for KeyValue and ObjectStore caches to sync so we can emit warnings if !cache.WaitForCacheSync(c.ctx.Done(), c.kvSynced) { return fmt.Errorf("failed to wait for keyvalue cache sync") } if !cache.WaitForCacheSync(c.ctx.Done(), c.osSynced) { return fmt.Errorf("failed to wait for objectstore cache sync") } go wait.Until(c.runStreamQueue, time.Second, c.ctx.Done()) go wait.Until(c.runConsumerQueue, time.Second, c.ctx.Done()) go c.cleanupStreams() go c.cleanupConsumers() <-c.ctx.Done() // Gracefully shutdown. return nil } // RealJSMC creates a new JSM client from pooled nats connections // Providing a blank string for servers, defaults to c.opts.NATSServerUrls // call deferred jsmC.Close() on returned instance to return the nats connection to pool func (c *Controller) RealJSMC(cfg *natsContext) (jsmClient, error) { if cfg == nil { cfg = &natsContext{} } pooledNc, err := c.connPool.Get(cfg) if err != nil { return nil, err } jm, err := jsm.New(pooledNc.nc) if err != nil { return nil, err } jsmc := &realJsmClient{pooledNc: pooledNc, jm: jm} return jsmc, nil } func selectMissingStreamsFromList(prev, cur map[string]*apis.Stream) []*apis.Stream { var deleted []*apis.Stream for name, ps := range prev { if _, ok := cur[name]; !ok { deleted = append(deleted, ps) } } return deleted } func streamsMap(ss []*apis.Stream) map[string]*apis.Stream { m := make(map[string]*apis.Stream) for _, s := range ss { m[fmt.Sprintf("%s/%s", s.Namespace, s.Name)] = s } return m } func (c *Controller) cleanupStreams() error { if c.opts.ReadOnly { return nil } tick := time.NewTicker(c.opts.CleanupPeriod) defer tick.Stop() // Track the Stream CRDs that may have been created. var prevStreams map[string]*apis.Stream for { select { case <-c.ctx.Done(): return c.ctx.Err() case <-tick.C: streams, err := c.strLister.List(labels.Everything()) if err != nil { klog.Infof("failed to list streams for cleanup: %s", err) continue } sm := streamsMap(streams) missing := selectMissingStreamsFromList(prevStreams, sm) for _, s := range missing { // A stream that we were tracking but that for some reason // was not part of the latest list shared by informer. // Need to double check whether the stream is present before // considering deletion. klog.Infof("stream %s/%s might be missing, looking it up...", s.Namespace, s.Name) ctx, done := context.WithTimeout(context.Background(), 10*time.Second) defer done() _, err := c.ji.Streams(s.Namespace).Get(ctx, s.Name, k8smeta.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { klog.Infof("stream %s/%s was not found anymore, deleting from JetStream", s.Namespace, s.Name) t := k8smeta.NewTime(time.Now()) s.DeletionTimestamp = &t if err := c.processStreamObject(s, c.RealJSMC); err != nil && !k8serrors.IsNotFound(err) { klog.Infof("failed to delete stream %s/%s: %s", s.Namespace, s.Name, err) continue } klog.Infof("deleted stream %s/%s from JetStream", s.Namespace, s.Name) } else { klog.Warningf("error looking up stream %s/%s", s.Namespace, s.Name) } } else { klog.Infof("found stream %s/%s, no further action needed", s.Namespace, s.Name) } } prevStreams = sm } } } func selectMissingConsumersFromList(prev, cur map[string]*apis.Consumer) []*apis.Consumer { var deleted []*apis.Consumer for name, ps := range prev { if _, ok := cur[name]; !ok { deleted = append(deleted, ps) } } return deleted } func consumerMap(cs []*apis.Consumer) map[string]*apis.Consumer { m := make(map[string]*apis.Consumer) for _, c := range cs { m[fmt.Sprintf("%s/%s", c.Namespace, c.Name)] = c } return m } func (c *Controller) cleanupConsumers() error { if c.opts.ReadOnly { return nil } tick := time.NewTicker(c.opts.CleanupPeriod) defer tick.Stop() // Track consumers that may have been deleted. var prevConsumers map[string]*apis.Consumer for { select { case <-c.ctx.Done(): return c.ctx.Err() case <-tick.C: consumers, err := c.cnsLister.List(labels.Everything()) if err != nil { klog.Infof("failed to list consumers for cleanup: %s", err) continue } cm := consumerMap(consumers) missing := selectMissingConsumersFromList(prevConsumers, cm) for _, cns := range missing { // A consumer that we were tracking but that for some reason // was not part of the latest list shared by informer. // Need to double check whether the consumer is present before // considering deletion. klog.Infof("consumer %s/%s might be missing, looking it up...", cns.Namespace, cns.Name) ctx, done := context.WithTimeout(context.Background(), 10*time.Second) defer done() _, err := c.ji.Consumers(cns.Namespace).Get(ctx, cns.Name, k8smeta.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { klog.Infof("consumer %s/%s was not found anymore, deleting from JetStream", cns.Namespace, cns.Name) t := k8smeta.NewTime(time.Now()) cns.DeletionTimestamp = &t if err := c.processConsumerObject(cns, c.RealJSMC); err != nil && !k8serrors.IsNotFound(err) { klog.Infof("failed to delete consumer %s/%s: %s", cns.Namespace, cns.Name, err) continue } klog.Infof("deleted consumer %s/%s from JetStream", cns.Namespace, cns.Name) } else { klog.Warningf("error looking up consumer %s/%s", cns.Namespace, cns.Name) } } else { klog.Infof("found consumer %s/%s, no further action needed", cns.Namespace, cns.Name) } } prevConsumers = cm } } } func (c *Controller) normalEvent(o runtime.Object, reason, message string) { if c.rec != nil { c.rec.Event(o, k8sapi.EventTypeNormal, reason, message) } } func (c *Controller) warningEvent(o runtime.Object, reason, message string) { if c.rec != nil { c.rec.Event(o, k8sapi.EventTypeWarning, reason, message) } } type accountOverrides struct { remoteClientCert string remoteClientKey string remoteRootCA string servers []string userCreds string nkey string user string password string token string } func (c *Controller) getAccountOverrides(account string, ns string) (*accountOverrides, error) { overrides := &accountOverrides{} if account == "" || !c.opts.CRDConnect { return overrides, nil } // Lookup the account using the REST client. ctx, done := context.WithTimeout(context.Background(), 5*time.Second) defer done() acc, err := c.ji.Accounts(ns).Get(ctx, account, k8smeta.GetOptions{}) if err != nil { return nil, err } overrides.servers = acc.Spec.Servers // Lookup the TLS secrets if acc.Spec.TLS != nil && acc.Spec.TLS.Secret != nil { secretName := acc.Spec.TLS.Secret.Name secret, err := c.ki.Secrets(ns).Get(c.ctx, secretName, k8smeta.GetOptions{}) if err != nil { return nil, err } // Write this to the cacheDir. accDir := filepath.Join(c.cacheDir, ns, account) if err := os.MkdirAll(accDir, 0o755); err != nil { return nil, err } var certData, keyData []byte var certPath, keyPath string for k, v := range secret.Data { switch k { case acc.Spec.TLS.ClientCert: certPath = filepath.Join(accDir, k) certData = v case acc.Spec.TLS.ClientKey: keyPath = filepath.Join(accDir, k) keyData = v case acc.Spec.TLS.RootCAs: overrides.remoteRootCA = filepath.Join(accDir, k) if err := os.WriteFile(overrides.remoteRootCA, v, 0o644); err != nil { return nil, err } } } if certData != nil && keyData != nil { overrides.remoteClientCert = certPath overrides.remoteClientKey = keyPath if err := os.WriteFile(certPath, certData, 0o644); err != nil { return nil, err } if err := os.WriteFile(keyPath, keyData, 0o644); err != nil { return nil, err } } } // Lookup the UserCredentials. if acc.Spec.Creds != nil && acc.Spec.Creds.Secret != nil { secretName := acc.Spec.Creds.Secret.Name secret, err := c.ki.Secrets(ns).Get(c.ctx, secretName, k8smeta.GetOptions{}) if err != nil { return nil, err } // Write the user credentials to the cache dir. accDir := filepath.Join(c.cacheDir, ns, account) if err := os.MkdirAll(accDir, 0o755); err != nil { return nil, err } if credsBytes, ok := secret.Data[acc.Spec.Creds.File]; ok { overrides.userCreds = filepath.Join(accDir, acc.Spec.Creds.File) if err := os.WriteFile(overrides.userCreds, credsBytes, 0o644); err != nil { return nil, err } } } // Lookup the NKey seed. if acc.Spec.NKey != nil && acc.Spec.NKey.Secret != nil { secretName := acc.Spec.NKey.Secret.Name secret, err := c.ki.Secrets(ns).Get(c.ctx, secretName, k8smeta.GetOptions{}) if err != nil { return nil, err } if nkeyBytes, ok := secret.Data[acc.Spec.NKey.Seed]; ok { overrides.nkey = string(nkeyBytes) } } // Lookup the Token. if acc.Spec.Token != nil { secretName := acc.Spec.Token.Secret.Name secret, err := c.ki.Secrets(ns).Get(c.ctx, secretName, k8smeta.GetOptions{}) if err != nil { return nil, err } if token, ok := secret.Data[acc.Spec.Token.Token]; ok { overrides.token = string(token) } } // Lookup the User. if acc.Spec.User != nil { secretName := acc.Spec.User.Secret.Name secret, err := c.ki.Secrets(ns).Get(c.ctx, secretName, k8smeta.GetOptions{}) if err != nil { return nil, err } userBytes := secret.Data[acc.Spec.User.User] passwordBytes := secret.Data[acc.Spec.User.Password] if userBytes != nil && passwordBytes != nil { overrides.user = string(userBytes) overrides.password = string(passwordBytes) } } return overrides, nil } type jsmcSpecOverrides struct { servers []string tls *apis.TLS creds string nkey string } func (c *Controller) runWithJsmc(jsm jsmClientFunc, acc *accountOverrides, spec *jsmcSpecOverrides, o runtime.Object, op func(jsmClient) error) error { if !c.opts.CRDConnect { jsmc, err := jsm(&natsContext{}) if err != nil { return err } return op(jsmc) } // Create a new client natsCtx := &natsContext{} // Use JWT/NKEYS/user-password/token based credentials if present. if spec.creds != "" { natsCtx.Credentials = spec.creds } else if spec.nkey != "" { natsCtx.Nkey = spec.nkey } if spec.tls != nil { if spec.tls.ClientCert != "" && spec.tls.ClientKey != "" { natsCtx.TLSCert = spec.tls.ClientCert natsCtx.TLSKey = spec.tls.ClientKey } } // Use fetched secrets for the account and server if defined. if acc.remoteClientCert != "" && acc.remoteClientKey != "" { natsCtx.TLSCert = acc.remoteClientCert natsCtx.TLSKey = acc.remoteClientKey } if acc.remoteRootCA != "" { natsCtx.TLSCAs = []string{acc.remoteRootCA} } if acc.userCreds != "" { natsCtx.Credentials = acc.userCreds } else if acc.nkey != "" { natsCtx.Nkey = acc.nkey } if acc.user != "" && acc.password != "" { natsCtx.Username = acc.user natsCtx.Password = acc.password } else if acc.token != "" { natsCtx.Token = acc.token } if spec.tls != nil && len(spec.tls.RootCAs) > 0 { natsCtx.TLSCAs = spec.tls.RootCAs } natsServers := strings.Join(append(spec.servers, acc.servers...), ",") natsCtx.URL = natsServers c.normalEvent(o, "Connecting", "Connecting to new nats-servers") jsmc, err := jsm(natsCtx) if err != nil { return fmt.Errorf("failed to connect to nats-servers(%s): %w", natsServers, err) } defer jsmc.Close() return op(jsmc) } func splitNamespaceName(item interface{}) (ns string, name string, err error) { defer func() { if err != nil { err = fmt.Errorf("failed to split namespace-name: %w", err) } }() key, ok := item.(string) if !ok { return "", "", fmt.Errorf("unexpected type: got=%T, want=%T", item, key) } ns, name, err = cache.SplitMetaNamespaceKey(key) if err != nil { return "", "", err } return ns, name, nil } func getStorageType(s string) (jsmapi.StorageType, error) { switch s { case strings.ToLower(jsmapi.FileStorage.String()): return jsmapi.FileStorage, nil case strings.ToLower(jsmapi.MemoryStorage.String()): return jsmapi.MemoryStorage, nil default: return 0, fmt.Errorf("invalid jetstream storage option: %s", s) } } func enqueueWork(q workqueue.TypedRateLimitingInterface[any], item interface{}) (err error) { key, err := cache.MetaNamespaceKeyFunc(item) if err != nil { return fmt.Errorf("failed to enqueue work: %w", err) } q.Add(key) return nil } type ( jsmClientFunc func(*natsContext) (jsmClient, error) processorFunc func(ns, name string, jmsClient jsmClientFunc) error ) func processQueueNext(q workqueue.TypedRateLimitingInterface[any], jmsClient jsmClientFunc, process processorFunc) { item, shutdown := q.Get() if shutdown { return } defer q.Done(item) ns, name, err := splitNamespaceName(item) if err != nil { // Probably junk, clean it up. utilruntime.HandleError(err) q.Forget(item) return } err = process(ns, name, jmsClient) if err == nil { // Item processed successfully, don't requeue. q.Forget(item) return } utilruntime.HandleError(err) if q.NumRequeues(item) < maxQueueRetries { // Failed to process item, try again. q.AddRateLimited(item) return } // If we haven't been able to recover by this point, then just stop. // The user should have enough info in kubectl describe to debug. q.Forget(item) } func UpsertCondition(cs []apis.Condition, next apis.Condition) []apis.Condition { for i := 0; i < len(cs); i++ { if cs[i].Type != next.Type { continue } cs[i] = next return cs } return append(cs, next) } func shouldEnqueue(prevObj, nextObj interface{}) bool { type crd interface { GetDeletionTimestamp() *k8smeta.Time GetSpec() interface{} } prev, ok := prevObj.(crd) if !ok { return false } next, ok := nextObj.(crd) if !ok { return false } markedDelete := next.GetDeletionTimestamp() != nil specChanged := !equality.Semantic.DeepEqual(prev.GetSpec(), next.GetSpec()) return markedDelete || specChanged } func eventHandlers(q workqueue.TypedRateLimitingInterface[any]) cache.ResourceEventHandlerFuncs { return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if err := enqueueWork(q, obj); err != nil { utilruntime.HandleError(err) } }, UpdateFunc: func(prev, next interface{}) { if !shouldEnqueue(prev, next) { return } if err := enqueueWork(q, next); err != nil { utilruntime.HandleError(err) } }, DeleteFunc: func(obj interface{}) { if err := enqueueWork(q, obj); err != nil { utilruntime.HandleError(err) } }, } } ================================================ FILE: controllers/jetstream/controller_test.go ================================================ package jetstream import ( "context" "fmt" "os" "testing" "time" jsmapi "github.com/nats-io/jsm.go/api" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" k8sapis "k8s.io/api/core/v1" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/util/workqueue" ) func TestMain(m *testing.M) { // Disable error logs. utilruntime.ErrorHandlers = []utilruntime.ErrorHandler{ func(ctx context.Context, err error, msg string, args ...any) {}, } os.Exit(m.Run()) } func TestGetStorageType(t *testing.T) { t.Parallel() cases := []struct { storage string wantType jsmapi.StorageType wantErr bool }{ {storage: "memory", wantType: jsmapi.MemoryStorage}, {storage: "file", wantType: jsmapi.FileStorage}, {storage: "junk", wantErr: true}, } for _, c := range cases { c := c t.Run(c.storage, func(t *testing.T) { t.Parallel() got, err := getStorageType(c.storage) if err != nil && !c.wantErr { t.Error("unexpected error") t.Fatalf("got=%s; want=nil", err) } else if err == nil && c.wantErr { t.Error("unexpected success") t.Fatalf("got=nil; want=err") } if got != c.wantType { t.Error("unexpected storage type") t.Fatalf("got=%v; want=%v", got, c.wantType) } }) } } func TestEnqueueWork(t *testing.T) { t.Parallel() limiter := workqueue.DefaultTypedControllerRateLimiter[any]() q := workqueue.NewNamedRateLimitingQueue(limiter, "StreamsTest") defer q.ShutDown() s := &apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "my-stream", }, } if err := enqueueWork(q, s); err != nil { t.Fatal(err) } if got, want := q.Len(), 1; got != want { t.Error("unexpected queue length") t.Fatalf("got=%d; want=%d", got, want) } wantItem := fmt.Sprintf("%s/%s", s.Namespace, s.Name) gotItem, _ := q.Get() if gotItem != wantItem { t.Error("unexpected queue item") t.Fatalf("got=%s; want=%s", gotItem, wantItem) } } func TestProcessQueueNext(t *testing.T) { t.Parallel() t.Run("bad item key", func(t *testing.T) { t.Parallel() limiter := workqueue.DefaultTypedControllerRateLimiter[any]() q := workqueue.NewNamedRateLimitingQueue(limiter, "StreamsTest") defer q.ShutDown() key := "this/is/a/bad/key" q.Add(key) processQueueNext(q, testWrapJSMC(&mockJsmClient{}), func(ns, name string, c jsmClientFunc) error { return nil }) if got, want := q.Len(), 0; got != want { t.Error("unexpected number of items in queue") t.Fatalf("got=%d; want=%d", got, want) } if got, want := q.NumRequeues(key), 0; got != want { t.Error("unexpected number of requeues") t.Fatalf("got=%d; want=%d", got, want) } }) t.Run("process error", func(t *testing.T) { t.Parallel() limiter := workqueue.DefaultTypedControllerRateLimiter[any]() q := workqueue.NewNamedRateLimitingQueue(limiter, "StreamsTest") defer q.ShutDown() ns, name := "default", "mystream" key := fmt.Sprintf("%s/%s", ns, name) q.Add(key) maxGets := maxQueueRetries + 1 numRequeues := -1 for i := 0; i < maxGets; i++ { if i == maxGets-1 { numRequeues = q.NumRequeues(key) } processQueueNext(q, testWrapJSMC(&mockJsmClient{}), func(ns, name string, c jsmClientFunc) error { return fmt.Errorf("processing error") }) } if got, want := q.Len(), 0; got != want { t.Error("unexpected number of items in queue") t.Fatalf("got=%d; want=%d", got, want) } if got, want := numRequeues, 10; got != want { t.Error("unexpected number of requeues") t.Fatalf("got=%d; want=%d", got, want) } }) t.Run("process ok", func(t *testing.T) { t.Parallel() limiter := workqueue.DefaultTypedControllerRateLimiter[any]() q := workqueue.NewNamedRateLimitingQueue(limiter, "StreamsTest") defer q.ShutDown() ns, name := "default", "mystream" key := fmt.Sprintf("%s/%s", ns, name) q.Add(key) numRequeues := q.NumRequeues(key) processQueueNext(q, testWrapJSMC(&mockJsmClient{}), func(ns, name string, c jsmClientFunc) error { return nil }) if got, want := q.Len(), 0; got != want { t.Error("unexpected number of items in queue") t.Fatalf("got=%d; want=%d", got, want) } if got, want := numRequeues, 0; got != want { t.Error("unexpected number of requeues") t.Fatalf("got=%d; want=%d", got, want) } }) } func TestUpsertCondition(t *testing.T) { t.Parallel() var cs []apis.Condition cs = UpsertCondition(cs, apis.Condition{ Type: readyCondType, Status: k8sapis.ConditionTrue, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Synced", Message: "Stream is synced with spec", }) if got, want := len(cs), 1; got != want { t.Error("unexpected len conditions") t.Fatalf("got=%d; want=%d", got, want) } if got, want := cs[0].Reason, "Synced"; got != want { t.Error("unexpected reason") t.Fatalf("got=%s; want=%s", got, want) } cs = UpsertCondition(cs, apis.Condition{ Type: readyCondType, Status: k8sapis.ConditionFalse, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Errored", Message: "invalid foo", }) if got, want := len(cs), 1; got != want { t.Error("unexpected len conditions") t.Fatalf("got=%d; want=%d", got, want) } if got, want := cs[0].Reason, "Errored"; got != want { t.Error("unexpected reason") t.Fatalf("got=%s; want=%s", got, want) } cs = UpsertCondition(cs, apis.Condition{ Type: "Foo", Status: k8sapis.ConditionTrue, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Bar", Message: "bar ok", }) if got, want := len(cs), 2; got != want { t.Error("unexpected len conditions") t.Fatalf("got=%d; want=%d", got, want) } if got, want := cs[1].Reason, "Bar"; got != want { t.Error("unexpected reason") t.Fatalf("got=%s; want=%s", got, want) } } func TestShouldEnqueue(t *testing.T) { t.Parallel() ts := k8smeta.NewTime(time.Now()) cases := []struct { name string prev interface{} next interface{} want bool }{ { name: "stream deleted", prev: &apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, }, next: &apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", DeletionTimestamp: &ts, }, }, want: true, }, { name: "stream spec changed", prev: &apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, Spec: apis.StreamSpec{ Name: "foo", }, }, next: &apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, Spec: apis.StreamSpec{ Name: "bar", }, }, want: true, }, { name: "consumer deleted", prev: &apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, }, next: &apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", DeletionTimestamp: &ts, }, }, want: true, }, { name: "consumer spec changed", prev: &apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, Spec: apis.ConsumerSpec{ DurableName: "foo", }, }, next: &apis.Consumer{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: "default", Name: "obj-name", }, Spec: apis.ConsumerSpec{ DurableName: "bar", }, }, want: true, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() got := shouldEnqueue(c.prev, c.next) if got != c.want { t.Fatalf("got=%t; want=%t", got, c.want) } }) } } ================================================ FILE: controllers/jetstream/jsmclient.go ================================================ package jetstream import ( "context" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" "github.com/nats-io/nats.go" "github.com/sirupsen/logrus" ) type jsmClient interface { Connect(servers string, opts ...nats.Option) error Close() LoadStream(ctx context.Context, name string) (jsmStream, error) NewStream(ctx context.Context, name string, opts []jsm.StreamOption) (jsmStream, error) LoadConsumer(ctx context.Context, stream, consumer string) (jsmConsumer, error) NewConsumer(ctx context.Context, stream string, opts []jsm.ConsumerOption) (jsmConsumer, error) } type jsmStream interface { UpdateConfiguration(cnf jsmapi.StreamConfig, opts ...jsm.StreamOption) error Delete() error } type jsmConsumer interface { UpdateConfiguration(opts ...jsm.ConsumerOption) error Delete() error } type realJsmClient struct { pooledNc *pooledNatsConn jm *jsm.Manager } func (c *realJsmClient) Connect(servers string, opts ...nats.Option) error { connPool := newNatsConnPool(logrus.New(), &natsContextDefaults{URL: servers}, opts) pooledNc, err := connPool.Get(&natsContext{}) if err != nil { return err } c.pooledNc = pooledNc m, err := jsm.New(pooledNc.nc) if err != nil { return err } c.jm = m return nil } func (c *realJsmClient) Close() { c.pooledNc.ReturnToPool() } func (c *realJsmClient) LoadStream(_ context.Context, name string) (jsmStream, error) { return c.jm.LoadStream(name) } func (c *realJsmClient) NewStream(_ context.Context, name string, opts []jsm.StreamOption) (jsmStream, error) { return c.jm.NewStream(name, opts...) } func (c *realJsmClient) LoadConsumer(_ context.Context, stream, consumer string) (jsmConsumer, error) { return c.jm.LoadConsumer(stream, consumer) } func (c *realJsmClient) NewConsumer(_ context.Context, stream string, opts []jsm.ConsumerOption) (jsmConsumer, error) { return c.jm.NewConsumer(stream, opts...) } ================================================ FILE: controllers/jetstream/jsmclient_test.go ================================================ package jetstream import ( "context" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" "github.com/nats-io/nats.go" ) type mockStream struct { deleteErr error capturedConfig *jsmapi.StreamConfig updateConfigCallback func(cnf jsmapi.StreamConfig) error } func (m *mockStream) UpdateConfiguration(cnf jsmapi.StreamConfig, opts ...jsm.StreamOption) error { if m.capturedConfig != nil { *m.capturedConfig = cnf } if m.updateConfigCallback != nil { return m.updateConfigCallback(cnf) } return nil } func (m *mockStream) Delete() error { return m.deleteErr } type mockConsumer struct { deleteErr error } func (m *mockConsumer) UpdateConfiguration(opts ...jsm.ConsumerOption) error { return nil } func (m *mockConsumer) Delete() error { return m.deleteErr } type mockJsmClient struct { connectErr error loadStream jsmStream loadStreamErr error newStream jsmStream newStreamErr error loadConsumer jsmConsumer loadConsumerErr error newConsumer jsmConsumer newConsumerErr error } func (c *mockJsmClient) Connect(servers string, opts ...nats.Option) error { return c.connectErr } func (c *mockJsmClient) Close() {} func (c *mockJsmClient) LoadStream(ctx context.Context, name string) (jsmStream, error) { return c.loadStream, c.loadStreamErr } func (c *mockJsmClient) NewStream(ctx context.Context, name string, opt []jsm.StreamOption) (jsmStream, error) { return c.newStream, c.newStreamErr } func (c *mockJsmClient) LoadConsumer(ctx context.Context, stream, consumer string) (jsmConsumer, error) { return c.loadConsumer, c.loadConsumerErr } func (c *mockJsmClient) NewConsumer(ctx context.Context, stream string, opts []jsm.ConsumerOption) (jsmConsumer, error) { return c.newConsumer, c.newConsumerErr } ================================================ FILE: controllers/jetstream/stream.go ================================================ // Copyright 2020 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jetstream import ( "context" "errors" "fmt" "time" jsm "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" typed "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" k8sapi "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" klog "k8s.io/klog/v2" ) func (c *Controller) runStreamQueue() { for { processQueueNext(c.strQueue, c.RealJSMC, c.processStream) } } func (c *Controller) processStream(ns, name string, jsm jsmClientFunc) (err error) { str, err := c.strLister.Streams(ns).Get(name) if err != nil && k8serrors.IsNotFound(err) { return nil } else if err != nil { return err } return c.processStreamObject(str, jsm) } func (c *Controller) processStreamObject(str *apis.Stream, jsm jsmClientFunc) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to process stream: %w", err) } }() spec := str.Spec ifc := c.ji.Streams(str.Namespace) ns := str.Namespace readOnly := c.opts.ReadOnly acc, err := c.getAccountOverrides(spec.Account, ns) if err != nil { return err } defer func() { if err == nil { return } if _, serr := setStreamErrored(c.ctx, str, ifc, err); serr != nil { err = fmt.Errorf("%s: %w", err, serr) } }() type operator func(ctx context.Context, c jsmClient, spec apis.StreamSpec) (err error) natsClientUtil := func(op operator) error { return c.runWithJsmc(jsm, acc, &jsmcSpecOverrides{ servers: spec.Servers, tls: spec.TLS, creds: spec.Creds, nkey: spec.Nkey, }, str, func(jsmc jsmClient) error { return op(c.ctx, jsmc, spec) }) } deleteOK := str.GetDeletionTimestamp() != nil newGeneration := str.Generation != str.Status.ObservedGeneration strOK := true err = natsClientUtil(streamExists) var apierr jsmapi.ApiError if errors.As(err, &apierr) && apierr.NotFoundError() { strOK = false } else if err != nil { return err } updateOK := (strOK && !deleteOK && newGeneration) createOK := (!strOK && !deleteOK) || (!updateOK && !deleteOK && newGeneration) switch { case createOK: if readOnly { c.normalEvent(str, "SkipCreate", fmt.Sprintf("Skip creating stream %q", spec.Name)) return nil } c.normalEvent(str, "Creating", fmt.Sprintf("Creating stream %q", spec.Name)) if err := natsClientUtil(createStream); err != nil { return err } if _, err := setStreamOK(c.ctx, str, ifc); err != nil { return err } c.normalEvent(str, "Created", fmt.Sprintf("Created stream %q", spec.Name)) case updateOK: if str.Spec.PreventUpdate || readOnly { c.normalEvent(str, "SkipUpdate", fmt.Sprintf("Skip updating stream %q", spec.Name)) if _, err := setStreamOK(c.ctx, str, ifc); err != nil { return err } return nil } c.normalEvent(str, "Updating", fmt.Sprintf("Updating stream %q", spec.Name)) if err := natsClientUtil(updateStream); err != nil { return err } if _, err := setStreamOK(c.ctx, str, ifc); err != nil { return err } c.normalEvent(str, "Updated", fmt.Sprintf("Updated stream %q", spec.Name)) return nil case deleteOK: if str.Spec.PreventDelete || readOnly { c.normalEvent(str, "SkipDelete", fmt.Sprintf("Skip deleting stream %q", spec.Name)) if _, err := setStreamOK(c.ctx, str, ifc); err != nil { return err } return nil } c.normalEvent(str, "Deleting", fmt.Sprintf("Deleting stream %q", spec.Name)) if err := natsClientUtil(deleteStream); err != nil { return err } default: c.normalEvent(str, "Noop", fmt.Sprintf("Nothing done for stream %q (prevent-delete=%v, prevent-update=%v)", spec.Name, spec.PreventDelete, spec.PreventUpdate, )) // Noop events only update the status of the CRD. if _, err := setStreamOK(c.ctx, str, ifc); err != nil { return err } } return nil } func streamExists(ctx context.Context, c jsmClient, spec apis.StreamSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to check if stream exists: %w", err) } }() _, err = c.LoadStream(ctx, spec.Name) return err } func createStream(ctx context.Context, c jsmClient, spec apis.StreamSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to create stream %q: %w", spec.Name, err) } }() maxAge, err := getDurationFromString(spec.MaxAge) if err != nil { return err } duplicates, err := getDuplicates(spec.DuplicateWindow) if err != nil { return err } opts := []jsm.StreamOption{ jsm.Subjects(spec.Subjects...), jsm.MaxConsumers(spec.MaxConsumers), jsm.MaxMessageSize(int32(spec.MaxMsgSize)), jsm.MaxMessages(int64(spec.MaxMsgs)), jsm.Replicas(spec.Replicas), jsm.DuplicateWindow(duplicates), jsm.MaxAge(maxAge), jsm.MaxBytes(int64(spec.MaxBytes)), } switch spec.Retention { case "limits": opts = append(opts, jsm.LimitsRetention()) case "interest": opts = append(opts, jsm.InterestRetention()) case "workqueue": opts = append(opts, jsm.WorkQueueRetention()) } switch spec.Storage { case "file": opts = append(opts, jsm.FileStorage()) case "memory": opts = append(opts, jsm.MemoryStorage()) } switch spec.Discard { case "old": opts = append(opts, jsm.DiscardOld()) case "new": opts = append(opts, jsm.DiscardNew()) } switch spec.Compression { case "s2": opts = append(opts, jsm.Compression(jsmapi.S2Compression)) case "none": opts = append(opts, jsm.Compression(jsmapi.NoCompression)) } if spec.NoAck { opts = append(opts, jsm.NoAck()) } if spec.Description != "" { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.Description = spec.Description return nil }) } if spec.MaxMsgsPerSubject > 0 { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.MaxMsgsPer = int64(spec.MaxMsgsPerSubject) return nil }) } if spec.Mirror != nil { ss, err := getStreamSource(spec.Mirror) if err != nil { return err } opts = append(opts, func(o *jsmapi.StreamConfig) error { o.Mirror = ss return nil }) } if spec.Placement != nil { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.Placement = &jsmapi.Placement{ Cluster: spec.Placement.Cluster, Tags: spec.Placement.Tags, } return nil }) } var srcs []*jsmapi.StreamSource for _, ss := range spec.Sources { jss, err := getStreamSource(ss) if err != nil { return err } srcs = append(srcs, jss) } opts = append(opts, func(o *jsmapi.StreamConfig) error { o.Sources = srcs return nil }) if spec.RePublish != nil { opts = append(opts, jsm.Republish(&jsmapi.RePublish{ Source: spec.RePublish.Source, Destination: spec.RePublish.Destination, })) } if spec.SubjectTransform != nil { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.SubjectTransform = &jsmapi.SubjectTransformConfig{ Source: spec.SubjectTransform.Source, Destination: spec.SubjectTransform.Dest, } return nil }) } if spec.AllowDirect { opts = append(opts, jsm.AllowDirect()) } if spec.AllowRollup { opts = append(opts, jsm.AllowRollup()) } if spec.DenyDelete { opts = append(opts, jsm.DenyDelete()) } if spec.DenyPurge { opts = append(opts, jsm.DenyPurge()) } if spec.DiscardPerSubject { opts = append(opts, jsm.DiscardNewPerSubject()) } if spec.FirstSequence != 0 { opts = append(opts, jsm.FirstSequence(spec.FirstSequence)) } if spec.Metadata != nil { opts = append(opts, jsm.StreamMetadata(spec.Metadata)) } if spec.AllowMsgTTL { opts = append(opts, jsm.AllowMsgTTL()) } if spec.SubjectDeleteMarkerTTL != "" { d, err := time.ParseDuration(spec.SubjectDeleteMarkerTTL) if err != nil { return fmt.Errorf("parse subject delete marker TTL: %w", err) } opts = append(opts, jsm.SubjectDeleteMarkerTTL(d)) } if spec.AllowMsgCounter { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowMsgCounter = true return nil }) } if spec.AllowAtomicPublish { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowAtomicPublish = true return nil }) } if spec.AllowMsgSchedules { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowMsgSchedules = true return nil }) } if spec.PersistMode == "async" { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.PersistMode = jsmapi.AsyncPersistMode return nil }) } else if spec.PersistMode == "default" { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.PersistMode = jsmapi.DefaultPersistMode return nil }) } _, err = c.NewStream(ctx, spec.Name, opts) return err } func updateStream(ctx context.Context, c jsmClient, spec apis.StreamSpec) (err error) { defer func() { if err != nil { err = fmt.Errorf("failed to update stream %q: %w", spec.Name, err) } }() js, err := c.LoadStream(ctx, spec.Name) if err != nil { return err } maxAge, err := getDurationFromString(spec.MaxAge) if err != nil { return err } subjectDeleteMarkerTTL, err := getDurationFromString(spec.SubjectDeleteMarkerTTL) if err != nil { return err } retention := getRetention(spec.Retention) storage := getStorage(spec.Storage) discard := getDiscard(spec.Discard) duplicates, err := getDuplicates(spec.DuplicateWindow) if err != nil { return err } var subjectTransform *jsmapi.SubjectTransformConfig if spec.SubjectTransform != nil { subjectTransform = &jsmapi.SubjectTransformConfig{ Source: spec.SubjectTransform.Source, Destination: spec.SubjectTransform.Dest, } } config := jsmapi.StreamConfig{ Name: spec.Name, Description: spec.Description, Retention: retention, Subjects: spec.Subjects, MaxConsumers: spec.MaxConsumers, MaxMsgs: int64(spec.MaxMsgs), MaxBytes: int64(spec.MaxBytes), MaxMsgsPer: int64(spec.MaxMsgsPerSubject), MaxAge: maxAge, MaxMsgSize: int32(spec.MaxMsgSize), Storage: storage, Discard: discard, DiscardNewPer: spec.DiscardPerSubject, Replicas: spec.Replicas, NoAck: spec.NoAck, Duplicates: duplicates, AllowDirect: spec.AllowDirect, DenyDelete: spec.DenyDelete, DenyPurge: spec.DenyPurge, RollupAllowed: spec.AllowRollup, FirstSeq: spec.FirstSequence, SubjectTransform: subjectTransform, AllowMsgTTL: spec.AllowMsgTTL, SubjectDeleteMarkerTTL: subjectDeleteMarkerTTL, AllowMsgCounter: spec.AllowMsgCounter, AllowAtomicPublish: spec.AllowAtomicPublish, AllowMsgSchedules: spec.AllowMsgSchedules, } if spec.RePublish != nil { config.RePublish = &jsmapi.RePublish{ Source: spec.RePublish.Source, Destination: spec.RePublish.Destination, HeadersOnly: spec.RePublish.HeadersOnly, } } if spec.Mirror != nil { ss, err := getStreamSource(spec.Mirror) if err != nil { return err } config.Mirror = ss } config.Sources = make([]*jsmapi.StreamSource, len(spec.Sources)) for i, ss := range spec.Sources { jss, err := getStreamSource(ss) if err != nil { return err } config.Sources[i] = jss } if spec.Placement != nil { config.Placement = &jsmapi.Placement{ Cluster: spec.Placement.Cluster, Tags: spec.Placement.Tags, } } if spec.Metadata != nil { config.Metadata = spec.Metadata } switch spec.Compression { case "s2": config.Compression = jsmapi.S2Compression case "none": config.Compression = jsmapi.NoCompression } // Handle PersistMode if spec.PersistMode == "async" { config.PersistMode = jsmapi.AsyncPersistMode } else if spec.PersistMode == "default" { config.PersistMode = jsmapi.DefaultPersistMode } return js.UpdateConfiguration(config) } func deleteStream(ctx context.Context, c jsmClient, spec apis.StreamSpec) (err error) { name := spec.Name defer func() { if err != nil { err = fmt.Errorf("failed to delete stream %q: %w", name, err) } }() if spec.PreventDelete { klog.Infof("Stream %q is configured to preventDelete:\n", name) return nil } var apierr jsmapi.ApiError str, err := c.LoadStream(ctx, name) if errors.As(err, &apierr) && apierr.NotFoundError() { return nil } else if err != nil { return err } return str.Delete() } func setStreamErrored(ctx context.Context, s *apis.Stream, sif typed.StreamInterface, err error) (*apis.Stream, error) { if err == nil { return s, nil } sc := s.DeepCopy() sc.Status.Conditions = UpsertCondition(sc.Status.Conditions, apis.Condition{ Type: readyCondType, Status: k8sapi.ConditionFalse, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Errored", Message: err.Error(), }) ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() var res *apis.Stream err = retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res, err = sif.UpdateStatus(ctx, sc, k8smeta.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to set stream errored status: %w", err) } return nil }) return res, err } func setStreamOK(ctx context.Context, s *apis.Stream, i typed.StreamInterface) (*apis.Stream, error) { sc := s.DeepCopy() sc.Status.ObservedGeneration = s.Generation sc.Status.Conditions = UpsertCondition(sc.Status.Conditions, apis.Condition{ Type: readyCondType, Status: k8sapi.ConditionTrue, LastTransitionTime: time.Now().UTC().Format(time.RFC3339Nano), Reason: "Created", Message: "Stream successfully created", }) ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() var res *apis.Stream err := retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res, err = i.UpdateStatus(ctx, sc, k8smeta.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to set stream %q status: %w", s.Spec.Name, err) } return nil }) return res, err } func getDurationFromString(v string) (time.Duration, error) { if v == "" { return time.Duration(0), nil } return time.ParseDuration(v) } func getRetention(v string) jsmapi.RetentionPolicy { retention := jsmapi.LimitsPolicy switch v { case "interest": retention = jsmapi.InterestPolicy case "workqueue": retention = jsmapi.WorkQueuePolicy } return retention } func getStorage(v string) jsmapi.StorageType { storage := jsmapi.MemoryStorage switch v { case "file": storage = jsmapi.FileStorage } return storage } func getDiscard(v string) jsmapi.DiscardPolicy { discard := jsmapi.DiscardOld switch v { case "new": discard = jsmapi.DiscardNew } return discard } func getDuplicates(v string) (time.Duration, error) { if v == "" { return time.Duration(0), nil } return time.ParseDuration(v) } func getStreamSource(ss *apis.StreamSource) (*jsmapi.StreamSource, error) { jss := &jsmapi.StreamSource{ Name: ss.Name, FilterSubject: ss.FilterSubject, } if ss.OptStartSeq > 0 { jss.OptStartSeq = uint64(ss.OptStartSeq) } else if ss.OptStartTime != "" { t, err := time.Parse(time.RFC3339, ss.OptStartTime) if err != nil { return nil, err } jss.OptStartTime = &t } if ss.ExternalAPIPrefix != "" || ss.ExternalDeliverPrefix != "" { jss.External = &jsmapi.ExternalStream{ ApiPrefix: ss.ExternalAPIPrefix, DeliverPrefix: ss.ExternalDeliverPrefix, } } for _, transform := range ss.SubjectTransforms { jss.SubjectTransforms = append(jss.SubjectTransforms, jsmapi.SubjectTransformConfig{ Source: transform.Source, Destination: transform.Dest, }) } return jss, nil } ================================================ FILE: controllers/jetstream/stream_test.go ================================================ package jetstream import ( "context" "errors" "strings" "testing" jsmapi "github.com/nats-io/jsm.go/api" apis "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" clientsetfake "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/fake" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8sclientsetfake "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/record" ) func TestProcessStream(t *testing.T) { t.Parallel() updateObject := func(a k8stesting.Action) (handled bool, o runtime.Object, err error) { ua, ok := a.(k8stesting.UpdateAction) if !ok { return false, nil, nil } return true, ua.GetObject(), nil } t.Run("create stream", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 2 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-stream" informer := ctrl.informerFactory.Jetstream().V1beta2().Streams() err := informer.Informer().GetStore().Add(&apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 1, }, Spec: apis.StreamSpec{ Name: name, MaxAge: "1h", Storage: "memory", }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "streams", updateObject) notFoundErr := jsmapi.ApiError{Code: 404} jsmc := &mockJsmClient{ loadStreamErr: notFoundErr, } if err := ctrl.processStream(ns, name, testWrapJSMC(jsmc)); err != nil { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } <-rec.Events <-rec.Events for i := 0; i < len(rec.Events); i++ { gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Creat") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Creating/Created...") } } }) t.Run("update stream", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 2 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-stream" informer := ctrl.informerFactory.Jetstream().V1beta2().Streams() err := informer.Informer().GetStore().Add(&apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 2, }, Spec: apis.StreamSpec{ Name: name, MaxAge: "1h", Storage: "memory", AllowMsgSchedules: true, }, Status: apis.Status{ ObservedGeneration: 1, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "streams", updateObject) // Capture the config that gets passed to UpdateConfiguration var capturedConfig jsmapi.StreamConfig jsmc := &mockJsmClient{ loadStreamErr: nil, loadStream: &mockStream{ capturedConfig: &capturedConfig, }, } if err := ctrl.processStream(ns, name, testWrapJSMC(jsmc)); err != nil { t.Fatal(err) } // Verify that AllowMsgSchedules was set in the config if !capturedConfig.AllowMsgSchedules { t.Errorf("AllowMsgSchedules not set in stream config during update: got=%v, want=true", capturedConfig.AllowMsgSchedules) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } for i := 0; i < len(rec.Events); i++ { gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Updat") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Updating/Updated...") } } }) t.Run("delete stream", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 1 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ts := k8smeta.Unix(1600216923, 0) ns, name := "default", "my-stream" informer := ctrl.informerFactory.Jetstream().V1beta2().Streams() err := informer.Informer().GetStore().Add(&apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 2, DeletionTimestamp: &ts, }, Spec: apis.StreamSpec{ Name: name, MaxAge: "1h", Storage: "memory", }, Status: apis.Status{ ObservedGeneration: 1, }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "streams", updateObject) jsmc := &mockJsmClient{ loadStreamErr: nil, loadStream: &mockStream{}, } if err := ctrl.processStream(ns, name, testWrapJSMC(jsmc)); err != nil { t.Fatal(err) } if got := len(rec.Events); got != wantEvents { t.Error("unexpected number of events") t.Fatalf("got=%d; want=%d", got, wantEvents) } for i := 0; i < len(rec.Events); i++ { gotEvent := <-rec.Events if !strings.Contains(gotEvent, "Delet") { t.Error("unexpected event") t.Fatalf("got=%s; want=%s", gotEvent, "Deleting/Deleted...") } } }) t.Run("process error", func(t *testing.T) { t.Parallel() jc := clientsetfake.NewSimpleClientset() wantEvents := 4 rec := record.NewFakeRecorder(wantEvents) ctrl := NewController(Options{ Ctx: context.Background(), KubeIface: k8sclientsetfake.NewSimpleClientset(), JetstreamIface: jc, Recorder: rec, }) ns, name := "default", "my-stream" informer := ctrl.informerFactory.Jetstream().V1beta2().Streams() err := informer.Informer().GetStore().Add(&apis.Stream{ ObjectMeta: k8smeta.ObjectMeta{ Namespace: ns, Name: name, Generation: 1, }, Spec: apis.StreamSpec{ Name: name, MaxAge: "1h", Storage: "memory", }, }) if err != nil { t.Fatal(err) } jc.PrependReactor("update", "streams", func(a k8stesting.Action) (handled bool, o runtime.Object, err error) { ua, ok := a.(k8stesting.UpdateAction) if !ok { return false, nil, nil } obj := ua.GetObject() str, ok := obj.(*apis.Stream) if !ok { t.Error("unexpected object type") t.Fatalf("got=%T; want=%T", obj, &apis.Stream{}) } if got, want := len(str.Status.Conditions), 1; got != want { t.Error("unexpected number of conditions") t.Fatalf("got=%d; want=%d", got, want) } if got, want := str.Status.Conditions[0].Reason, "Errored"; got != want { t.Error("unexpected condition reason") t.Fatalf("got=%s; want=%s", got, want) } return true, obj, nil }) jsmc := &mockJsmClient{ loadStreamErr: errors.New("failed to load stream"), } if err := ctrl.processStream(ns, name, testWrapJSMC(jsmc)); err == nil { t.Fatal("unexpected success") } }) } ================================================ FILE: dependencies.md ================================================ # External Dependencies This file lists the dependencies used in this repository. | Dependency | License | |--------------------------------------|--------------| | github.com/fsnotify/fsnotify | BSD-3-Clause | | github.com/nats-io/jsm.go | Apache-2.0 | | github.com/nats-io/nats.go | Apache-2.0 | | github.com/sirupsen/logrus | MIT | | github.com/stretchr/testify | MIT | | k8s.io/api | Apache-2.0 | | k8s.io/apimachinery | Apache-2.0 | | k8s.io/client-go | Apache-2.0 | | k8s.io/code-generator | Apache-2.0 | | k8s.io/klog/v2 | Apache-2.0 | | sigs.k8s.io/structured-merge-diff/v4 | Apache-2.0 | ================================================ FILE: deploy/crds.yml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: streams.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: Stream singular: stream plural: streams versions: - name: v1beta2 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: name: description: A unique name for the Stream. type: string pattern: '^[^.*>]*$' minLength: 1 description: description: The description of the stream. type: string subjects: description: A list of subjects to consume, supports wildcards. type: array minLength: 1 items: type: string minLength: 1 retention: description: How messages are retained in the Stream, once this is exceeded old messages are removed. type: string enum: - limits - interest - workqueue default: limits maxConsumers: description: How many Consumers can be defined for a given Stream. -1 for unlimited. type: integer minimum: -1 default: -1 maxMsgs: description: How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited. type: integer minimum: -1 default: -1 maxBytes: description: How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited. type: integer minimum: -1 default: -1 discard: description: When a Stream reach it's limits either old messages are deleted or new ones are denied. type: string enum: - old - new default: old discardPerSubject: description: Applies discard policy on a per-subject basis. Requires discard policy 'new' and 'maxMsgs' to be set. type: boolean default: false maxAge: description: Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited. type: string default: '' maxMsgsPerSubject: description: The maximum number of messages per subject. type: integer default: 0 maxMsgSize: description: The largest message that will be accepted by the Stream. -1 for unlimited. type: integer minimum: -1 default: -1 storage: description: The storage backend to use for the Stream. type: string enum: - file - memory default: memory replicas: description: How many replicas to keep for each message. type: integer minimum: 1 default: 1 noAck: description: Disables acknowledging messages that are received by the Stream. type: boolean default: false duplicateWindow: description: The duration window to track duplicate messages for. type: string placement: description: A stream's placement. type: object properties: cluster: type: string tags: type: array items: type: string mirror: description: A stream mirror. type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string subjectTransforms: description: List of subject transforms for this mirror. type: array items: description: A subject transform pair. type: object properties: source: description: Source subject. type: string dest: description: Destination subject. type: string sources: description: A stream's sources. type: array items: type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string subjectTransforms: description: List of subject transforms for this mirror. type: array items: description: A subject transform pair. type: object properties: source: description: Source subject. type: string dest: description: Destination subject. type: string sealed: description: Seal an existing stream so no new messages may be added. type: boolean default: false denyDelete: description: When true, restricts the ability to delete messages from a stream via the API. Cannot be changed once set to true. type: boolean default: false denyPurge: description: When true, restricts the ability to purge a stream via the API. Cannot be changed once set to true. type: boolean default: false allowRollup: description: When true, allows the use of the Nats-Rollup header to replace all contents of a stream, or subject in a stream, with a single new message. type: boolean default: false compression: description: Stream specific compression. type: string enum: - s2 - none - '' default: '' firstSequence: description: Sequence number from which the Stream will start. type: number default: 0 subjectTransform: description: SubjectTransform is for applying a subject transform (to matching messages) when a new message is received. type: object properties: source: type: string description: Source subject. dest: type: string description: Destination subject to transform into. republish: description: Republish configuration of the stream. type: object properties: destination: type: string description: Messages will be additionally published to this subject. source: type: string description: Messages will be published from this subject to the destination subject. allowDirect: description: When true, allow higher performance, direct access to get individual messages. type: boolean default: false mirrorDirect: description: When true, enables direct access to messages from the origin stream. type: boolean default: false allowMsgTtl: description: When true, allows header initiated per-message TTLs. If disabled, then the `NATS-TTL` header will be ignored. type: boolean default: false subjectDeleteMarkerTtl: description: Enables and sets a duration for adding server markers for delete, purge and max age limits. type: string default: '' allowMsgCounter: description: When true, enables message counters for the stream. type: boolean default: false allowAtomicPublish: description: When true, enables atomic batch publishing. type: boolean default: false allowMsgSchedules: description: When true, enables message scheduling. type: boolean default: false persistMode: description: Configures stream persistence settings (async or default). type: string default: '' consumerLimits: type: object properties: inactiveThreshold: description: The duration of inactivity after which a consumer is considered inactive. type: string maxAckPending: description: Maximum number of outstanding unacknowledged messages. type: integer metadata: description: Additional Stream metadata. type: object additionalProperties: type: string account: description: Name of the account to which the Stream belongs. type: string pattern: '^[^.*>]*$' creds: description: NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on this path. type: string default: '' nkey: description: NATS user NKey for connecting to servers. type: string default: '' preventDelete: description: When true, the managed Stream will not be deleted when the resource is deleted. type: boolean default: false preventUpdate: description: When true, the managed Stream will not be updated when the resource is updated. type: boolean default: false servers: description: A list of servers for creating stream. type: array items: type: string default: [] tls: description: A client's TLS certs and keys. type: object properties: clientCert: description: A client's cert filepath. Should be mounted. type: string clientKey: description: A client's key filepath. Should be mounted. type: string rootCas: description: A list of filepaths to CAs. Should be mounted. type: array items: type: string tlsFirst: description: When true, the KV Store will initiate TLS before server INFO. type: boolean default: false jsDomain: description: The JetStream domain to use for the stream. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the stream. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Stream Name type: string description: The name of the JetStream Stream. jsonPath: .spec.name - name: Subjects type: string description: The subjects this Stream produces. jsonPath: .spec.subjects - name: v1beta1 served: false storage: false subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: name: description: A unique name for the Stream. type: string pattern: '^[^.*>]*$' minLength: 1 subjects: description: A list of subjects to consume, supports wildcards. type: array minLength: 1 items: type: string minLength: 1 retention: description: How messages are retained in the Stream, once this is exceeded old messages are removed. type: string enum: - limits - interest - workqueue default: limits maxConsumers: description: How many Consumers can be defined for a given Stream. -1 for unlimited. type: integer minimum: -1 default: -1 maxMsgs: description: How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited. type: integer minimum: -1 default: -1 maxBytes: description: How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited. type: integer minimum: -1 default: -1 maxAge: description: Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited. type: string default: '' maxMsgSize: description: The largest message that will be accepted by the Stream. -1 for unlimited. type: integer minimum: -1 default: -1 storage: description: The storage backend to use for the Stream. type: string enum: - file - memory default: memory replicas: description: How many replicas to keep for each message. type: integer minimum: 1 default: 1 noAck: description: Disables acknowledging messages that are received by the Stream. type: boolean default: false discard: description: When a Stream reach it's limits either old messages are deleted or new ones are denied. type: string enum: - old - new default: old duplicateWindow: description: The duration window to track duplicate messages for. type: string description: description: The description of the stream. type: string maxMsgsPerSubject: description: The maximum number of messages per subject. type: integer default: 0 mirror: description: A stream mirror. type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string placement: description: A stream's placement. type: object properties: cluster: type: string tags: type: array items: type: string sources: description: A stream's sources. type: array items: type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the stream. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Stream Name type: string description: The name of the JetStream Stream. jsonPath: .spec.name - name: Subjects type: string description: The subjects this Stream produces. jsonPath: .spec.subjects --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: consumers.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: Consumer singular: consumer plural: consumers versions: - name: v1beta2 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: durableName: description: The name of the Consumer. type: string pattern: '^[^.*>]+$' minLength: 1 streamName: description: The name of the Stream to create the Consumer in. type: string deliverPolicy: type: string enum: - all - last - new # Requires optStartSeq - byStartSequence # Requires optStartTime - byStartTime - lastPerSubject default: all optStartSeq: type: integer minimum: 0 optStartTime: description: Time format must be RFC3339. type: string deliverSubject: description: The subject to deliver observed messages, when not set, a pull-based Consumer is created. type: string ackPolicy: description: How messages should be acknowledged. type: string enum: - none - all - explicit default: none ackWait: description: How long to allow messages to remain un-acknowledged before attempting redelivery. type: string default: 1ns maxDeliver: type: integer minimum: -1 backoff: description: List of durations representing a retry time scale for NaK'd or retried messages. type: array items: type: string filterSubject: description: Select only a specific incoming subjects, supports wildcards. type: string filterSubjects: description: List of incoming subjects, supports wildcards. Available since 2.10. type: array items: type: string replayPolicy: description: How messages are sent. type: string enum: - instant - original default: instant sampleFreq: description: What percentage of acknowledgements should be samples for observability. type: string maxWaiting: description: The number of pulls that can be outstanding on a pull consumer, pulls received after this is reached are ignored. type: integer rateLimitBps: description: Rate at which messages will be delivered to clients, expressed in bit per second. type: integer maxAckPending: description: Maximum pending Acks before consumers are paused. type: integer deliverGroup: description: The name of a queue group. type: string description: description: The description of the consumer. type: string flowControl: description: Enables flow control. type: boolean default: false headersOnly: description: When set, only the headers of messages in the stream are delivered, and not the bodies. Additionally, Nats-Msg-Size header is added to indicate the size of the removed payload. type: boolean default: false heartbeatInterval: description: The interval used to deliver idle heartbeats for push-based consumers, in Go's time.Duration format. type: string maxRequestBatch: description: The largest batch property that may be specified when doing a pull on a Pull Consumer. type: integer maxRequestExpires: description: The maximum expires duration that may be set when doing a pull on a Pull Consumer. type: string maxRequestMaxBytes: description: The maximum max_bytes value that maybe set when dong a pull on a Pull Consumer. type: integer inactiveThreshold: description: The idle time an Ephemeral Consumer allows before it is removed. type: string pauseUntil: description: RFC3339 timestamp until which the consumer should be paused. type: string default: '' priorityPolicy: description: Priority policy for consumer (pinned_client, overflow, prioritized, or none). type: string default: '' pinnedTtl: description: TTL for pinned client when using pinned_client priority policy. type: string default: '' priorityGroups: description: List of priority groups for the consumer. For now, only one group is supported. type: array items: type: string replicas: description: When set do not inherit the replica count from the stream but specifically set it to this amount. type: integer memStorage: description: Force the consumer state to be kept in memory rather than inherit the setting from the stream. type: boolean default: false metadata: description: Additional Consumer metadata. type: object additionalProperties: type: string account: description: Name of the account to which the Consumer belongs. type: string pattern: '^[^.*>]*$' creds: description: NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path. type: string default: '' nkey: description: NATS user NKey for connecting to servers. type: string default: '' preventDelete: description: When true, the managed Consumer will not be deleted when the resource is deleted. type: boolean default: false preventUpdate: description: When true, the managed Consumer will not be updated when the resource is updated. type: boolean default: false servers: description: A list of servers for creating consumer. type: array items: type: string default: [] tls: description: A client's TLS certs and keys. type: object properties: clientCert: description: A client's cert filepath. Should be mounted. type: string clientKey: description: A client's key filepath. Should be mounted. type: string rootCas: description: A list of filepaths to CAs. Should be mounted. type: array items: type: string tlsFirst: description: When true, the KV Store will initiate TLS before server INFO. type: boolean default: false jsDomain: description: The JetStream domain to use for the consumer. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the consumer. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Stream type: string description: The name of the JetStream Stream. jsonPath: .spec.streamName - name: Consumer type: string description: The name of the JetStream Consumer. jsonPath: .spec.durableName - name: Ack Policy type: string description: The ack policy. jsonPath: .spec.ackPolicy - name: v1beta1 served: false storage: false subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: streamName: description: The name of the Stream to create the Consumer in. type: string deliverPolicy: type: string enum: - all - last - new # Requires optStartSeq - byStartSequence # Requires optStartTime - byStartTime default: all optStartSeq: type: integer minimum: 0 optStartTime: description: Time format must be RFC3339. type: string durableName: description: The name of the Consumer. type: string pattern: '^[^.*>]+$' minLength: 1 deliverSubject: description: The subject to deliver observed messages, when not set, a pull-based Consumer is created. type: string ackPolicy: description: How messages should be acknowledged. type: string enum: - none - all - explicit default: none ackWait: description: How long to allow messages to remain un-acknowledged before attempting redelivery. type: string default: 1ns maxDeliver: type: integer minimum: -1 filterSubject: description: Select only a specific incoming subjects, supports wildcards. type: string replayPolicy: description: How messages are sent. type: string enum: - instant - original default: instant sampleFreq: description: What percentage of acknowledgements should be samples for observability. type: string rateLimitBps: description: Rate at which messages will be delivered to clients, expressed in bit per second. type: integer maxAckPending: description: Maximum pending Acks before consumers are paused. type: integer deliverGroup: description: The name of a queue group. type: string description: description: The description of the consumer. type: string flowControl: description: Enables flow control. type: boolean default: false heartbeatInterval: description: The interval used to deliver idle heartbeats for push-based consumers, in Go's time.Duration format. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the consumer. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Stream type: string description: The name of the JetStream Stream. jsonPath: .spec.streamName - name: Consumer type: string description: The name of the JetStream Consumer. jsonPath: .spec.durableName - name: Ack Policy type: string description: The ack policy. jsonPath: .spec.ackPolicy --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: streamtemplates.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: StreamTemplate singular: streamtemplate plural: streamtemplates versions: - name: v1beta1 served: false storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: name: description: A unique name for the Stream Template. type: string pattern: '^[^.*>]*$' minLength: 1 maxStreams: description: The maximum number of Streams this Template can create, -1 for unlimited. type: integer minimum: -1 default: -1 subjects: description: A list of subjects to consume, supports wildcards. type: array minLength: 1 items: type: string minLength: 1 retention: description: How messages are retained in the Stream, once this is exceeded old messages are removed. type: string enum: - limits - interest - workqueue default: limits maxConsumers: description: How many Consumers can be defined for a given Stream. -1 for unlimited. type: integer minimum: -1 default: -1 maxMsgs: description: How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited. type: integer minimum: -1 default: -1 maxBytes: description: How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited. type: integer minimum: -1 default: -1 maxAge: description: Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited. type: string default: '' maxMsgSize: description: The largest message that will be accepted by the Stream. -1 for unlimited. type: integer minimum: -1 default: -1 storage: description: The storage backend to use for the Stream. type: string enum: - file - memory default: memory replicas: description: How many replicas to keep for each message. type: integer minimum: 1 default: 1 noAck: description: Disables acknowledging messages that are received by the Stream. type: boolean default: false discard: description: When a Stream reach it's limits either old messages are deleted or new ones are denied. type: string enum: - old - new default: old duplicateWindow: description: The duration window to track duplicate messages for. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the stream. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Stream Template Name type: string description: The name of the JetStream Stream Template. jsonPath: .spec.name - name: Subjects type: string description: The subjects this Stream produces. jsonPath: .spec.subjects --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: accounts.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: Account singular: account plural: accounts versions: - name: v1beta2 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: name: description: A unique name for the Account. type: string pattern: '^[^.*>]*$' minLength: 1 creds: description: The creds to be used to connect to the NATS Service. type: object properties: secret: type: object properties: name: description: Name of the secret with the creds. type: string file: description: Credentials file, generated with github.com/nats-io/nsc tool. type: string nkey: description: The NKey seed to be used to connect to the NATS Service. type: object properties: secret: type: object properties: name: description: Name of the secret containing the NKey seed. type: string seed: description: Key in the secret that contains the NKey seed. type: string servers: description: A list of servers to connect. type: array minLength: 1 items: type: string minLength: 1 tls: description: The TLS certs to be used to connect to the NATS Service. type: object properties: secret: type: object properties: name: description: Name of the TLS secret with the certs. type: string ca: description: Filename of the Root CA of the TLS cert. type: string cert: description: Filename of the TLS cert. type: string key: description: Filename of the TLS cert key. type: string token: description: The token to be used to connect to the NATS Service. type: object properties: secret: type: object properties: name: description: Name of the secret with the token. type: string token: description: Key in the secret that contains the token. type: string user: description: The user and password to be used to connect to the NATS Service. type: object properties: secret: type: object properties: name: description: Name of the secret with the user and password. type: string user: description: Key in the secret that contains the user. type: string password: description: Key in the secret that contains the password. type: string tlsFirst: description: When true, the KV Store will initiate TLS before server INFO. type: boolean default: false status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: keyvalues.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: KeyValue singular: keyvalue plural: keyvalues shortNames: - kv versions: - name: v1beta2 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: bucket: description: A unique name for the KV Store. type: string description: description: The description of the KV Store. type: string maxValueSize: description: The maximum size of a value in bytes. type: integer history: description: The number of historical values to keep per key. type: integer ttl: description: The time expiry for keys. type: string limitMarkerTtl: description: LimitMarkerTTL is how long the bucket keeps markers when keys are removed by the TTL setting, 0 meaning markers are not supported type: integer maxBytes: description: The maximum size of the KV Store in bytes. type: integer storage: description: The storage backend to use for the KV Store. type: string enum: - file - memory replicas: description: The number of replicas to keep for the KV Store in clustered JetStream. type: integer minimum: 1 maximum: 5 default: 1 placement: description: The KV Store placement via tags or cluster name. type: object properties: cluster: type: string tags: type: array items: type: string republish: description: Republish configuration for the KV Store. type: object properties: destination: type: string description: Messages will be additionally published to this subject after Bucket. source: type: string description: Messages will be published from this subject to the destination subject. mirror: description: A KV Store mirror. type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string subjectTransforms: description: List of subject transforms for this mirror. type: array items: description: A subject transform pair. type: object properties: source: description: Source subject. type: string dest: description: Destination subject. type: string compression: description: KV Store compression. type: boolean sources: description: A KV Store's sources. type: array items: type: object properties: name: type: string optStartSeq: type: integer optStartTime: description: Time format must be RFC3339. type: string filterSubject: type: string externalApiPrefix: type: string externalDeliverPrefix: type: string subjectTransforms: description: List of subject transforms for this mirror. type: array items: description: A subject transform pair. type: object properties: source: description: Source subject. type: string dest: description: Destination subject. type: string account: description: Name of the account to which the Stream belongs. type: string pattern: '^[^.*>]*$' creds: description: NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path. type: string default: '' nkey: description: NATS user NKey for connecting to servers. type: string default: '' preventDelete: description: When true, the managed KV Store will not be deleted when the resource is deleted. type: boolean default: false preventUpdate: description: When true, the managed KV Store will not be updated when the resource is updated. type: boolean default: false servers: description: A list of servers for creating the KV Store. type: array items: type: string default: [] tls: description: A client's TLS certs and keys. type: object properties: clientCert: description: A client's cert filepath. Should be mounted. type: string clientKey: description: A client's key filepath. Should be mounted. type: string rootCas: description: A list of filepaths to CAs. Should be mounted. type: array items: type: string tlsFirst: description: When true, the KV Store will initiate TLS before server INFO. type: boolean default: false jsDomain: description: The JetStream domain to use for the KV store. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the KV Store. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: KV Store Name type: string description: The name of the KV Store. jsonPath: .spec.bucket --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: objectstores.jetstream.nats.io spec: group: jetstream.nats.io scope: Namespaced names: kind: ObjectStore singular: objectstore plural: objectstores versions: - name: v1beta2 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: bucket: description: A unique name for the Object Store. type: string description: description: The description of the Object Store. type: string ttl: description: The time expiry for keys. type: string maxBytes: description: The maximum size of the Store in bytes. type: integer storage: description: The storage backend to use for the Object Store. type: string enum: - file - memory replicas: description: The number of replicas to keep for the Object Store in clustered JetStream. type: integer minimum: 1 maximum: 5 default: 1 placement: description: The Object Store placement via tags or cluster name. type: object properties: cluster: type: string tags: type: array items: type: string compression: description: Object Store compression. type: boolean metadata: description: Additional Object Store metadata. type: object additionalProperties: type: string account: description: Name of the account to which the Object Store belongs. type: string pattern: '^[^.*>]*$' creds: description: NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path. type: string default: '' nkey: description: NATS user NKey for connecting to servers. type: string default: '' preventDelete: description: When true, the managed Object Store will not be deleted when the resource is deleted. type: boolean default: false preventUpdate: description: When true, the managed Object Store will not be updated when the resource is updated. type: boolean default: false servers: description: A list of servers for creating the Object Store. type: array items: type: string default: [] tls: description: A client's TLS certs and keys. type: object properties: clientCert: description: A client's cert filepath. Should be mounted. type: string clientKey: description: A client's key filepath. Should be mounted. type: string rootCas: description: A list of filepaths to CAs. Should be mounted. type: array items: type: string tlsFirst: description: When true, the Object Store will initiate TLS before server INFO. type: boolean default: false jsDomain: description: The JetStream domain to use for the Object Store. type: string status: type: object properties: observedGeneration: type: integer conditions: type: array items: type: object properties: type: type: string status: type: string lastTransitionTime: type: string reason: type: string message: type: string additionalPrinterColumns: - name: State type: string description: The current state of the Object Store. jsonPath: .status.conditions[?(@.type == 'Ready')].reason - name: Object Store Name type: string description: The name of the Object Store. jsonPath: .spec.bucket ================================================ FILE: deploy/examples/consumer_pull.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Consumer metadata: name: my-pull-consumer spec: streamName: mystream durableName: my-pull-consumer deliverPolicy: all filterSubject: orders.received maxDeliver: 20 ackPolicy: explicit ================================================ FILE: deploy/examples/consumer_push.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Consumer metadata: name: my-push-consumer spec: streamName: mystream durableName: my-push-consumer deliverSubject: my-push-consumer.orders deliverPolicy: last ackPolicy: none replayPolicy: instant description: my consumer description flowControl: true heartbeatInterval: 1s ================================================ FILE: deploy/examples/stream.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream spec: name: mystream subjects: ["orders.*"] storage: file maxAge: 1h replicas: 1 ================================================ FILE: deploy/examples/stream_mirror.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream-mirror spec: name: mystream-mirror storage: file mirror: name: my-publish-subj externalApiPrefix: FOO.JS.API externalDeliverPrefix: FOO.DELIVER.SYNC.MIRRORS ================================================ FILE: deploy/examples/stream_placement.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream-placement spec: name: mystream-placement storage: file placement: tags: - NODE_0 ================================================ FILE: deploy/examples/stream_servers.yml ================================================ apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream spec: name: mystream servers: - nats://acme.org:4222 tls: clientCert: /etc/certs/client/foo/client-cert.pem clientKey: /etc/certs/client/foo/client-key.pem rootCas: - /etc/certs/ca/foo/ca-cert.pem creds: /etc/jsc-creds/my.creds ================================================ FILE: deploy/examples/stream_sources.yml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: mystream-sources spec: name: mystream-sources storage: file sources: - name: m1 - name: m2 ================================================ FILE: deploy/rbac.yml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: name: jetstream-controller namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: jetstream-controller-cluster-role rules: - apiGroups: - '' resources: - events verbs: - create - update - patch - apiGroups: - '' resources: - secrets verbs: - get - watch - list - apiGroups: - jetstream.nats.io resources: - streams - streams/status - objectstores - objectstores/status - keyvalues - keyvalues/status - consumers - consumers/status - streamtemplates - streamtemplates/status - accounts - accounts/status verbs: - create - get - list - watch - patch - update - delete - apiGroups: - jetstream.nats.io resources: - streams/finalizers - keyvalues/finalizers - objectstores/finalizers - consumers/finalizers - accounts/finalizers verbs: - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: jetstream-controller-cluster-role-binding subjects: - kind: ServiceAccount name: jetstream-controller namespace: default roleRef: kind: ClusterRole name: jetstream-controller-cluster-role apiGroup: rbac.authorization.k8s.io ================================================ FILE: docker-bake.hcl ================================================ ################### ### Variables ################### variable REGISTRY { default = "" } # Comma delimited list of tags variable TAGS { default = "latest" } variable CI { default = false } variable PUSH { default = false } ################### ### Functions ################### function "get_tags" { params = [image] result = [for tag in split(",", TAGS) : join("/", compact([REGISTRY, "${image}:${tag}"]))] } function "get_platforms_multiarch" { params = [] result = (CI || PUSH) ? ["linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64"] : [] } function "get_output" { params = [] result = (CI || PUSH) ? ["type=registry"] : ["type=docker"] } ################### ### Groups ################### group "default" { targets = [ "jetstream-controller", "nats-boot-config", "nats-server-config-reloader" ] } ################### ### Targets ################### target "goreleaser" { contexts = { src = "." } args = { CI = CI PUSH = PUSH GITHUB_TOKEN = "" } dockerfile = "cicd/Dockerfile_goreleaser" } target "jetstream-controller" { contexts = { build = "target:goreleaser" assets = "cicd/assets" } args = { GO_APP = "jetstream-controller" } dockerfile = "cicd/Dockerfile" platforms = get_platforms_multiarch() tags = get_tags("jetstream-controller") output = get_output() } target "nats-boot-config-base" { contexts = { build = "target:goreleaser" assets = "cicd/assets" } args = { GO_APP = "nats-boot-config" } dockerfile = "cicd/Dockerfile" platforms = get_platforms_multiarch() } target "nats-boot-config" { inherits = ["nats-boot-config-base"] contexts = { base = "target:nats-boot-config-base" } dockerfile-inline = <[↩ Parent](#jetstreamnatsiov1beta2)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta2 true
kind string Stream true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### Stream.spec [↩ Parent](#stream)
Name Type Description Required
account string Name of the account to which the Stream belongs.
false
allowDirect boolean When true, allow higher performance, direct access to get individual messages.

Default: false
false
allowRollup boolean When true, allows the use of the Nats-Rollup header to replace all contents of a stream, or subject in a stream, with a single new message.

Default: false
false
compression enum Stream specific compression.

Enum: s2, none,
Default:
false
consumerLimits object
false
creds string NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on this path.

Default:
false
denyDelete boolean When true, restricts the ability to delete messages from a stream via the API. Cannot be changed once set to true.

Default: false
false
denyPurge boolean When true, restricts the ability to purge a stream via the API. Cannot be changed once set to true.

Default: false
false
description string The description of the stream.
false
discard enum When a Stream reach it's limits either old messages are deleted or new ones are denied.

Enum: old, new
Default: old
false
discardPerSubject boolean Applies discard policy on a per-subject basis. Requires discard policy 'new' and 'maxMsgs' to be set.

Default: false
false
duplicateWindow string The duration window to track duplicate messages for.
false
firstSequence number Sequence number from which the Stream will start.

Default: 0
false
maxAge string Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited.

Default:
false
maxBytes integer How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited.

Default: -1
Minimum: -1
false
maxConsumers integer How many Consumers can be defined for a given Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgSize integer The largest message that will be accepted by the Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgs integer How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgsPerSubject integer The maximum number of messages per subject.

Default: 0
false
allowMsgTtl boolean When true, allows header initiated per-message TTLs. If disabled, then the `NATS-TTL` header will be ignored.

Default: false
false
subjectDeleteMarkerTtl string Enables and sets a duration for adding server markers for delete, purge and max age limits, expressed in Go's time.Duration format.
false
metadata map[string]string Additional Stream metadata.
false
mirror object A stream mirror.
false
mirrorDirect boolean When true, enables direct access to messages from the origin stream.

Default: false
false
name string A unique name for the Stream.
false
nkey string NATS user NKey for connecting to servers.

Default:
false
noAck boolean Disables acknowledging messages that are received by the Stream.

Default: false
false
placement object A stream's placement.
false
preventDelete boolean When true, the managed Stream will not be deleted when the resource is deleted.

Default: false
false
preventUpdate boolean When true, the managed Stream will not be updated when the resource is updated.

Default: false
false
replicas integer How many replicas to keep for each message.

Default: 1
Minimum: 1
false
republish object Republish configuration of the stream.
false
retention enum How messages are retained in the Stream, once this is exceeded old messages are removed.

Enum: limits, interest, workqueue
Default: limits
false
sealed boolean Seal an existing stream so no new messages may be added.

Default: false
false
servers []string A list of servers for creating stream.

Default: []
false
sources []object A stream's sources.
false
storage enum The storage backend to use for the Stream.

Enum: file, memory
Default: memory
false
subjectTransform object SubjectTransform is for applying a subject transform (to matching messages) when a new message is received.
false
subjects []string A list of subjects to consume, supports wildcards.
false
tls object A client's TLS certs and keys.
false
tlsFirst boolean When true, the KV Store will initiate TLS before server INFO.

Default: false
false
### Stream.spec.consumerLimits [↩ Parent](#streamspec)
Name Type Description Required
inactiveThreshold string The duration of inactivity after which a consumer is considered inactive.
false
maxAckPending integer Maximum number of outstanding unacknowledged messages.
false
### Stream.spec.mirror [↩ Parent](#streamspec) A stream mirror.
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
subjectTransforms []object List of subject transforms for this mirror.
false
### Stream.spec.mirror.subjectTransforms[index] [↩ Parent](#streamspecmirror) A subject transform pair.
Name Type Description Required
dest string Destination subject.
false
source string Source subject.
false
### Stream.spec.placement [↩ Parent](#streamspec) A stream's placement.
Name Type Description Required
cluster string
false
tags []string
false
### Stream.spec.republish [↩ Parent](#streamspec) Republish configuration of the stream.
Name Type Description Required
destination string Messages will be additionally published to this subject.
false
source string Messages will be published from this subject to the destination subject.
false
### Stream.spec.sources[index] [↩ Parent](#streamspec)
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
subjectTransforms []object List of subject transforms for this mirror.
false
### Stream.spec.sources[index].subjectTransforms[index] [↩ Parent](#streamspecsourcesindex) A subject transform pair.
Name Type Description Required
dest string Destination subject.
false
source string Source subject.
false
### Stream.spec.subjectTransform [↩ Parent](#streamspec) SubjectTransform is for applying a subject transform (to matching messages) when a new message is received.
Name Type Description Required
dest string Destination subject to transform into.
false
source string Source subject.
false
### Stream.spec.tls [↩ Parent](#streamspec) A client's TLS certs and keys.
Name Type Description Required
clientCert string A client's cert filepath. Should be mounted.
false
clientKey string A client's key filepath. Should be mounted.
false
rootCas []string A list of filepaths to CAs. Should be mounted.
false
### Stream.status [↩ Parent](#stream)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### Stream.status.conditions[index] [↩ Parent](#streamstatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## Consumer [↩ Parent](#jetstreamnatsiov1beta2)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta2 true
kind string Consumer true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### Consumer.spec [↩ Parent](#consumer)
Name Type Description Required
account string Name of the account to which the Consumer belongs.
false
ackPolicy enum How messages should be acknowledged.

Enum: none, all, explicit
Default: none
false
ackWait string How long to allow messages to remain un-acknowledged before attempting redelivery.

Default: 1ns
false
backoff []string List of durations representing a retry time scale for NaK'd or retried messages.
false
creds string NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path.

Default:
false
deliverGroup string The name of a queue group.
false
deliverPolicy enum Enum: all, last, new, byStartSequence, byStartTime
Default: all
false
deliverSubject string The subject to deliver observed messages, when not set, a pull-based Consumer is created.
false
description string The description of the consumer.
false
durableName string The name of the Consumer.
false
filterSubject string Select only a specific incoming subjects, supports wildcards.
false
filterSubjects []string List of incoming subjects, supports wildcards. Available since 2.10.
false
flowControl boolean Enables flow control.

Default: false
false
headersOnly boolean When set, only the headers of messages in the stream are delivered, and not the bodies. Additionally, Nats-Msg-Size header is added to indicate the size of the removed payload.

Default: false
false
heartbeatInterval string The interval used to deliver idle heartbeats for push-based consumers, in Go's time.Duration format.
false
inactiveThreshold string The idle time an Ephemeral Consumer allows before it is removed.
false
maxAckPending integer Maximum pending Acks before consumers are paused.
false
maxDeliver integer Minimum: -1
false
maxRequestBatch integer The largest batch property that may be specified when doing a pull on a Pull Consumer.
false
maxRequestExpires string The maximum expires duration that may be set when doing a pull on a Pull Consumer.
false
maxRequestMaxBytes integer The maximum max_bytes value that maybe set when dong a pull on a Pull Consumer.
false
maxWaiting integer The number of pulls that can be outstanding on a pull consumer, pulls received after this is reached are ignored.
false
memStorage boolean Force the consumer state to be kept in memory rather than inherit the setting from the stream.

Default: false
false
metadata map[string]string Additional Consumer metadata.
false
nkey string NATS user NKey for connecting to servers.

Default:
false
optStartSeq integer Minimum: 0
false
optStartTime string Time format must be RFC3339.
false
preventDelete boolean When true, the managed Consumer will not be deleted when the resource is deleted.

Default: false
false
preventUpdate boolean When true, the managed Consumer will not be updated when the resource is updated.

Default: false
false
rateLimitBps integer Rate at which messages will be delivered to clients, expressed in bit per second.
false
replayPolicy enum How messages are sent.

Enum: instant, original
Default: instant
false
replicas integer When set do not inherit the replica count from the stream but specifically set it to this amount.
false
sampleFreq string What percentage of acknowledgements should be samples for observability.
false
servers []string A list of servers for creating consumer.

Default: []
false
streamName string The name of the Stream to create the Consumer in.
false
tls object A client's TLS certs and keys.
false
tlsFirst boolean When true, the KV Store will initiate TLS before server INFO.

Default: false
false
### Consumer.spec.tls [↩ Parent](#consumerspec) A client's TLS certs and keys.
Name Type Description Required
clientCert string A client's cert filepath. Should be mounted.
false
clientKey string A client's key filepath. Should be mounted.
false
rootCas []string A list of filepaths to CAs. Should be mounted.
false
### Consumer.status [↩ Parent](#consumer)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### Consumer.status.conditions[index] [↩ Parent](#consumerstatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## Account [↩ Parent](#jetstreamnatsiov1beta2)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta2 true
kind string Account true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### Account.spec [↩ Parent](#account)
Name Type Description Required
creds object The creds to be used to connect to the NATS Service.
false
name string A unique name for the Account.
false
servers []string A list of servers to connect.
false
tls object The TLS certs to be used to connect to the NATS Service.
false
tlsFirst boolean When true, the KV Store will initiate TLS before server INFO.

Default: false
false
token object The token to be used to connect to the NATS Service.
false
user object The user and password to be used to connect to the NATS Service.
false
### Account.spec.creds [↩ Parent](#accountspec) The creds to be used to connect to the NATS Service.
Name Type Description Required
file string Credentials file, generated with github.com/nats-io/nsc tool.
false
secret object
false
### Account.spec.creds.secret [↩ Parent](#accountspeccreds)
Name Type Description Required
name string Name of the secret with the creds.
false
### Account.spec.tls [↩ Parent](#accountspec) The TLS certs to be used to connect to the NATS Service.
Name Type Description Required
ca string Filename of the Root CA of the TLS cert.
false
cert string Filename of the TLS cert.
false
key string Filename of the TLS cert key.
false
secret object
false
### Account.spec.tls.secret [↩ Parent](#accountspectls)
Name Type Description Required
name string Name of the TLS secret with the certs.
false
### Account.spec.token [↩ Parent](#accountspec) The token to be used to connect to the NATS Service.
Name Type Description Required
secret object
false
token string Key in the secret that contains the token.
false
### Account.spec.token.secret [↩ Parent](#accountspectoken)
Name Type Description Required
name string Name of the secret with the token.
false
### Account.spec.user [↩ Parent](#accountspec) The user and password to be used to connect to the NATS Service.
Name Type Description Required
password string Key in the secret that contains the password.
false
secret object
false
user string Key in the secret that contains the user.
false
### Account.spec.user.secret [↩ Parent](#accountspecuser)
Name Type Description Required
name string Name of the secret with the user and password.
false
### Account.status [↩ Parent](#account)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### Account.status.conditions[index] [↩ Parent](#accountstatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## KeyValue > **⚠️ Important**: KeyValue resources require the JetStream controller to be running in **control-loop mode** (`--control-loop` flag). They are not supported in the default legacy mode. [↩ Parent](#jetstreamnatsiov1beta2)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta2 true
kind string KeyValue true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### KeyValue.spec [↩ Parent](#keyvalue)
Name Type Description Required
account string Name of the account to which the Stream belongs.
false
bucket string A unique name for the KV Store.
false
compression boolean KV Store compression.
false
creds string NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path.

Default:
false
description string The description of the KV Store.
false
history integer The number of historical values to keep per key.
false
maxBytes integer The maximum size of the KV Store in bytes.
false
maxValueSize integer The maximum size of a value in bytes.
false
mirror object A KV Store mirror.
false
nkey string NATS user NKey for connecting to servers.

Default:
false
placement object The KV Store placement via tags or cluster name.
false
preventDelete boolean When true, the managed KV Store will not be deleted when the resource is deleted.

Default: false
false
preventUpdate boolean When true, the managed KV Store will not be updated when the resource is updated.

Default: false
false
replicas integer The number of replicas to keep for the KV Store in clustered JetStream.

Default: 1
Minimum: 1
Maximum: 5
false
republish object Republish configuration for the KV Store.
false
servers []string A list of servers for creating the KV Store.

Default: []
false
sources []object A KV Store's sources.
false
storage enum The storage backend to use for the KV Store.

Enum: file, memory
false
tls object A client's TLS certs and keys.
false
tlsFirst boolean When true, the KV Store will initiate TLS before server INFO.

Default: false
false
ttl string The time expiry for keys.
false
limitMarkerTtl integer How long the bucket keeps markers when keys are removed by the TTL setting, 0 meaning markers are not supported
false
### KeyValue.spec.mirror [↩ Parent](#keyvaluespec) A KV Store mirror.
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
subjectTransforms []object List of subject transforms for this mirror.
false
### KeyValue.spec.mirror.subjectTransforms[index] [↩ Parent](#keyvaluespecmirror) A subject transform pair.
Name Type Description Required
dest string Destination subject.
false
source string Source subject.
false
### KeyValue.spec.placement [↩ Parent](#keyvaluespec) The KV Store placement via tags or cluster name.
Name Type Description Required
cluster string
false
tags []string
false
### KeyValue.spec.republish [↩ Parent](#keyvaluespec) Republish configuration for the KV Store.
Name Type Description Required
destination string Messages will be additionally published to this subject after Bucket.
false
source string Messages will be published from this subject to the destination subject.
false
### KeyValue.spec.sources[index] [↩ Parent](#keyvaluespec)
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
subjectTransforms []object List of subject transforms for this mirror.
false
### KeyValue.spec.sources[index].subjectTransforms[index] [↩ Parent](#keyvaluespecsourcesindex) A subject transform pair.
Name Type Description Required
dest string Destination subject.
false
source string Source subject.
false
### KeyValue.spec.tls [↩ Parent](#keyvaluespec) A client's TLS certs and keys.
Name Type Description Required
clientCert string A client's cert filepath. Should be mounted.
false
clientKey string A client's key filepath. Should be mounted.
false
rootCas []string A list of filepaths to CAs. Should be mounted.
false
### KeyValue.status [↩ Parent](#keyvalue)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### KeyValue.status.conditions[index] [↩ Parent](#keyvaluestatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## ObjectStore > **⚠️ Important**: ObjectStore resources require the JetStream controller to be running in **control-loop mode** (`--control-loop` flag). They are not supported in the default legacy mode. [↩ Parent](#jetstreamnatsiov1beta2)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta2 true
kind string ObjectStore true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### ObjectStore.spec [↩ Parent](#objectstore)
Name Type Description Required
account string Name of the account to which the Object Store belongs.
false
bucket string A unique name for the Object Store.
false
compression boolean Object Store compression.
false
creds string NATS user credentials for connecting to servers. Please make sure your controller has mounted the creds on its path.

Default:
false
description string The description of the Object Store.
false
maxBytes integer The maximum size of the Store in bytes.
false
metadata map[string]string Additional Object Store metadata.
false
nkey string NATS user NKey for connecting to servers.

Default:
false
placement object The Object Store placement via tags or cluster name.
false
preventDelete boolean When true, the managed Object Store will not be deleted when the resource is deleted.

Default: false
false
preventUpdate boolean When true, the managed Object Store will not be updated when the resource is updated.

Default: false
false
replicas integer The number of replicas to keep for the Object Store in clustered JetStream.

Default: 1
Minimum: 1
Maximum: 5
false
servers []string A list of servers for creating the Object Store.

Default: []
false
storage enum The storage backend to use for the Object Store.

Enum: file, memory
false
tls object A client's TLS certs and keys.
false
tlsFirst boolean When true, the KV Store will initiate TLS before server INFO.

Default: false
false
ttl string The time expiry for keys.
false
### ObjectStore.spec.placement [↩ Parent](#objectstorespec) The Object Store placement via tags or cluster name.
Name Type Description Required
cluster string
false
tags []string
false
### ObjectStore.spec.tls [↩ Parent](#objectstorespec) A client's TLS certs and keys.
Name Type Description Required
clientCert string A client's cert filepath. Should be mounted.
false
clientKey string A client's key filepath. Should be mounted.
false
rootCas []string A list of filepaths to CAs. Should be mounted.
false
### ObjectStore.status [↩ Parent](#objectstore)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### ObjectStore.status.conditions[index] [↩ Parent](#objectstorestatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
# jetstream.nats.io/v1beta1 Resource Types: - [Stream](#stream) - [Consumer](#consumer) - [StreamTemplate](#streamtemplate) ## Stream [↩ Parent](#jetstreamnatsiov1beta1)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta1 true
kind string Stream true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### Stream.spec [↩ Parent](#stream-1)
Name Type Description Required
description string The description of the stream.
false
discard enum When a Stream reach it's limits either old messages are deleted or new ones are denied.

Enum: old, new
Default: old
false
duplicateWindow string The duration window to track duplicate messages for.
false
maxAge string Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited.

Default:
false
maxBytes integer How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited.

Default: -1
Minimum: -1
false
maxConsumers integer How many Consumers can be defined for a given Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgSize integer The largest message that will be accepted by the Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgs integer How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgsPerSubject integer The maximum number of messages per subject.

Default: 0
false
mirror object A stream mirror.
false
name string A unique name for the Stream.
false
noAck boolean Disables acknowledging messages that are received by the Stream.

Default: false
false
placement object A stream's placement.
false
replicas integer How many replicas to keep for each message.

Default: 1
Minimum: 1
false
retention enum How messages are retained in the Stream, once this is exceeded old messages are removed.

Enum: limits, interest, workqueue
Default: limits
false
sources []object A stream's sources.
false
storage enum The storage backend to use for the Stream.

Enum: file, memory
Default: memory
false
subjects []string A list of subjects to consume, supports wildcards.
false
### Stream.spec.mirror [↩ Parent](#streamspec-1) A stream mirror.
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
### Stream.spec.placement [↩ Parent](#streamspec-1) A stream's placement.
Name Type Description Required
cluster string
false
tags []string
false
### Stream.spec.sources[index] [↩ Parent](#streamspec-1)
Name Type Description Required
externalApiPrefix string
false
externalDeliverPrefix string
false
filterSubject string
false
name string
false
optStartSeq integer
false
optStartTime string Time format must be RFC3339.
false
### Stream.status [↩ Parent](#stream-1)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### Stream.status.conditions[index] [↩ Parent](#streamstatus-1)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## Consumer [↩ Parent](#jetstreamnatsiov1beta1)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta1 true
kind string Consumer true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### Consumer.spec [↩ Parent](#consumer-1)
Name Type Description Required
ackPolicy enum How messages should be acknowledged.

Enum: none, all, explicit
Default: none
false
ackWait string How long to allow messages to remain un-acknowledged before attempting redelivery.

Default: 1ns
false
deliverGroup string The name of a queue group.
false
deliverPolicy enum Enum: all, last, new, byStartSequence, byStartTime
Default: all
false
deliverSubject string The subject to deliver observed messages, when not set, a pull-based Consumer is created.
false
description string The description of the consumer.
false
durableName string The name of the Consumer.
false
filterSubject string Select only a specific incoming subjects, supports wildcards.
false
flowControl boolean Enables flow control.

Default: false
false
heartbeatInterval string The interval used to deliver idle heartbeats for push-based consumers, in Go's time.Duration format.
false
maxAckPending integer Maximum pending Acks before consumers are paused.
false
maxDeliver integer Minimum: -1
false
optStartSeq integer Minimum: 0
false
optStartTime string Time format must be RFC3339.
false
rateLimitBps integer Rate at which messages will be delivered to clients, expressed in bit per second.
false
replayPolicy enum How messages are sent.

Enum: instant, original
Default: instant
false
sampleFreq string What percentage of acknowledgements should be samples for observability.
false
streamName string The name of the Stream to create the Consumer in.
false
### Consumer.status [↩ Parent](#consumer-1)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### Consumer.status.conditions[index] [↩ Parent](#consumerstatus-1)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
## StreamTemplate [↩ Parent](#jetstreamnatsiov1beta1)
Name Type Description Required
apiVersion string jetstream.nats.io/v1beta1 true
kind string StreamTemplate true
metadata object Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
spec object
false
status object
false
### StreamTemplate.spec [↩ Parent](#streamtemplate)
Name Type Description Required
discard enum When a Stream reach it's limits either old messages are deleted or new ones are denied.

Enum: old, new
Default: old
false
duplicateWindow string The duration window to track duplicate messages for.
false
maxAge string Maximum age of any message in the stream, expressed in Go's time.Duration format. Empty for unlimited.

Default:
false
maxBytes integer How big the Stream may be, when the combined stream size exceeds this old messages are removed. -1 for unlimited.

Default: -1
Minimum: -1
false
maxConsumers integer How many Consumers can be defined for a given Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgSize integer The largest message that will be accepted by the Stream. -1 for unlimited.

Default: -1
Minimum: -1
false
maxMsgs integer How many messages may be in a Stream, oldest messages will be removed if the Stream exceeds this size. -1 for unlimited.

Default: -1
Minimum: -1
false
maxStreams integer The maximum number of Streams this Template can create, -1 for unlimited.

Default: -1
Minimum: -1
false
name string A unique name for the Stream Template.
false
noAck boolean Disables acknowledging messages that are received by the Stream.

Default: false
false
replicas integer How many replicas to keep for each message.

Default: 1
Minimum: 1
false
retention enum How messages are retained in the Stream, once this is exceeded old messages are removed.

Enum: limits, interest, workqueue
Default: limits
false
storage enum The storage backend to use for the Stream.

Enum: file, memory
Default: memory
false
subjects []string A list of subjects to consume, supports wildcards.
false
### StreamTemplate.status [↩ Parent](#streamtemplate)
Name Type Description Required
conditions []object
false
observedGeneration integer
false
### StreamTemplate.status.conditions[index] [↩ Parent](#streamtemplatestatus)
Name Type Description Required
lastTransitionTime string
false
message string
false
reason string
false
status string
false
type string
false
================================================ FILE: examples/secure/client-tls.yaml ================================================ --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nats-sys-tls spec: secretName: nats-sys-tls duration: 2160h # 90 days renewBefore: 240h # 10 days issuerRef: name: nats-ca kind: Issuer usages: - digital signature - key encipherment - client auth commonName: nats-sys-user ================================================ FILE: examples/secure/issuer.yaml ================================================ --- apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigning spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nats-ca spec: secretName: nats-ca duration: 8736h # 1 year renewBefore: 240h # 10 days issuerRef: name: selfsigning kind: ClusterIssuer commonName: nats-ca isCA: true --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: nats-ca spec: ca: secretName: nats-ca ================================================ FILE: examples/secure/nack/account-foo.yaml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Account metadata: name: a spec: name: a servers: - nats://nats:4222 tls: secret: name: nack-a-tls ca: "ca.crt" cert: "tls.crt" key: "tls.key" ================================================ FILE: examples/secure/nack/nats-account-a.yaml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Account metadata: name: a spec: name: a servers: - nats://nats:4222 tls: secret: name: nack-a-tls ca: "ca.crt" cert: "tls.crt" key: "tls.key" ================================================ FILE: examples/secure/nack/nats-consumer-bar-a.yaml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Consumer metadata: name: bar spec: streamName: foo durableName: bar ackPolicy: explicit account: a ================================================ FILE: examples/secure/nack/nats-stream-foo-a.yaml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: foo spec: name: foo subjects: ["foo", "foo.>"] storage: file replicas: 1 account: a ================================================ FILE: examples/secure/nack/stream-foo.yaml ================================================ --- apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: foo spec: name: foo subjects: ["foo", "foo.>"] storage: file replicas: 1 account: a ================================================ FILE: examples/secure/nack-a-client-tls.yaml ================================================ --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nack-a-tls spec: secretName: nack-a-tls duration: 2160h # 90 days renewBefore: 240h # 10 days issuerRef: name: nats-ca kind: Issuer usages: - digital signature - key encipherment - client auth commonName: nack-a ================================================ FILE: examples/secure/nack-b-client-tls.yaml ================================================ --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nack-b-tls spec: secretName: nack-b-tls duration: 2160h # 90 days renewBefore: 240h # 10 days issuerRef: name: nats-ca kind: Issuer usages: - digital signature - key encipherment - client auth commonName: nack-b ================================================ FILE: examples/secure/nats-helm.yaml ================================================ tlsCA: enabled: true secretName: nats-sys-tls key: ca.crt config: cluster: enabled: true jetstream: enabled: true memoryStore: enabled: true maxSize: 2Gi fileStore: enabled: true pvc: enabled: true size: 1Gi nats: tls: enabled: true secretName: nats-server-tls cert: tls.crt key: tls.key merge: verify_and_map: true merge: system_account: SYS accounts: SYS: users: - user: CN=nats-sys-user A: jetstream: true users: - user: CN=nack-a B: jetstream: true users: - user: CN=nack-b natsBox: contexts: default: tls: secretName: nack-a-tls cert: tls.crt key: tls.key ================================================ FILE: examples/secure/server-tls.yaml ================================================ --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nats-server-tls spec: secretName: nats-server-tls duration: 2160h # 90 days renewBefore: 240h # 10 days issuerRef: name: nats-ca kind: Issuer commonName: nats.default.svc.cluster.local dnsNames: - nats - nats.default - nats.default.svc - nats.default.svc.cluster.local - '*.nats' - '*.nats.default' - '*.nats.default.svc' ================================================ FILE: go.mod ================================================ module github.com/nats-io/nack go 1.25.0 require ( github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/nats-io/jsm.go v0.3.0 github.com/nats-io/nats-server/v2 v2.12.6 github.com/nats-io/nats.go v1.50.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.20.0 k8s.io/api v0.35.3 k8s.io/apimachinery v0.35.3 k8s.io/client-go v0.35.3 k8s.io/code-generator v0.35.3 k8s.io/klog/v2 v2.140.0 sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 ) require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/expr-lang/expr v1.17.7 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // 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/nats-io/jwt/v2 v2.8.1 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.42.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // indirect k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: go.sum ================================================ 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/antithesishq/antithesis-sdk-go v0.5.0 h1:cudCFF83pDDANcXFzkQPUHHedfnnIbUO3JMr9fqwFJs= github.com/antithesishq/antithesis-sdk-go v0.5.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 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/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/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/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= 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-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= 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/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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 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/nats-io/jsm.go v0.3.0 h1:fl0yZtK6U+kkAQ/mm5AXzRGHwJa35j2E+aopCcebfgU= github.com/nats-io/jsm.go v0.3.0/go.mod h1:PLObK5L+Vcq1GGGJY3DfrFs8QaSDuWF0co81sEHdFcg= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 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.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 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/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/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.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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.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/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.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= k8s.io/code-generator v0.35.3 h1:NDGCLkEm6Ho65wTdSe2EgErmmtsrezOPwwOchlNc6FQ= k8s.io/code-generator v0.35.3/go.mod h1:LAVriRGXQusHQ0Ns64SE1ublSswm1KrK7cXn0GuQETg= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 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/controller-runtime v0.23.0 h1:Ubi7klJWiwEWqDY+odSVZiFA0aDSevOCXpa38yCSYu8= sigs.k8s.io/controller-runtime v0.23.0/go.mod h1:DBOIr9NsprUqCZ1ZhsuJ0wAnQSIxY/C6VjZbmLgw0j0= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= 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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/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= ================================================ FILE: internal/controller/account_controller.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "errors" "fmt" "github.com/go-logr/logr" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // AccountReconciler reconciles a Account object type AccountReconciler struct { Scheme *runtime.Scheme JetStreamController } type JetStreamResource interface { GetName() string GetNamespace() string } type JetStreamResourceList []JetStreamResource // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // It performs two main operations: // - Initialize finalizer if not present // - Remove the finalizer on deletion once no other resources are referencing the account // // A call to reconcile may perform only one action, expecting the reconciliation to be triggered again by an update. // For example: Setting the finalizer triggers a second reconciliation. Reconcile returns after setting the finalizer, // to prevent parallel reconciliations performing the same steps. func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := klog.FromContext(ctx) if ok := r.ValidNamespace(req.Namespace); !ok { log.Info("Controller restricted to namespace, skipping reconciliation.") return ctrl.Result{}, nil } // Fetch Account resource account := &api.Account{} if err := r.Get(ctx, req.NamespacedName, account); err != nil { if apierrors.IsNotFound(err) { log.Info("Account deleted.", "accountName", req.NamespacedName.String()) return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get account resource '%s': %w", req.NamespacedName.String(), err) } // Update ready status to unknown when no status is set if len(account.Status.Conditions) == 0 { log.Info("Setting initial ready condition to unknown.") account.Status.Conditions = updateReadyCondition(account.Status.Conditions, v1.ConditionUnknown, stateReconciling, "Starting reconciliation") err := r.Status().Update(ctx, account) if err != nil { // If we get a conflict error, another reconciliation has already updated the status. // Just requeue and let the next reconciliation handle it. if apierrors.IsConflict(err) { return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("set condition unknown: %w", err) } r.Get(ctx, req.NamespacedName, account) log.Info("Status", "Conditions", account.Status.Conditions) return ctrl.Result{Requeue: true}, nil } // Check Deletion markedForDeletion := account.GetDeletionTimestamp() != nil if markedForDeletion { if controllerutil.ContainsFinalizer(account, accountFinalizer) { // Get list of resources referencing this account requests := r.findDependentResources(ctx, log, account) if len(requests) > 0 { log.Info("Account still has dependent resources, cannot delete", "dependentCount", len(requests)) account.Status.Conditions = updateReadyCondition( account.Status.Conditions, v1.ConditionFalse, stateFinalizing, "Account has dependent resources that must be deleted first", ) if err := r.Status().Update(ctx, account); err != nil { return ctrl.Result{}, fmt.Errorf("update status: %w", err) } return ctrl.Result{Requeue: true}, nil } log.Info("Removing Account finalizer") if ok := controllerutil.RemoveFinalizer(account, accountFinalizer); !ok { return ctrl.Result{}, errors.New("failed to remove finalizer") } if err := r.Update(ctx, account); err != nil { return ctrl.Result{}, fmt.Errorf("remove finalizer: %w", err) } } else { log.Info("Account marked for deletion and already finalized. Ignoring.") } return ctrl.Result{}, nil } // Add finalizer if !controllerutil.ContainsFinalizer(account, accountFinalizer) { log.Info("Adding Account finalizer.") if ok := controllerutil.AddFinalizer(account, accountFinalizer); !ok { return ctrl.Result{}, errors.New("failed to add finalizer to account resource") } if err := r.Update(ctx, account); err != nil { return ctrl.Result{}, fmt.Errorf("update account resource to add finalizer: %w", err) } return ctrl.Result{Requeue: true}, nil } // Update ready status for non-deleted accounts account.Status.ObservedGeneration = account.Generation account.Status.Conditions = updateReadyCondition( account.Status.Conditions, v1.ConditionTrue, stateReady, "Account is ready", ) if err := r.Status().Update(ctx, account); err != nil { return ctrl.Result{}, fmt.Errorf("update status: %w", err) } return ctrl.Result{}, nil } func (r *AccountReconciler) findDependentResources(ctx context.Context, log logr.Logger, obj client.Object) []reconcile.Request { var resourceList JetStreamResourceList var consumerList api.ConsumerList if err := r.List(ctx, &consumerList, client.InNamespace(obj.GetNamespace()), ); err != nil { log.Error(err, "Failed to list consumers") } for _, i := range consumerList.Items { if i.Spec.Account == obj.GetName() { resourceList = append(resourceList, &i) } } var keyValueList api.KeyValueList if err := r.List(ctx, &keyValueList, client.InNamespace(obj.GetNamespace()), ); err != nil { log.Error(err, "Failed to list accounts") } for _, i := range keyValueList.Items { if i.Spec.Account == obj.GetName() { resourceList = append(resourceList, &i) } } var objectStoreList api.ObjectStoreList if err := r.List(ctx, &objectStoreList, client.InNamespace(obj.GetNamespace()), ); err != nil { log.Error(err, "Failed to list objectstores") } for _, i := range objectStoreList.Items { if i.Spec.Account == obj.GetName() { resourceList = append(resourceList, &i) } } var streamList api.StreamList if err := r.List(ctx, &streamList, client.InNamespace(obj.GetNamespace()), ); err != nil { log.Error(err, "Failed to list streams") } for _, i := range streamList.Items { if i.Spec.Account == obj.GetName() { resourceList = append(resourceList, &i) } } requests := make([]reconcile.Request, 0, len(resourceList)) for _, resource := range resourceList { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: resource.GetNamespace(), Name: resource.GetName(), }, }) } return requests } // SetupWithManager sets up the controller with the Manager. func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.Account{}). WithEventFilter(predicate.GenerationChangedPredicate{}). WithOptions(controller.Options{ MaxConcurrentReconciles: 1, }). Complete(r) } ================================================ FILE: internal/controller/account_controller_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) var _ = Describe("Account Controller", func() { Context("When reconciling a resource", func() { const resourceName = "test-account" typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", } account := &api.Account{} // Tested controller var controller *AccountReconciler BeforeEach(func(ctx SpecContext) { controller = &AccountReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: baseController, } }) When("the resource is marked for deletion", func() { var stream *api.Stream var streamName types.NamespacedName BeforeEach(func(ctx SpecContext) { By("creating the custom resource for the Kind Account") err := k8sClient.Get(ctx, typeNamespacedName, account) if err != nil && k8serrors.IsNotFound(err) { resource := &api.Account{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: api.AccountSpec{ Servers: []string{"nats://nats.io"}, }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, account)).To(Succeed()) controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(k8sClient.Get(ctx, typeNamespacedName, account)).To(Succeed()) Expect(controllerutil.ContainsFinalizer(account, accountFinalizer)).To(BeTrue()) Expect(len(account.Status.Conditions)).To(BeNumerically("==", 1)) Expect(account.Status.Conditions[0].Type).To(Equal(readyCondType)) Expect(account.Status.Conditions[0].Status).To(Equal(v1.ConditionTrue)) Expect(account.Status.Conditions[0].Message).To(Equal("Account is ready")) } By("creating a dependent stream resource") stream = &api.Stream{ ObjectMeta: metav1.ObjectMeta{ Name: "test-stream", Namespace: "default", }, Spec: api.StreamSpec{ Name: "test-stream", Replicas: 1, Discard: "old", Storage: "file", Retention: "workqueue", BaseStreamConfig: api.BaseStreamConfig{ ConnectionOpts: api.ConnectionOpts{ Account: resourceName, }, }, }, } streamName = types.NamespacedName{ Name: stream.Name, Namespace: stream.Namespace, } Expect(k8sClient.Create(ctx, stream)).To(Succeed()) By("marking the account for deletion") Expect(k8sClient.Delete(ctx, account)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, account)).To(Succeed()) }) AfterEach(func(ctx SpecContext) { By("cleaning up the stream") stream := &api.Stream{} err := k8sClient.Get(ctx, streamName, stream) if err == nil { Expect(k8sClient.Delete(ctx, stream)).To(Succeed()) } By("removing the account resource") controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(k8sClient.Get(ctx, typeNamespacedName, account)).To(Not(Succeed())) }) It("should not remove finalizer while dependent resources exist", func(ctx SpecContext) { By("reconciling the deletion") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.Requeue).To(BeTrue()) By("checking the account still exists") Expect(k8sClient.Get(ctx, typeNamespacedName, account)).To(Succeed()) Expect(account.Finalizers).To(ContainElement(accountFinalizer)) By("verifying the ready condition is set to false") Expect(account.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches( account.Status.Conditions[0], v1.ConditionFalse, stateFinalizing, "Account has dependent resources that must be deleted first", time.Now(), ) }) It("should remove finalizer after dependent resources are removed", func(ctx SpecContext) { By("removing the dependent stream") Expect(k8sClient.Delete(ctx, stream)).To(Succeed()) By("reconciling the deletion") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking the account is deleted") err = k8sClient.Get(ctx, typeNamespacedName, account) Expect(err).To(HaveOccurred()) Expect(k8serrors.IsNotFound(err)).To(BeTrue()) }) It("should remove finalizer after dependent resources are updated", func(ctx SpecContext) { By("updating the dependent stream to remove account reference") Expect(k8sClient.Get(ctx, streamName, stream)).To(Succeed()) stream.Spec.Account = "" Expect(k8sClient.Update(ctx, stream)).To(Succeed()) By("reconciling the deletion") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking the account is deleted") err = k8sClient.Get(ctx, typeNamespacedName, account) Expect(err).To(HaveOccurred()) Expect(k8serrors.IsNotFound(err)).To(BeTrue()) }) }) }) }) ================================================ FILE: internal/controller/client.go ================================================ package controller import ( "crypto/sha256" "encoding/json" "errors" "fmt" "os" "github.com/nats-io/jsm.go" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) type NatsConfig struct { ClientName string `json:"name,omitempty"` ServerURL string `json:"url,omitempty"` Certificate string `json:"tls_cert,omitempty"` Key string `json:"tls_key,omitempty"` TLSFirst bool `json:"tls_first,omitempty"` CAs []string `json:"tls_ca,omitempty"` Credentials string `json:"credential,omitempty"` NKey string `json:"nkey,omitempty"` Token string `json:"token,omitempty"` User string `json:"username,omitempty"` Password string `json:"password,omitempty"` JsDomain string `json:"js_domain,omitempty"` } func (o *NatsConfig) Copy() *NatsConfig { if o == nil { return nil } cp := *o return &cp } func (o *NatsConfig) Hash() (string, error) { b, err := json.Marshal(o) if err != nil { return "", fmt.Errorf("error marshaling config to json: %v", err) } if o.NKey != "" { fb, err := os.ReadFile(o.NKey) if err != nil { return "", fmt.Errorf("error opening nkey file %s: %v", o.NKey, err) } b = append(b, fb...) } if o.Credentials != "" { fb, err := os.ReadFile(o.Credentials) if err != nil { return "", fmt.Errorf("error opening creds file %s: %v", o.Credentials, err) } b = append(b, fb...) } if len(o.CAs) > 0 { for _, cert := range o.CAs { fb, err := os.ReadFile(cert) if err != nil { return "", fmt.Errorf("error opening ca file %s: %v", cert, err) } b = append(b, fb...) } } if o.Certificate != "" { fb, err := os.ReadFile(o.Certificate) if err != nil { return "", fmt.Errorf("error opening cert file %s: %v", o.Certificate, err) } b = append(b, fb...) } if o.Key != "" { fb, err := os.ReadFile(o.Key) if err != nil { return "", fmt.Errorf("error opening key file %s: %v", o.Key, err) } b = append(b, fb...) } hash := sha256.New() hash.Write(b) return fmt.Sprintf("%x", hash.Sum(nil)), nil } func (o *NatsConfig) Overlay(overlay *NatsConfig) { if overlay.ClientName != "" { o.ClientName = overlay.ClientName } if overlay.ServerURL != "" { o.ServerURL = overlay.ServerURL } if overlay.JsDomain != "" { o.JsDomain = overlay.JsDomain } if overlay.Certificate != "" && overlay.Key != "" { o.Certificate = overlay.Certificate o.Key = overlay.Key } if len(overlay.CAs) > 0 { o.CAs = overlay.CAs } if overlay.TLSFirst { o.TLSFirst = overlay.TLSFirst } if !overlay.HasAuth() { return } o.UnsetAuth() if overlay.Credentials != "" { o.Credentials = overlay.Credentials } else if overlay.NKey != "" { o.NKey = overlay.NKey } else if overlay.Token != "" { o.Token = overlay.Token } else if overlay.User != "" && overlay.Password != "" { o.User = overlay.User o.Password = overlay.Password } } func (o *NatsConfig) HasAuth() bool { return o.Credentials != "" || o.NKey != "" || o.Token != "" || (o.User != "" && o.Password != "") } func (o *NatsConfig) UnsetAuth() { o.Credentials = "" o.NKey = "" o.User = "" o.Password = "" o.Token = "" } // buildOptions creates options from the config to be used in nats.Connect. func (o *NatsConfig) buildOptions() ([]nats.Option, error) { opts := make([]nats.Option, 0) if o.ClientName != "" { opts = append(opts, nats.Name(o.ClientName)) } if o.ServerURL == "" { return nil, fmt.Errorf("server url is required") } if o.Certificate != "" && o.Key != "" { opts = append(opts, nats.ClientCert(o.Certificate, o.Key)) } if o.TLSFirst { opts = append(opts, nats.TLSHandshakeFirst()) } if len(o.CAs) > 0 { opts = append(opts, nats.RootCAs(o.CAs...)) } if o.Credentials != "" { opts = append(opts, nats.UserCredentials(o.Credentials)) } if o.NKey != "" { opt, err := nats.NkeyOptionFromSeed(o.NKey) if err != nil { return nil, fmt.Errorf("nkey option from seed: %w", err) } opts = append(opts, opt) } if o.Token != "" { opts = append(opts, nats.Token(o.Token)) } if o.User != "" && o.Password != "" { opts = append(opts, nats.UserInfo(o.User, o.Password)) } return opts, nil } type Closable interface { Close() } func CreateJSMClient(conn *pooledConnection, pedantic bool, domain string) (*jsm.Manager, error) { if !conn.nc.IsConnected() { return nil, errors.New("not connected") } major, minor, _, err := versionComponents(conn.nc.ConnectedServerVersion()) if err != nil { return nil, fmt.Errorf("parse server version: %w", err) } // JetStream pedantic mode unsupported prior to NATS Server 2.11 if pedantic && major < 2 || (major == 2 && minor < 11) { pedantic = false } jsmOpts := make([]jsm.Option, 0) if pedantic { jsmOpts = append(jsmOpts, jsm.WithPedanticRequests()) } if domain != "" { jsmOpts = append(jsmOpts, jsm.WithDomain(domain)) } jsmClient, err := jsm.New(conn.nc, jsmOpts...) if err != nil { return nil, fmt.Errorf("new jsm client: %w", err) } return jsmClient, nil } // CreateJetStreamClient creates new Jetstream client with a connection based on the given NatsConfig. // Returns a jetstream.Jetstream client and the Closable of the underlying connection. // Close should be called when the client is no longer used. func CreateJetStreamClient(conn *pooledConnection, pedantic bool, domain string) (jetstream.JetStream, error) { var ( err error js jetstream.JetStream ) if domain != "" { js, err = jetstream.NewWithDomain(conn.nc, domain) } else { js, err = jetstream.New(conn.nc) } if err != nil { return nil, fmt.Errorf("new jetstream: %w", err) } return js, nil } func createNatsConn(cfg *NatsConfig) (*nats.Conn, error) { opts, err := cfg.buildOptions() if err != nil { return nil, err } // client should always attempt to reconnect opts = append(opts, nats.MaxReconnects(-1)) return nats.Connect(cfg.ServerURL, opts...) } ================================================ FILE: internal/controller/connection_pool.go ================================================ package controller import ( "sync" "time" "github.com/nats-io/nats.go" ) type pooledConnection struct { nc *nats.Conn pool *connectionPool hash string refCount int } func (pc *pooledConnection) Close() { if pc.pool != nil { pc.pool.release(pc.hash) } else if pc.nc != nil { pc.nc.Close() // Close directly if not pool-managed } } type connectionPool struct { connections map[string]*pooledConnection gracePeriod time.Duration mu sync.Mutex } func newConnPool(gracePeriod time.Duration) *connectionPool { return &connectionPool{ connections: make(map[string]*pooledConnection), gracePeriod: gracePeriod, } } func (p *connectionPool) Get(c *NatsConfig, pedantic bool) (*pooledConnection, error) { p.mu.Lock() defer p.mu.Unlock() hash, err := c.Hash() if err != nil { // If hash fails, create a new non-pooled connection nc, err := createNatsConn(c) if err != nil { return nil, err } return &pooledConnection{nc: nc}, nil } if pc, ok := p.connections[hash]; ok && !pc.nc.IsClosed() { pc.refCount++ return pc, nil } nc, err := createNatsConn(c) if err != nil { return nil, err } pc := &pooledConnection{ nc: nc, pool: p, hash: hash, refCount: 1, } p.connections[hash] = pc return pc, nil } func (p *connectionPool) release(hash string) { p.mu.Lock() defer p.mu.Unlock() pc, ok := p.connections[hash] if !ok { return } pc.refCount-- if pc.refCount < 1 { go func() { if p.gracePeriod > 0 { time.Sleep(p.gracePeriod) } p.mu.Lock() defer p.mu.Unlock() if pc, ok := p.connections[hash]; ok && pc.refCount < 1 { pc.nc.Close() delete(p.connections, hash) } }() } } ================================================ FILE: internal/controller/connection_pool_test.go ================================================ package controller import ( "sync" "testing" "time" natsservertest "github.com/nats-io/nats-server/v2/test" "github.com/stretchr/testify/require" ) func TestConnPool(t *testing.T) { t.Parallel() s := natsservertest.RunRandClientPortServer() defer s.Shutdown() c1 := &NatsConfig{ ClientName: "Client 1", ServerURL: s.ClientURL(), } c2 := &NatsConfig{ ClientName: "Client 1", ServerURL: s.ClientURL(), } c3 := &NatsConfig{ ClientName: "Client 2", ServerURL: s.ClientURL(), } pool := newConnPool(0) var conn1, conn2, conn3 *pooledConnection var err1, err2, err3 error wg := &sync.WaitGroup{} wg.Add(3) go func() { conn1, err1 = pool.Get(c1, true) wg.Done() }() go func() { conn2, err2 = pool.Get(c2, true) wg.Done() }() go func() { conn3, err3 = pool.Get(c3, true) wg.Done() }() wg.Wait() require := require.New(t) require.NoError(err1) require.NoError(err2) require.NoError(err3) require.Same(conn1, conn2) require.NotSame(conn1, conn3) require.NotSame(conn2, conn3) conn1.Close() conn3.Close() time.Sleep(time.Second) require.False(conn1.nc.IsClosed()) require.False(conn2.nc.IsClosed()) require.True(conn3.nc.IsClosed()) conn4, err4 := pool.Get(c1, true) require.NoError(err4) require.Same(conn1, conn4) require.Same(conn2, conn4) conn2.Close() conn4.Close() time.Sleep(time.Second) require.True(conn1.nc.IsClosed()) require.True(conn2.nc.IsClosed()) require.True(conn3.nc.IsClosed()) require.True(conn4.nc.IsClosed()) conn5, err5 := pool.Get(c1, true) require.NoError(err5) require.NotSame(conn1, conn5) conn5.Close() time.Sleep(time.Second) require.True(conn5.nc.IsClosed()) } ================================================ FILE: internal/controller/consumer_controller.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/predicate" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ctrl "sigs.k8s.io/controller-runtime" ) // ConsumerReconciler reconciles a Consumer object type ConsumerReconciler struct { Scheme *runtime.Scheme JetStreamController } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile func (r *ConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := klog.FromContext(ctx) if ok := r.ValidNamespace(req.Namespace); !ok { log.Info("Controller restricted to namespace, skipping reconciliation.") return ctrl.Result{}, nil } // Fetch consumer resource consumer := &api.Consumer{} if err := r.Get(ctx, req.NamespacedName, consumer); err != nil { if apierrors.IsNotFound(err) { log.Info("Consumer resource deleted.", "consumerName", req.NamespacedName.String()) return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get consumer resource '%s': %w", req.NamespacedName.String(), err) } log = log.WithValues( "streamName", consumer.Spec.StreamName, "consumerName", consumer.Spec.DurableName, ) // Update ready status to unknown when no status is set if len(consumer.Status.Conditions) == 0 { log.Info("Setting initial ready condition to unknown.") consumer.Status.Conditions = updateReadyCondition(consumer.Status.Conditions, v1.ConditionUnknown, stateReconciling, "Starting reconciliation") err := r.Status().Update(ctx, consumer) if err != nil { // If we get a conflict error, another reconciliation has already updated the status. // Just requeue and let the next reconciliation handle it. if apierrors.IsConflict(err) { return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("set condition unknown: %w", err) } return ctrl.Result{Requeue: true}, nil } // Check Deletion markedForDeletion := consumer.GetDeletionTimestamp() != nil if markedForDeletion { if controllerutil.ContainsFinalizer(consumer, consumerFinalizer) { err := r.deleteConsumer(ctx, log, consumer) if err != nil { return ctrl.Result{}, fmt.Errorf("delete consumer: %w", err) } } else { log.Info("Consumer marked for deletion and already finalized. Ignoring.") } return ctrl.Result{}, nil } // Add finalizer if !controllerutil.ContainsFinalizer(consumer, consumerFinalizer) { log.Info("Adding consumer finalizer.") if ok := controllerutil.AddFinalizer(consumer, consumerFinalizer); !ok { return ctrl.Result{}, errors.New("failed to add finalizer to consumer resource") } if err := r.Update(ctx, consumer); err != nil { return ctrl.Result{}, fmt.Errorf("update consumer resource to add finalizer: %w", err) } return ctrl.Result{Requeue: true}, nil } // Create or update stream if err := r.createOrUpdate(ctx, log, consumer); err != nil { if err := r.Get(ctx, client.ObjectKeyFromObject(consumer), consumer); err != nil { return ctrl.Result{}, fmt.Errorf("get consumer resource: %w", err) } consumer.Status.Conditions = updateReadyCondition(consumer.Status.Conditions, v1.ConditionFalse, stateErrored, err.Error()) if err := r.Status().Update(ctx, consumer); err != nil { log.Error(err, "Failed to update ready condition to Errored.") } return ctrl.Result{}, fmt.Errorf("create or update: %s", err) } return ctrl.Result{RequeueAfter: r.RequeueInterval()}, nil } func (r *ConsumerReconciler) deleteConsumer(ctx context.Context, log logr.Logger, consumer *api.Consumer) error { // Set status to false consumer.Status.Conditions = updateReadyCondition(consumer.Status.Conditions, v1.ConditionFalse, stateFinalizing, "Performing finalizer operations.") if err := r.Status().Update(ctx, consumer); err != nil { return fmt.Errorf("update ready condition: %w", err) } storedState, err := getStoredConsumerState(consumer) if err != nil { log.Error(err, "Failed to fetch stored state.") } if !consumer.Spec.PreventDelete && !r.ReadOnly() { err := r.WithJSMClient(consumer.Spec.ConnectionOpts, consumer.Namespace, func(js *jsm.Manager) error { _, err := getServerConsumerState(js, consumer) // If we have no known state for this consumer it has never been reconciled. // If we are also receiving an error fetching state, either the consumer does not exist // or this resource config is invalid. if err != nil && storedState == nil { return nil } return js.DeleteConsumer(consumer.Spec.StreamName, consumer.Spec.DurableName) }) switch { case jsm.IsNatsError(err, JSConsumerNotFoundErr): log.Info("Consumer does not exist. Unable to delete.") case jsm.IsNatsError(err, JSStreamNotFoundErr): log.Info("Stream of consumer does not exist. Unable to delete.") case err != nil: if storedState == nil { log.Info("Consumer not reconciled and no state received from server. Removing finalizer.") } else { return fmt.Errorf("delete jetstream consumer: %w", err) } default: log.Info("Consumer deleted.") } } else { log.Info("Skipping consumer deletion.", "consumerName", consumer.Spec.DurableName, "preventDelete", consumer.Spec.PreventDelete, "read-only", r.ReadOnly(), ) } log.Info("Removing consumer finalizer.") if ok := controllerutil.RemoveFinalizer(consumer, consumerFinalizer); !ok { return errors.New("failed to remove consumer finalizer") } if err := r.Update(ctx, consumer); err != nil { return fmt.Errorf("remove finalizer: %w", err) } return nil } func (r *ConsumerReconciler) createOrUpdate(ctx context.Context, log klog.Logger, consumer *api.Consumer) error { // Create or Update the stream based on the spec // Map spec to consumer target config targetConfig, err := consumerSpecToConfig(&consumer.Spec) if err != nil { return fmt.Errorf("map consumer spec to target config: %w", err) } err = r.WithJSMClient(consumer.Spec.ConnectionOpts, consumer.Namespace, func(js *jsm.Manager) error { storedState, err := getStoredConsumerState(consumer) if err != nil { log.Error(err, "Failed to fetch stored consumer state.") } serverState, err := getServerConsumerState(js, consumer) if err != nil { return fmt.Errorf("fetching consumer current state: %w", err) } // Check against known state. Skip Update if converged. // Storing returned state from the server avoids have to // check default values or call Update on already converged resources if storedState != nil && serverState != nil && consumer.Status.ObservedGeneration == consumer.Generation { diff := compareConfigState(storedState, serverState) if diff == "" { return nil } log.Info("Consumer config drifted from desired state.", "diff", diff) } if r.ReadOnly() { log.Info("Skipping consumer creation or update.", "read-only", r.ReadOnly(), ) return nil } var updatedConsumer *jsm.Consumer err = nil if serverState == nil { log.Info("Creating Consumer.") updatedConsumer, err = js.NewConsumer(consumer.Spec.StreamName, targetConfig...) if err != nil { return fmt.Errorf("creating consumer: %w", err) } } else if !consumer.Spec.PreventUpdate { log.Info("Updating Consumer.") c, err := js.LoadConsumer(consumer.Spec.StreamName, consumer.Spec.DurableName) if err != nil { return fmt.Errorf("loading consumer: %w", err) } err = c.UpdateConfiguration(targetConfig...) if err != nil { return fmt.Errorf("updating the consumer configuration: %w", err) } updatedConsumer, err = js.LoadConsumer(consumer.Spec.StreamName, consumer.Spec.DurableName) if err != nil { return fmt.Errorf("loading updated consumer: %w", err) } diff := compareConfigState(updatedConsumer.Configuration(), *serverState) log.Info("Updated Consumer.", "diff", diff) } else { log.Info("Skipping Consumer update.", "preventUpdate", consumer.Spec.PreventUpdate, ) } if updatedConsumer != nil { // Store known state in annotation updatedState, err := json.Marshal(updatedConsumer.Configuration()) if err != nil { return fmt.Errorf("marshaling JSON: %w", err) } if consumer.Annotations == nil { consumer.Annotations = map[string]string{} } consumer.Annotations[stateAnnotationConsumer] = string(updatedState) return r.Update(ctx, consumer) } return nil }) if err != nil { err = fmt.Errorf("create or update consumer: %w", err) consumer.Status.Conditions = updateReadyCondition(consumer.Status.Conditions, v1.ConditionFalse, stateErrored, err.Error()) if err := r.Status().Update(ctx, consumer); err != nil { log.Error(err, "Failed to update ready condition to Errored.") } return err } // update the observed generation and ready status consumer.Status.ObservedGeneration = consumer.Generation consumer.Status.Conditions = updateReadyCondition( consumer.Status.Conditions, v1.ConditionTrue, stateReady, "Consumer successfully created or updated.", ) err = r.Status().Update(ctx, consumer) if err != nil { return fmt.Errorf("update ready condition: %w", err) } return nil } func getStoredConsumerState(consumer *api.Consumer) (*jsmapi.ConsumerConfig, error) { var storedState *jsmapi.ConsumerConfig if state, ok := consumer.Annotations[stateAnnotationConsumer]; ok { err := json.Unmarshal([]byte(state), &storedState) if err != nil { return nil, err } } return storedState, nil } // Fetch the current state of the consumer from the server. // ErrConsumerNotFound is considered a valid response and does not return error func getServerConsumerState(js *jsm.Manager, consumer *api.Consumer) (*jsmapi.ConsumerConfig, error) { c, err := js.LoadConsumer(consumer.Spec.StreamName, consumer.Spec.DurableName) if jsm.IsNatsError(err, JSConsumerNotFoundErr) { return nil, nil } if err != nil { return nil, err } consumerCfg := c.Configuration() return &consumerCfg, nil } func consumerSpecToConfig(spec *api.ConsumerSpec) ([]jsm.ConsumerOption, error) { opts := []jsm.ConsumerOption{ jsm.ConsumerDescription(spec.Description), jsm.DeliverySubject(spec.DeliverSubject), jsm.DeliverGroup(spec.DeliverGroup), jsm.DurableName(spec.DurableName), jsm.MaxAckPending(uint(spec.MaxAckPending)), jsm.MaxWaiting(uint(spec.MaxWaiting)), jsm.RateLimitBitsPerSecond(uint64(spec.RateLimitBps)), jsm.MaxRequestBatch(uint(spec.MaxRequestBatch)), jsm.MaxRequestMaxBytes(spec.MaxRequestMaxBytes), jsm.ConsumerOverrideReplicas(spec.Replicas), jsm.ConsumerMetadata(spec.Metadata), } // ackPolicy switch spec.AckPolicy { case "none": opts = append(opts, jsm.AcknowledgeNone()) case "all": opts = append(opts, jsm.AcknowledgeAll()) case "explicit": opts = append(opts, jsm.AcknowledgeExplicit()) case "": default: return nil, fmt.Errorf("invalid value for 'ackPolicy': '%s'. Must be one of 'none', 'all', 'explicit'", spec.AckPolicy) } // ackWait if spec.AckWait != "" { d, err := time.ParseDuration(spec.AckWait) if err != nil { return nil, fmt.Errorf("invalid ack wait duration: %w", err) } opts = append(opts, jsm.AckWait(d)) } // deliverPolicy switch spec.DeliverPolicy { case "all": opts = append(opts, jsm.DeliverAllAvailable()) case "last": opts = append(opts, jsm.StartWithLastReceived()) case "new": opts = append(opts, jsm.StartWithNextReceived()) case "byStartSequence": opts = append(opts, jsm.StartAtSequence(uint64(spec.OptStartSeq))) case "byStartTime": if spec.OptStartTime == "" { return nil, fmt.Errorf("'optStartTime' is required for deliver policy 'byStartTime'") } t, err := time.Parse(time.RFC3339, spec.OptStartTime) if err != nil { return nil, err } opts = append(opts, jsm.StartAtTime(t)) case "lastPerSubject": opts = append(opts, jsm.DeliverLastPerSubject()) case "": default: return nil, fmt.Errorf("invalid value for 'deliverPolicy': '%s'. Must be one of 'all', 'last', 'new', 'lastPerSubject', 'byStartSequence', 'byStartTime'", spec.DeliverPolicy) } // filterSubject if spec.FilterSubject != "" && len(spec.FilterSubjects) > 0 { return nil, errors.New("cannot set both 'filterSubject' and 'filterSubjects'") } if spec.FilterSubject != "" { opts = append(opts, jsm.FilterStreamBySubject(spec.FilterSubject)) } else if len(spec.FilterSubjects) > 0 { opts = append(opts, jsm.FilterStreamBySubject(spec.FilterSubjects...)) } // flowControl if spec.FlowControl { opts = append(opts, jsm.PushFlowControl()) } // heartbeatInterval if spec.HeartbeatInterval != "" { d, err := time.ParseDuration(spec.HeartbeatInterval) if err != nil { return nil, fmt.Errorf("invalid heartbeat interval: %w", err) } opts = append(opts, jsm.IdleHeartbeat(d)) } // maxDeliver if spec.MaxDeliver != 0 { opts = append(opts, jsm.MaxDeliveryAttempts(spec.MaxDeliver)) } // backoff if len(spec.BackOff) > 0 { backoffs := make([]time.Duration, 0) for _, bo := range spec.BackOff { d, err := time.ParseDuration(bo) if err != nil { return nil, fmt.Errorf("invalid backoff: %w", err) } backoffs = append(backoffs, d) } opts = append(opts, jsm.BackoffIntervals(backoffs...)) } // replayPolicy switch spec.ReplayPolicy { case "instant": opts = append(opts, jsm.ReplayInstantly()) case "original": opts = append(opts, jsm.ReplayAsReceived()) case "": default: return nil, fmt.Errorf("invalid value for 'replayPolicy': '%s'. Must be one of 'instant', 'original'", spec.ReplayPolicy) } if spec.SampleFreq != "" { n, err := strconv.Atoi( strings.TrimSuffix(spec.SampleFreq, "%"), ) if err != nil { return nil, err } opts = append(opts, jsm.SamplePercent(n)) } if spec.HeadersOnly { opts = append(opts, jsm.DeliverHeadersOnly()) } // MaxRequestExpires if spec.MaxRequestExpires != "" { d, err := time.ParseDuration(spec.MaxRequestExpires) if err != nil { return nil, fmt.Errorf("invalid opt start time: %w", err) } opts = append(opts, jsm.MaxRequestExpires(d)) } // inactiveThreshold if spec.InactiveThreshold != "" { d, err := time.ParseDuration(spec.InactiveThreshold) if err != nil { return nil, fmt.Errorf("invalid inactive threshold: %w", err) } opts = append(opts, jsm.InactiveThreshold(d)) } // memStorage if spec.MemStorage { opts = append(opts, jsm.ConsumerOverrideMemoryStorage()) } if spec.PauseUntil != "" { t, err := time.Parse(time.RFC3339, spec.PauseUntil) if err != nil { return nil, fmt.Errorf("invalid pauseUntil time: %w", err) } opts = append(opts, jsm.PauseUntil(t)) } switch spec.PriorityPolicy { case "", "none": // Default is none, no need to set case "pinned_client": if spec.PinnedTTL != "" { dur, err := time.ParseDuration(spec.PinnedTTL) if err != nil { return nil, fmt.Errorf("invalid pinnedTTL duration: %w", err) } opts = append(opts, jsm.PinnedClientPriorityGroups(dur, spec.PriorityGroups...)) } case "overflow": opts = append(opts, jsm.OverflowPriorityGroups(spec.PriorityGroups...)) case "prioritized": opts = append(opts, jsm.PrioritizedPriorityGroups(spec.PriorityGroups...)) default: return nil, fmt.Errorf("invalid priority policy: %s", spec.PriorityPolicy) } return opts, nil } // SetupWithManager sets up the controller with the Manager. func (r *ConsumerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.Consumer{}). WithEventFilter(predicate.GenerationChangedPredicate{}). WithOptions(controller.Options{ MaxConcurrentReconciles: 1, }). Complete(r) } ================================================ FILE: internal/controller/consumer_controller_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "testing" "time" jsmapi "github.com/nats-io/jsm.go/api" "github.com/nats-io/nats.go/jetstream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) var _ = Describe("Consumer Controller", func() { Context("When reconciling a resource", func() { const resourceName = "test-consumer" const streamName = "orders" const consumerName = "test-consumer" const alternateResource = "alternate-consumer" const alternateNamespace = "alternate-namespace" typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", // TODO(user):Modify as needed } consumer := &api.Consumer{} emptyStreamConfig := jetstream.StreamConfig{ Name: streamName, Replicas: 1, Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardOld, Storage: jetstream.FileStorage, } emptyConsumerConfig := jetstream.ConsumerConfig{ Durable: consumerName, } // Tested controller var controller *ConsumerReconciler BeforeEach(func(ctx SpecContext) { By("creating the custom resource for the Kind Consumer") err := k8sClient.Get(ctx, typeNamespacedName, consumer) if err != nil && k8serrors.IsNotFound(err) { resource := &api.Consumer{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: api.ConsumerSpec{ AckPolicy: "explicit", DeliverPolicy: "all", DurableName: consumerName, Description: "test consumer", StreamName: streamName, ReplayPolicy: "instant", FilterSubject: "test.wildcard.>", }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) // Fetch consumer Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) } By("creating the underlying stream") _, err = jsClient.CreateStream(ctx, emptyStreamConfig) Expect(err).ToNot(HaveOccurred()) By("setting up the tested controller") controller = &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: baseController, } }) AfterEach(func(ctx SpecContext) { By("removing the consumer resource") resource := &api.Consumer{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err != nil { Expect(err).To(MatchError(k8serrors.IsNotFound, "Is not found")) } else { if controllerutil.ContainsFinalizer(resource, consumerFinalizer) { By("removing the finalizer") controllerutil.RemoveFinalizer(resource, consumerFinalizer) Expect(k8sClient.Update(ctx, resource)).To(Succeed()) } By("removing the consumer resource") Expect(k8sClient.Delete(ctx, resource)). To(SatisfyAny( Succeed(), MatchError(k8serrors.IsNotFound, "is not found"), )) } By("deleting the nats consumer") Expect(jsClient.DeleteConsumer(ctx, streamName, consumerName)). To(SatisfyAny( Succeed(), MatchError(jetstream.ErrStreamNotFound), MatchError(jetstream.ErrConsumerNotFound), )) By("deleting the consumers nats stream") Expect(jsClient.DeleteStream(ctx, streamName)). To(SatisfyAny( Succeed(), MatchError(jetstream.ErrStreamNotFound), )) }) When("reconciling a not existing resource", func() { It("should stop reconciliation without error", func(ctx SpecContext) { By("reconciling the created resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: "fake", Name: "not-existing", }, }) Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) }) }) When("reconciling a not initialized resource", func() { It("should initialize a new resource", func(ctx SpecContext) { By("re-queueing until it is initialized") // Initialization can require multiple reconciliation loops Eventually(func(ctx SpecContext) *api.Consumer { _, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) got := &api.Consumer{} Expect(k8sClient.Get(ctx, typeNamespacedName, got)).To(Succeed()) return got }).WithContext(ctx). Within(time.Second). Should(SatisfyAll( HaveField("Finalizers", HaveExactElements(consumerFinalizer)), HaveField("Status.Conditions", Not(BeEmpty())), )) By("validating the ready condition") // Fetch consumer Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) Expect(consumer.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(consumer.Status.Conditions[0], v1.ConditionUnknown, stateReconciling, "Starting reconciliation", time.Now()) }) }) When("reconciling a resource in a different namespace", func() { BeforeEach(func(ctx SpecContext) { By("creating a consumer resource in an alternate namespace while namespaced") alternateNamespaceResource := &api.Consumer{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, Spec: api.ConsumerSpec{ AckPolicy: "explicit", DeliverPolicy: "all", DurableName: alternateResource, Description: "consumer in alternate namespace", StreamName: streamName, ReplayPolicy: "instant", }, } ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: alternateNamespace, }, } err := k8sClient.Create(ctx, ns) if err != nil && !k8serrors.IsAlreadyExists(err) { Expect(err).NotTo(HaveOccurred()) } Expect(k8sClient.Create(ctx, alternateNamespaceResource)).To(Succeed()) }) AfterEach(func(ctx SpecContext) { By("cleaning up the resource in alternate namespace") alternateConsumer := &api.Consumer{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, } err := k8sClient.Delete(ctx, alternateConsumer) if err != nil && !k8serrors.IsNotFound(err) { Expect(err).NotTo(HaveOccurred()) } }) It("should not watch the resource in alternate namespace", func(ctx SpecContext) { By("reconciling with no explicit namespace restriction") alternateNamespacedName := types.NamespacedName{ Namespace: alternateNamespace, Name: alternateResource, } By("running reconciliation for the resource in alternate namespace") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: alternateNamespacedName, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) By("checking the consumer doesn't exist in NATS") _, err = jsClient.Consumer(ctx, streamName, alternateResource) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) By("verifying the resource still exists in the alternate namespace") alternateConsumer := &api.Consumer{} Expect(k8sClient.Get(ctx, alternateNamespacedName, alternateConsumer)).To(Succeed()) By("checking no conditions were set on the resource") Expect(alternateConsumer.Status.Conditions).To(BeEmpty()) }) It("should watch the resource in alternate namespace when not namespaced", func(ctx SpecContext) { By("reconciling with a non-namespaced controller") testNatsConfig := &NatsConfig{ServerURL: clientUrl} alternateBaseController, err := NewJSController(k8sClient, testNatsConfig, &Config{}) Expect(err).NotTo(HaveOccurred()) alternateController := &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: alternateBaseController, } resourceNames := []types.NamespacedName{ typeNamespacedName, { Namespace: alternateNamespace, Name: alternateResource, }, } By("running reconciliation for the resources in all namespaces") for _, n := range resourceNames { result, err := alternateController.Reconcile(ctx, reconcile.Request{ NamespacedName: n, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).NotTo(Equal(ctrl.Result{})) } }) }) When("reconciling an initialized resource", func() { BeforeEach(func(ctx SpecContext) { By("initializing the stream resource") By("setting the finalizer") Expect(controllerutil.AddFinalizer(consumer, consumerFinalizer)).To(BeTrue()) Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("setting an unknown ready state") consumer.Status.Conditions = []api.Condition{{ Type: readyCondType, Status: v1.ConditionUnknown, Reason: "Test", Message: "start condition", LastTransitionTime: time.Now().Format(time.RFC3339Nano), }} Expect(k8sClient.Status().Update(ctx, consumer)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) }) When("the underlying stream does not exist", func() { It("should set false ready state and error", func(ctx SpecContext) { By("setting a not existing stream on the resource") consumer.Spec.StreamName = "not-existing" Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).To(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking for expected ready state") Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) Expect(consumer.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches( consumer.Status.Conditions[0], v1.ConditionFalse, stateErrored, "stream", // Not existing stream as message time.Now(), ) }) }) It("should create a new consumer", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) By("checking if the ready state was updated") Expect(consumer.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(consumer.Status.Conditions[0], v1.ConditionTrue, stateReady, "created or updated", time.Now()) By("checking if the observed generation matches") Expect(consumer.Status.ObservedGeneration).To(Equal(consumer.Generation)) By("checking if the consumer was created") natsconsumer, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) consumerInfo, err := natsconsumer.Info(ctx) Expect(err).NotTo(HaveOccurred()) Expect(consumerInfo.Config.Name).To(Equal(consumerName)) Expect(consumerInfo.Config.Description).To(Equal("test consumer")) Expect(consumerInfo.Created).To(BeTemporally("~", time.Now(), time.Second)) }) It("should update an existing consumer", func(ctx SpecContext) { By("reconciling once to create the consumer") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) previousTransitionTime := consumer.Status.Conditions[0].LastTransitionTime By("updating the resource") consumer.Spec.Description = "new description" Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("reconciling the updated resource") result, err = controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) By("checking if the state transition time was not updated") Expect(consumer.Status.Conditions).To(HaveLen(1)) Expect(consumer.Status.Conditions[0].LastTransitionTime).To(Equal(previousTransitionTime)) By("checking if the observed generation matches") Expect(consumer.Status.ObservedGeneration).To(Equal(consumer.Generation)) By("checking if the consumer was updated") natsStream, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) streamInfo, err := natsStream.Info(ctx) Expect(err).NotTo(HaveOccurred()) Expect(streamInfo.Config.Description).To(Equal("new description")) // Other fields unchanged Expect(streamInfo.Config.ReplayPolicy).To(Equal(jetstream.ReplayInstantPolicy)) }) It("should set InactiveThreshold and priority fields on the server", func(ctx SpecContext) { By("updating the consumer spec with new fields") err := k8sClient.Get(ctx, typeNamespacedName, consumer) Expect(err).NotTo(HaveOccurred()) consumer.Spec.InactiveThreshold = "30s" consumer.Spec.PriorityPolicy = "pinned_client" consumer.Spec.PinnedTTL = "5m" consumer.Spec.PriorityGroups = []string{"high", "medium"} Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("reconciling the updated resource") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("fetching the updated consumer from NATS") natsConsumer, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) consumerInfo, err := natsConsumer.Info(ctx) Expect(err).NotTo(HaveOccurred()) By("verifying new fields are set on server") Expect(consumerInfo.Config.InactiveThreshold).To(Equal(30 * time.Second)) Expect(consumerInfo.Config.PriorityPolicy).To(Equal(jetstream.PriorityPolicyPinned)) Expect(consumerInfo.Config.PinnedTTL).To(Equal(5 * time.Minute)) Expect(consumerInfo.Config.PriorityGroups).To(Equal([]string{"high", "medium"})) }) When("PreventUpdate is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventUpdate on the resource") consumer.Spec.PreventUpdate = true Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) }) It("should create the consumer", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that consumer was created") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).ToNot(HaveOccurred()) }) It("should not update the consumer", func(ctx SpecContext) { By("creating the consumer") _, err := jsClient.CreateConsumer(ctx, streamName, emptyConsumerConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that consumer was not updated") c, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) Expect(c.CachedInfo().Config.Description).To(BeEmpty()) }) }) When("read-only mode is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should not create the consumer", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no consumer was created") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) }) It("should not update the consumer", func(ctx SpecContext) { By("creating the consumer") _, err := jsClient.CreateConsumer(ctx, streamName, emptyConsumerConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that consumer was not updated") s, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) Expect(s.CachedInfo().Config.Description).To(BeEmpty()) }) }) When("namespace restriction is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting a namespace on the resource") namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not create the consumer", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no consumer was created") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) }) It("should not update the consumer", func(ctx SpecContext) { By("creating the consumer") _, err := jsClient.CreateConsumer(ctx, streamName, emptyConsumerConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that consumer was not updated") s, err := jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) Expect(s.CachedInfo().Config.Description).To(BeEmpty()) }) }) When("the resource is marked for deletion", func() { BeforeEach(func(ctx SpecContext) { By("marking the resource for deletion") Expect(k8sClient.Delete(ctx, consumer)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) // re-fetch after update }) It("should succeed deleting a not existing consumer", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, consumer). ShouldNot(Succeed()) }) It("should succeed deleting a consumer of a deleted stream", func(ctx SpecContext) { By("Setting not existing stream") consumer.Spec.StreamName = "deleted-stream" Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, consumer). ShouldNot(Succeed()) }) When("the underlying consumer exists", func() { BeforeEach(func(ctx SpecContext) { By("creating the consumer on the nats server") _, err := jsClient.CreateConsumer(ctx, streamName, emptyConsumerConfig) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func(ctx SpecContext) { err := jsClient.DeleteConsumer(ctx, streamName, consumerName) if err != nil { Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) } }) It("should delete the consumer", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the consumer is deleted") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, consumer). ShouldNot(Succeed()) }) When("PreventDelete is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") consumer.Spec.PreventDelete = true Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) }) It("Should delete the resource and not delete the nats consumer", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the consumer is not deleted") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, consumer). ShouldNot(Succeed()) }) }) When("read only is set", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: testServer.ClientURL()}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should delete the resource and not delete the consumer", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the consumer is not deleted") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, consumer). ShouldNot(Succeed()) }) }) When("controller is restricted to different namespace", func() { BeforeEach(func(ctx SpecContext) { namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: testServer.ClientURL()}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &ConsumerReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not delete the resource and consumer", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the consumer is not deleted") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) By("checking that the finalizer is not removed") Expect(k8sClient.Get(ctx, typeNamespacedName, consumer)).To(Succeed()) Expect(consumer.Finalizers).To(ContainElement(consumerFinalizer)) }) }) }) }) It("should create consumer on different server as specified in spec", func(ctx SpecContext) { By("setting up the alternative server") altServer := CreateTestServer() defer altServer.Shutdown() connPool := newConnPool(0) conn, err := connPool.Get(&NatsConfig{ServerURL: altServer.ClientURL()}, true) Expect(err).NotTo(HaveOccurred()) domain := "" // Setup altClient for alternate server altClient, err := CreateJetStreamClient(conn, true, domain) defer conn.Close() Expect(err).NotTo(HaveOccurred()) By("setting up the stream on the alternative server") _, err = altClient.CreateStream(ctx, emptyStreamConfig) Expect(err).NotTo(HaveOccurred()) By("setting the server in the consumer spec") consumer.Spec.Servers = []string{altServer.ClientURL()} Expect(k8sClient.Update(ctx, consumer)).To(Succeed()) By("checking precondition, that the consumer does not yet exist") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) By("reconciling the resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking if the consumer was created on the alternative server") got, err := altClient.Consumer(ctx, streamName, consumerName) Expect(err).NotTo(HaveOccurred()) Expect(got.CachedInfo().Created).To(BeTemporally("~", time.Now(), time.Second)) By("checking that the consumer was NOT created on the original server") _, err = jsClient.Consumer(ctx, streamName, consumerName) Expect(err).To(MatchError(jetstream.ErrConsumerNotFound)) }) }) }) }) func Test_consumerSpecToConfig(t *testing.T) { date := time.Date(2024, 12, 3, 16, 55, 5, 0, time.UTC) dateString := date.Format(time.RFC3339) tests := []struct { name string spec *api.ConsumerSpec want *jsmapi.ConsumerConfig wantErr bool }{ { name: "empty spec", spec: &api.ConsumerSpec{}, want: &jsmapi.ConsumerConfig{}, wantErr: false, }, { name: "full spec", spec: &api.ConsumerSpec{ AckPolicy: "explicit", AckWait: "10ns", BackOff: []string{"1s", "5m"}, DeliverGroup: "", DeliverPolicy: "byStartSequence", DeliverSubject: "", Description: "test consumer", DurableName: "test-consumer", FilterSubject: "", FilterSubjects: []string{"time.us.east", "time.us.west"}, FlowControl: false, HeadersOnly: true, HeartbeatInterval: "", MaxAckPending: 6, MaxDeliver: 3, MaxRequestBatch: 7, MaxRequestExpires: "8s", MaxRequestMaxBytes: 1024, MaxWaiting: 5, MemStorage: true, OptStartSeq: 17, OptStartTime: "", RateLimitBps: 512, ReplayPolicy: "instant", Replicas: 9, SampleFreq: "25%", StreamName: "", InactiveThreshold: "30s", Metadata: map[string]string{ "meta": "data", }, BaseStreamConfig: api.BaseStreamConfig{ PreventDelete: false, PreventUpdate: false, ConnectionOpts: api.ConnectionOpts{ Account: "", Creds: "", Nkey: "", TLS: &api.TLS{}, Servers: nil, }, }, }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", Description: "test consumer", DeliverPolicy: jsmapi.DeliverByStartSequence, OptStartSeq: 17, AckPolicy: jsmapi.AckExplicit, AckWait: 10 * time.Nanosecond, MaxDeliver: 3, BackOff: []time.Duration{time.Second, 5 * time.Minute}, FilterSubject: "", ReplayPolicy: jsmapi.ReplayInstant, RateLimit: 512, SampleFrequency: "25%", MaxWaiting: 5, MaxAckPending: 6, HeadersOnly: true, MaxRequestBatch: 7, MaxRequestExpires: 8 * time.Second, MaxRequestMaxBytes: 1024, InactiveThreshold: 30 * time.Second, Replicas: 9, MemoryStorage: true, FilterSubjects: []string{"time.us.east", "time.us.west"}, Metadata: map[string]string{ "meta": "data", }, }, wantErr: false, }, { name: "full spec alt", spec: &api.ConsumerSpec{ AckPolicy: "all", AckWait: "20ns", BackOff: []string{"1s", "5m"}, DeliverGroup: "", DeliverPolicy: "byStartTime", DeliverSubject: "", Description: "test consumer", DurableName: "test-consumer", FilterSubject: "time.us.>", FlowControl: true, HeadersOnly: false, HeartbeatInterval: "", MaxAckPending: 5, MaxDeliver: 6, MaxRequestBatch: 7, MaxRequestExpires: "8s", MaxRequestMaxBytes: 1024, MaxWaiting: 5, MemStorage: false, OptStartSeq: 17, OptStartTime: dateString, RateLimitBps: 1024, ReplayPolicy: "original", Replicas: 9, SampleFreq: "30%", StreamName: "", InactiveThreshold: "1m", Metadata: map[string]string{ "meta": "data", }, BaseStreamConfig: api.BaseStreamConfig{ PreventDelete: false, PreventUpdate: false, ConnectionOpts: api.ConnectionOpts{ Account: "", Creds: "", Nkey: "", TLS: &api.TLS{}, Servers: nil, }, }, }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", Description: "test consumer", DeliverPolicy: jsmapi.DeliverByStartTime, OptStartSeq: 0, OptStartTime: &date, AckPolicy: jsmapi.AckAll, AckWait: 20 * time.Nanosecond, MaxDeliver: 6, BackOff: []time.Duration{time.Second, 5 * time.Minute}, FlowControl: true, FilterSubject: "time.us.>", ReplayPolicy: jsmapi.ReplayOriginal, RateLimit: 1024, SampleFrequency: "30%", MaxWaiting: 5, MaxAckPending: 5, HeadersOnly: false, MaxRequestBatch: 7, MaxRequestExpires: 8 * time.Second, MaxRequestMaxBytes: 1024, InactiveThreshold: time.Minute, Replicas: 9, MemoryStorage: false, Metadata: map[string]string{ "meta": "data", }, }, wantErr: false, }, { name: "deliver policy lastPerSubject", spec: &api.ConsumerSpec{ DurableName: "test-consumer", DeliverPolicy: "lastPerSubject", }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", DeliverPolicy: jsmapi.DeliverLastPerSubject, }, wantErr: false, }, { name: "inactive threshold valid duration", spec: &api.ConsumerSpec{ DurableName: "test-consumer", InactiveThreshold: "2h30m", }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", InactiveThreshold: 2*time.Hour + 30*time.Minute, }, wantErr: false, }, { name: "inactive threshold empty string", spec: &api.ConsumerSpec{ DurableName: "test-consumer", InactiveThreshold: "", }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", InactiveThreshold: 0, }, wantErr: false, }, { name: "inactive threshold invalid duration", spec: &api.ConsumerSpec{ DurableName: "test-consumer", InactiveThreshold: "not-a-duration", }, want: nil, wantErr: true, }, { name: "priority policy pinned_client with ttl", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "pinned_client", PinnedTTL: "10m", PriorityGroups: []string{"gold", "silver"}, }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", PriorityPolicy: jsmapi.PriorityPinnedClient, PinnedTTL: 10 * time.Minute, PriorityGroups: []string{"gold", "silver"}, }, wantErr: false, }, { name: "priority policy overflow", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "overflow", PriorityGroups: []string{"backup1", "backup2"}, }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", PriorityPolicy: jsmapi.PriorityOverflow, PriorityGroups: []string{"backup1", "backup2"}, }, wantErr: false, }, { name: "priority policy prioritized", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "prioritized", PriorityGroups: []string{"level1", "level2", "level3"}, }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", PriorityPolicy: jsmapi.PriorityPrioritized, PriorityGroups: []string{"level1", "level2", "level3"}, }, wantErr: false, }, { name: "priority policy none", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "none", }, want: &jsmapi.ConsumerConfig{ Durable: "test-consumer", }, wantErr: false, }, { name: "priority policy invalid", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "invalid_policy", }, want: nil, wantErr: true, }, { name: "priority policy pinned_client invalid ttl", spec: &api.ConsumerSpec{ DurableName: "test-consumer", PriorityPolicy: "pinned_client", PinnedTTL: "not-a-duration", PriorityGroups: []string{"gold"}, }, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cOpts, err := consumerSpecToConfig(tt.spec) if (err != nil) != tt.wantErr { t.Errorf("consumerSpecToConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr { got := &jsmapi.ConsumerConfig{} for _, o := range cOpts { o(got) } assert.EqualValues(t, tt.want, got, "consumerSpecToConfig(%v)", tt.spec) } }) } } ================================================ FILE: internal/controller/helpers_test.go ================================================ package controller import ( "os" "time" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/nats-io/nats-server/v2/server" natsserver "github.com/nats-io/nats-server/v2/test" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" ) func assertReadyStateMatches(condition api.Condition, status v1.ConditionStatus, reason string, message string, transitionTime time.Time) { GinkgoHelper() Expect(condition.Type).To(Equal(readyCondType)) Expect(condition.Status).To(Equal(status)) Expect(condition.Reason).To(Equal(reason)) Expect(condition.Message).To(ContainSubstring(message)) // Assert valid transition time t, err := time.Parse(time.RFC3339Nano, condition.LastTransitionTime) Expect(err).NotTo(HaveOccurred()) Expect(t).To(BeTemporally("~", transitionTime, time.Second)) } func CreateTestServer() *server.Server { opts := &natsserver.DefaultTestOptions opts.JetStream = true opts.Port = -1 opts.Debug = true dir, err := os.MkdirTemp("", "nats-*") Expect(err).NotTo(HaveOccurred()) opts.StoreDir = dir ns := natsserver.RunServer(opts) Expect(err).NotTo(HaveOccurred()) return ns } ================================================ FILE: internal/controller/jetstream_controller.go ================================================ package controller import ( "bytes" "context" "errors" "fmt" "math/rand/v2" "os" "path/filepath" "regexp" "strconv" "strings" "sync" "time" "github.com/google/go-cmp/cmp" "github.com/nats-io/jsm.go" js "github.com/nats-io/nack/controllers/jetstream" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/nats-io/nats.go/jetstream" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) const ( JSConsumerNotFoundErr uint16 = 10014 JSStreamNotFoundErr uint16 = 10059 ) var semVerRe = regexp.MustCompile(`\Av?([0-9]+)\.?([0-9]+)?\.?([0-9]+)?`) type JetStreamController interface { client.Client // ReadOnly returns true when no changes should be made by the controller. ReadOnly() bool // ValidNamespace ok if the controllers namespace restriction allows the given namespace. ValidNamespace(namespace string) bool // WithJetStreamClient provides a jetStream client to the given operation. // The client uses the controllers connection configuration merged with opts. // // The given opts values take precedence over the controllers base configuration. // // Returns the error of the operation or errors during client setup. WithJetStreamClient(opts api.ConnectionOpts, ns string, op func(js jetstream.JetStream) error) error // WithJSMClient provides a jsm.go client to the given operation. WithJSMClient(opts api.ConnectionOpts, ns string, op func(jsm *jsm.Manager) error) error RequeueInterval() time.Duration } func NewJSController(k8sClient client.Client, natsConfig *NatsConfig, controllerConfig *Config) (JetStreamController, error) { return &jsController{ Client: k8sClient, config: natsConfig, controllerConfig: controllerConfig, cacheDir: controllerConfig.CacheDir, connPool: newConnPool(time.Second * 15), }, nil } type jsController struct { client.Client config *NatsConfig controllerConfig *Config cacheDir string cacheLock sync.Mutex connPool *connectionPool } func (c *jsController) RequeueInterval() time.Duration { // Stagger the requeue slightly interval := c.controllerConfig.RequeueInterval // Allow up to a 10% variance intervalRange := float64(interval.Nanoseconds()) * 0.1 randomFactor := (rand.Float64() * 2) - 1.0 return time.Duration(float64(interval.Nanoseconds()) + (intervalRange * randomFactor)) } func (c *jsController) ReadOnly() bool { return c.controllerConfig.ReadOnly } func (c *jsController) ValidNamespace(namespace string) bool { ns := c.controllerConfig.Namespace return ns == "" || ns == namespace } func (c *jsController) WithJSMClient(opts api.ConnectionOpts, ns string, op func(js *jsm.Manager) error) error { cfg, err := c.natsConfigFromOpts(opts, ns) if err != nil { return err } conn, err := c.connPool.Get(cfg, true) if err != nil { return err } jsmClient, err := CreateJSMClient(conn, true, cfg.JsDomain) if err != nil { return fmt.Errorf("create jsm client: %w", err) } defer conn.Close() return op(jsmClient) } func (c *jsController) WithJetStreamClient(opts api.ConnectionOpts, ns string, op func(js jetstream.JetStream) error) error { cfg, err := c.natsConfigFromOpts(opts, ns) if err != nil { return err } conn, err := c.connPool.Get(cfg, true) if err != nil { return err } jsClient, err := CreateJetStreamClient(conn, true, cfg.JsDomain) if err != nil { return fmt.Errorf("create jetstream client: %w", err) } defer conn.Close() return op(jsClient) } // Setup default options, override from account resource and CRD options if configured func (c *jsController) natsConfigFromOpts(opts api.ConnectionOpts, ns string) (*NatsConfig, error) { ctx, done := context.WithTimeout(context.Background(), 5*time.Second) defer done() natsConfig := &NatsConfig{} natsConfig.Overlay(c.config) if opts.Account == "" { natsConfig.Overlay(natsConfigFromOpts(opts)) return natsConfig, nil } // Apply Account options first, over global. // Apply remaining CRD options last accountOverlay := &NatsConfig{} account := &api.Account{} err := c.Get(ctx, types.NamespacedName{ Name: opts.Account, Namespace: ns, }, account, ) if err != nil { return nil, err } if len(account.Spec.Servers) > 0 { accountOverlay.ServerURL = strings.Join(account.Spec.Servers, ",") } c.cacheLock.Lock() defer c.cacheLock.Unlock() if account.Spec.TLS != nil && account.Spec.TLS.Secret != nil { tlsSecret := &v1.Secret{} err := c.Get(ctx, types.NamespacedName{ Name: account.Spec.TLS.Secret.Name, Namespace: ns, }, tlsSecret, ) if err != nil { return nil, err } accDir := filepath.Join(c.cacheDir, ns, opts.Account) if err := os.MkdirAll(accDir, 0o755); err != nil { return nil, err } var certData, keyData []byte var certPath, keyPath string for k, v := range tlsSecret.Data { switch k { case account.Spec.TLS.ClientCert: certPath = filepath.Join(accDir, k) certData = v case account.Spec.TLS.ClientKey: keyPath = filepath.Join(accDir, k) keyData = v case account.Spec.TLS.RootCAs: rootCAPath := filepath.Join(accDir, k) accountOverlay.CAs = append(accountOverlay.CAs, rootCAPath) if _, err := os.Stat(rootCAPath); err == nil { caBytes, err := os.ReadFile(rootCAPath) // Skip file write if data is unchanged if err == nil && bytes.Equal(caBytes, v) { continue } } if err := os.WriteFile(rootCAPath, v, 0o644); err != nil { return nil, err } } } if certData != nil && keyData != nil { accountOverlay.Certificate = certPath accountOverlay.Key = keyPath writeCert := true if _, err := os.Stat(certPath); err == nil { fileBytes, err := os.ReadFile(certPath) // Skip disk write if data is unchanged if err == nil && bytes.Equal(fileBytes, certData) { writeCert = false } } if writeCert { if err := os.WriteFile(certPath, certData, 0o644); err != nil { return nil, err } } writeKey := true if _, err := os.Stat(keyPath); err == nil { fileBytes, err := os.ReadFile(keyPath) // Skip disk write if data is unchanged if err == nil && bytes.Equal(fileBytes, keyData) { writeKey = false } } if writeKey { if err := os.WriteFile(keyPath, keyData, 0o600); err != nil { return nil, err } } } } else if account.Spec.TLS != nil { if account.Spec.TLS.ClientCert != "" && account.Spec.TLS.ClientKey != "" { accountOverlay.Certificate = account.Spec.TLS.ClientCert accountOverlay.Key = account.Spec.TLS.ClientKey } accountOverlay.CAs = []string{account.Spec.TLS.RootCAs} } if account.Spec.Creds != nil && account.Spec.Creds.Secret != nil { credsSecret := &v1.Secret{} err := c.Get(ctx, types.NamespacedName{ Name: account.Spec.Creds.Secret.Name, Namespace: ns, }, credsSecret, ) if err != nil { return nil, err } accDir := filepath.Join(c.cacheDir, ns, opts.Account) if err := os.MkdirAll(accDir, 0o755); err != nil { return nil, err } if credsBytes, ok := credsSecret.Data[account.Spec.Creds.File]; ok { filePath := filepath.Join(accDir, account.Spec.Creds.File) accountOverlay.Credentials = filePath writeCreds := true if _, err := os.Stat(filePath); err == nil { fileBytes, err := os.ReadFile(filePath) // Skip disk write if data is unchanged if err == nil && bytes.Equal(fileBytes, credsBytes) { writeCreds = false } } if writeCreds { if err := os.WriteFile(filePath, credsBytes, 0o600); err != nil { return nil, err } } } } else if account.Spec.Creds != nil { accountOverlay.Credentials = account.Spec.Creds.File } if account.Spec.NKey != nil && account.Spec.NKey.Secret != nil { nkeySecret := &v1.Secret{} err := c.Get(ctx, types.NamespacedName{ Name: account.Spec.NKey.Secret.Name, Namespace: ns, }, nkeySecret, ) if err != nil { return nil, err } accDir := filepath.Join(c.cacheDir, ns, opts.Account) if err := os.MkdirAll(accDir, 0o755); err != nil { return nil, err } if nkeyBytes, ok := nkeySecret.Data[account.Spec.NKey.Seed]; ok { filePath := filepath.Join(accDir, account.Spec.NKey.Seed) accountOverlay.NKey = filePath writeNKey := true if _, err := os.Stat(filePath); err == nil { fileBytes, err := os.ReadFile(filePath) if err == nil && bytes.Equal(fileBytes, nkeyBytes) { writeNKey = false } } if writeNKey { if err := os.WriteFile(filePath, nkeyBytes, 0o600); err != nil { return nil, err } } } } if account.Spec.User != nil { userSecret := &v1.Secret{} err := c.Get(ctx, types.NamespacedName{ Name: account.Spec.User.Secret.Name, Namespace: ns, }, userSecret, ) if err != nil { return nil, err } userName := userSecret.Data[account.Spec.User.User] userPassword := userSecret.Data[account.Spec.User.Password] if userName != nil && userPassword != nil { accountOverlay.User = string(userName) accountOverlay.Password = string(userPassword) } } if account.Spec.Token != nil { tokenSecret := &v1.Secret{} err := c.Get(ctx, types.NamespacedName{ Name: account.Spec.Token.Secret.Name, Namespace: ns, }, tokenSecret, ) if err != nil { return nil, err } if token := tokenSecret.Data[account.Spec.Token.Token]; token != nil { accountOverlay.Token = string(token) } } // Overlay Account Config natsConfig.Overlay(accountOverlay) // Overlay Spec Config natsConfig.Overlay(natsConfigFromOpts(opts)) return natsConfig, nil } func natsConfigFromOpts(opts api.ConnectionOpts) *NatsConfig { natsConfig := &NatsConfig{} if len(opts.Servers) > 0 { natsConfig.ServerURL = strings.Join(opts.Servers, ",") } // Currently, if the global TLSFirst is set, a false value in the CRD will not override // due to that being the bool zero value. A true value in the CRD can override a global false. if opts.TLSFirst { natsConfig.TLSFirst = opts.TLSFirst } if opts.Creds != "" { natsConfig.Credentials = opts.Creds } if opts.Nkey != "" { natsConfig.NKey = opts.Nkey } if opts.TLS != nil { if len(opts.TLS.RootCAs) > 0 { natsConfig.CAs = opts.TLS.RootCAs } if opts.TLS.ClientCert != "" && opts.TLS.ClientKey != "" { natsConfig.Certificate = opts.TLS.ClientCert natsConfig.Key = opts.TLS.ClientKey } } if opts.JsDomain != "" { natsConfig.JsDomain = opts.JsDomain } return natsConfig } // updateReadyCondition returns the given conditions with an added or updated ready condition. func updateReadyCondition(conditions []api.Condition, status v1.ConditionStatus, reason string, message string) []api.Condition { var currentStatus v1.ConditionStatus var lastTransitionTime string for _, condition := range conditions { if condition.Type == readyCondType { currentStatus = condition.Status lastTransitionTime = condition.LastTransitionTime break } } // Set transition time to now, when no previous ready condition or the status changed if lastTransitionTime == "" || currentStatus != status { lastTransitionTime = time.Now().UTC().Format(time.RFC3339Nano) } newCondition := api.Condition{ Type: readyCondType, Status: status, Reason: reason, Message: message, LastTransitionTime: lastTransitionTime, } if conditions == nil { return []api.Condition{newCondition} } else { return js.UpsertCondition(conditions, newCondition) } } // jsonString returns the given string wrapped in " and converted to []byte. // Helper for mapping spec config to jetStream config using UnmarshalJSON. func jsonString(v string) []byte { return []byte("\"" + v + "\"") } func compareConfigState(actual any, desired any) string { return cmp.Diff(desired, actual) } func versionComponents(version string) (major, minor, patch int, err error) { m := semVerRe.FindStringSubmatch(version) if m == nil { return 0, 0, 0, errors.New("invalid semver") } major, err = strconv.Atoi(m[1]) if err != nil { return -1, -1, -1, err } minor, err = strconv.Atoi(m[2]) if err != nil { return -1, -1, -1, err } patch, err = strconv.Atoi(m[3]) if err != nil { return -1, -1, -1, err } return major, minor, patch, err } ================================================ FILE: internal/controller/jetstream_controller_test.go ================================================ package controller import ( "testing" "time" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" ) func Test_updateReadyCondition(t *testing.T) { pastTransition := time.Now().UTC().Add(-time.Hour).Format(time.RFC3339Nano) updatedTransition := "now" otherCondition := api.Condition{ Type: "other", Status: v1.ConditionFalse, Reason: "Reason", Message: "Message", LastTransitionTime: pastTransition, } type args struct { conditions []api.Condition status v1.ConditionStatus reason string message string } tests := []struct { name string args args want []api.Condition }{ { name: "new ready condition", args: args{ conditions: nil, status: v1.ConditionTrue, reason: "Test", message: "Test Message", }, want: []api.Condition{ { Type: readyCondType, Status: v1.ConditionTrue, Reason: "Test", Message: "Test Message", LastTransitionTime: updatedTransition, }, }, }, { name: "update ready condition", args: args{ conditions: []api.Condition{ otherCondition, { Type: readyCondType, Status: v1.ConditionFalse, Reason: "Test", Message: "Test Message", LastTransitionTime: pastTransition, }, }, status: v1.ConditionTrue, reason: "New Reason", message: "New Message", }, want: []api.Condition{ otherCondition, { Type: readyCondType, Status: v1.ConditionTrue, Reason: "New Reason", Message: "New Message", LastTransitionTime: updatedTransition, }, }, }, { name: "should not update transition time when status is not changed", args: args{ conditions: []api.Condition{ otherCondition, { Type: readyCondType, Status: v1.ConditionTrue, Reason: "Test", Message: "Test Message", LastTransitionTime: pastTransition, }, }, status: v1.ConditionTrue, reason: "New Reason", message: "New Message", }, want: []api.Condition{ otherCondition, { Type: readyCondType, Status: v1.ConditionTrue, Reason: "New Reason", Message: "New Message", LastTransitionTime: pastTransition, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) got := updateReadyCondition(tt.args.conditions, tt.args.status, tt.args.reason, tt.args.message) assert.Len(got, len(tt.want)) for i, want := range tt.want { actual := got[i] assert.Equal(actual.Type, want.Type) assert.Equal(actual.Status, want.Status) assert.Equal(actual.Reason, want.Reason) assert.Equal(actual.Message, want.Message) // Assert transition time was updated if want.LastTransitionTime == updatedTransition { actualTransitionTime, err := time.Parse(time.RFC3339Nano, actual.LastTransitionTime) assert.NoError(err) assert.WithinDuration(actualTransitionTime, time.Now(), 5*time.Second) } // Assert transition time was not updated if want.LastTransitionTime == pastTransition { assert.Equal(pastTransition, actual.LastTransitionTime) } } }) } } ================================================ FILE: internal/controller/keyvalue_controller.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/go-logr/logr" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/nats-io/nats.go/jetstream" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/predicate" ) const ( kvStreamPrefix = "KV_" ) // KeyValueReconciler reconciles a KeyValue object type KeyValueReconciler struct { Scheme *runtime.Scheme JetStreamController } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // It performs three main operations: // - Initialize finalizer and ready condition if not present // - Delete KeyValue if it is marked for deletion. // - Create or Update the KeyValue // // A call to reconcile may perform only one action, expecting the reconciliation to be triggered again by an update. // For example: Setting the finalizer triggers a second reconciliation. Reconcile returns after setting the finalizer, // to prevent parallel reconciliations performing the same steps. func (r *KeyValueReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := klog.FromContext(ctx) if ok := r.ValidNamespace(req.Namespace); !ok { log.Info("Controller restricted to namespace, skipping reconciliation.") return ctrl.Result{}, nil } // Fetch KeyValue resource keyValue := &api.KeyValue{} if err := r.Get(ctx, req.NamespacedName, keyValue); err != nil { if apierrors.IsNotFound(err) { log.Info("KeyValue resource deleted.", "keyValueName", req.NamespacedName.String()) return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get keyvalue resource '%s': %w", req.NamespacedName.String(), err) } log = log.WithValues("keyValueName", keyValue.Spec.Bucket) // Update ready status to unknown when no status is set if len(keyValue.Status.Conditions) == 0 { log.Info("Setting initial ready condition to unknown.") keyValue.Status.Conditions = updateReadyCondition(keyValue.Status.Conditions, v1.ConditionUnknown, stateReconciling, "Starting reconciliation") err := r.Status().Update(ctx, keyValue) if err != nil { // If we get a conflict error, another reconciliation has already updated the status. // Just requeue and let the next reconciliation handle it. if apierrors.IsConflict(err) { return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("set condition unknown: %w", err) } return ctrl.Result{Requeue: true}, nil } // Check Deletion markedForDeletion := keyValue.GetDeletionTimestamp() != nil if markedForDeletion { if controllerutil.ContainsFinalizer(keyValue, keyValueFinalizer) { err := r.deleteKeyValue(ctx, log, keyValue) if err != nil { return ctrl.Result{}, fmt.Errorf("delete keyvalue: %w", err) } } else { log.Info("KeyValue marked for deletion and already finalized. Ignoring.") } return ctrl.Result{}, nil } // Add finalizer if !controllerutil.ContainsFinalizer(keyValue, keyValueFinalizer) { log.Info("Adding KeyValue finalizer.") if ok := controllerutil.AddFinalizer(keyValue, keyValueFinalizer); !ok { return ctrl.Result{}, errors.New("failed to add finalizer to keyvalue resource") } if err := r.Update(ctx, keyValue); err != nil { return ctrl.Result{}, fmt.Errorf("update keyvalue resource to add finalizer: %w", err) } // After we have added the finalizer, we need to requeue to make sure we reconcile the // rest of the object. Just updating metadata won't make the API server update generation // so the update above shouldn't trigger a new reconciliation. return ctrl.Result{Requeue: true}, nil } // Create or update KeyValue if err := r.createOrUpdate(ctx, log, keyValue); err != nil { return ctrl.Result{}, fmt.Errorf("create or update: %s", err) } return ctrl.Result{RequeueAfter: r.RequeueInterval()}, nil } func (r *KeyValueReconciler) deleteKeyValue(ctx context.Context, log logr.Logger, keyValue *api.KeyValue) error { // Set status to false keyValue.Status.Conditions = updateReadyCondition(keyValue.Status.Conditions, v1.ConditionFalse, stateFinalizing, "Performing finalizer operations.") if err := r.Status().Update(ctx, keyValue); err != nil { return fmt.Errorf("update ready condition: %w", err) } storedState, err := getStoredKeyValueState(keyValue) if err != nil { log.Error(err, "Failed to fetch stored state.") } if !keyValue.Spec.PreventDelete && !r.ReadOnly() { log.Info("Deleting KeyValue.") err := r.WithJetStreamClient(keyValue.Spec.ConnectionOpts, keyValue.Namespace, func(js jetstream.JetStream) error { _, err := getServerKeyValueState(ctx, js, keyValue) // If we have no known state for this KeyValue it has never been reconciled. // If we are also receiving an error fetching state, either the KeyValue does not exist // or this resource config is invalid. if err != nil && storedState == nil { return nil } return js.DeleteKeyValue(ctx, keyValue.Spec.Bucket) }) if errors.Is(err, jetstream.ErrBucketNotFound) { log.Info("KeyValue does not exist, unable to delete.", "keyValueName", keyValue.Spec.Bucket) } else if err != nil && storedState == nil { log.Info("KeyValue not reconciled and no state received from server. Removing finalizer.") } else if err != nil { return fmt.Errorf("delete keyvalue during finalization: %w", err) } } else { log.Info("Skipping KeyValue deletion.", "preventDelete", keyValue.Spec.PreventDelete, "read-only", r.ReadOnly(), ) } log.Info("Removing KeyValue finalizer.") if ok := controllerutil.RemoveFinalizer(keyValue, keyValueFinalizer); !ok { return errors.New("failed to remove keyvalue finalizer") } if err := r.Update(ctx, keyValue); err != nil { return fmt.Errorf("remove finalizer: %w", err) } return nil } func (r *KeyValueReconciler) createOrUpdate(ctx context.Context, log logr.Logger, keyValue *api.KeyValue) error { // Create or Update the KeyValue based on the spec // Map spec to KeyValue targetConfig targetConfig, err := keyValueSpecToConfig(&keyValue.Spec) if err != nil { return fmt.Errorf("map spec to keyvalue targetConfig: %w", err) } // UpdateKeyValue is called on every reconciliation when the stream is not to be deleted. err = r.WithJetStreamClient(keyValue.Spec.ConnectionOpts, keyValue.Namespace, func(js jetstream.JetStream) error { storedState, err := getStoredKeyValueState(keyValue) if err != nil { log.Error(err, "Failed to fetch stored KeyValue state") } serverState, err := getServerKeyValueState(ctx, js, keyValue) if err != nil { return err } // Check against known state. Skip Update if converged. // Storing returned state from the server avoids have to // check default values or call Update on already converged resources if storedState != nil && serverState != nil && keyValue.Status.ObservedGeneration == keyValue.Generation { diff := compareConfigState(storedState, serverState) if diff == "" { return nil } log.Info("KeyValue config drifted from desired state.", "diff", diff) } if r.ReadOnly() { log.Info("Skipping KeyValue creation or update.", "read-only", r.ReadOnly(), ) return nil } var updatedKeyValue jetstream.KeyValue err = nil if serverState == nil { log.Info("Creating KeyValue.") updatedKeyValue, err = js.CreateKeyValue(ctx, targetConfig) if err != nil { return err } } else if !keyValue.Spec.PreventUpdate { log.Info("Updating KeyValue.") updatedKeyValue, err = js.UpdateKeyValue(ctx, targetConfig) if err != nil { return err } updatedKeyValue, err := getServerKeyValueState(ctx, js, keyValue) if err != nil { log.Error(err, "Failed to fetch updated KeyValue state") } else { diff := compareConfigState(updatedKeyValue, serverState) log.Info("Updated KeyValue.", "diff", diff) } } else { log.Info("Skipping KeyValue update.", "preventUpdate", keyValue.Spec.PreventUpdate, ) } if updatedKeyValue != nil { // Store known state in annotation serverState, err = getServerKeyValueState(ctx, js, keyValue) if err != nil { return err } updatedState, err := json.Marshal(serverState) if err != nil { return err } if keyValue.Annotations == nil { keyValue.Annotations = map[string]string{} } keyValue.Annotations[stateAnnotationKV] = string(updatedState) return r.Update(ctx, keyValue) } return nil }) if err != nil { err = fmt.Errorf("create or update keyvalue: %w", err) keyValue.Status.Conditions = updateReadyCondition(keyValue.Status.Conditions, v1.ConditionFalse, stateErrored, err.Error()) if err := r.Status().Update(ctx, keyValue); err != nil { log.Error(err, "Failed to update ready condition to Errored.") } return err } // update the observed generation and ready status keyValue.Status.ObservedGeneration = keyValue.Generation keyValue.Status.Conditions = updateReadyCondition( keyValue.Status.Conditions, v1.ConditionTrue, stateReady, "KeyValue successfully created or updated.", ) err = r.Status().Update(ctx, keyValue) if err != nil { return fmt.Errorf("update ready condition: %w", err) } return nil } func getStoredKeyValueState(keyValue *api.KeyValue) (*jetstream.StreamConfig, error) { var storedState *jetstream.StreamConfig if state, ok := keyValue.Annotations[stateAnnotationKV]; ok { err := json.Unmarshal([]byte(state), &storedState) if err != nil { return nil, err } } return storedState, nil } // Fetch the current state of the KeyValue stream from the server. // ErrStreamNotFound is considered a valid response and does not return error func getServerKeyValueState(ctx context.Context, js jetstream.JetStream, keyValue *api.KeyValue) (*jetstream.StreamConfig, error) { s, err := js.Stream(ctx, fmt.Sprintf("%s%s", kvStreamPrefix, keyValue.Spec.Bucket)) if errors.Is(err, jetstream.ErrStreamNotFound) { return nil, nil } if err != nil { return nil, err } return &s.CachedInfo().Config, nil } // keyValueSpecToConfig creates a jetstream.KeyValueConfig matching the given KeyValue resource spec func keyValueSpecToConfig(spec *api.KeyValueSpec) (jetstream.KeyValueConfig, error) { // Set directly mapped fields config := jetstream.KeyValueConfig{ Bucket: spec.Bucket, Compression: spec.Compression, Description: spec.Description, History: uint8(spec.History), MaxBytes: int64(spec.MaxBytes), MaxValueSize: int32(spec.MaxValueSize), Replicas: spec.Replicas, LimitMarkerTTL: spec.LimitMarkerTTL, } // TTL if spec.TTL != "" { t, err := time.ParseDuration(spec.TTL) if err != nil { return jetstream.KeyValueConfig{}, fmt.Errorf("invalid ttl: %w", err) } config.TTL = t } // storage if spec.Storage != "" { err := config.Storage.UnmarshalJSON(jsonString(spec.Storage)) if err != nil { return jetstream.KeyValueConfig{}, fmt.Errorf("invalid storage: %w", err) } } // placement if spec.Placement != nil { config.Placement = &jetstream.Placement{ Cluster: spec.Placement.Cluster, Tags: spec.Placement.Tags, } } // mirror if spec.Mirror != nil { ss, err := mapStreamSource(spec.Mirror) if err != nil { return jetstream.KeyValueConfig{}, fmt.Errorf("map mirror keyvalue source: %w", err) } config.Mirror = ss } // sources if spec.Sources != nil { config.Sources = []*jetstream.StreamSource{} for _, source := range spec.Sources { s, err := mapStreamSource(source) if err != nil { return jetstream.KeyValueConfig{}, fmt.Errorf("map keyvalue source: %w", err) } config.Sources = append(config.Sources, s) } } // RePublish if spec.RePublish != nil { config.RePublish = &jetstream.RePublish{ Source: spec.RePublish.Source, Destination: spec.RePublish.Destination, HeadersOnly: spec.RePublish.HeadersOnly, } } return config, nil } // SetupWithManager sets up the controller with the Manager. func (r *KeyValueReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.KeyValue{}). WithEventFilter(predicate.GenerationChangedPredicate{}). WithOptions(controller.Options{ MaxConcurrentReconciles: 1, }). Complete(r) } ================================================ FILE: internal/controller/keyvalue_controller_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "testing" "time" "github.com/nats-io/nats.go/jetstream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) var _ = Describe("KeyValue Controller", func() { // The test keyValue resource const resourceName = "test-kv" const keyValueName = "orders" const alternateResource = "alternate-kv" const alternateNamespace = "alternate-namespace" typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", } keyValue := &api.KeyValue{} // The tested controller var controller *KeyValueReconciler // Config to create minimal nats KeyValue store emptyKeyValueConfig := jetstream.KeyValueConfig{ Bucket: keyValueName, Replicas: 1, Storage: jetstream.FileStorage, } BeforeEach(func(ctx SpecContext) { By("creating a test keyvalue resource") err := k8sClient.Get(ctx, typeNamespacedName, keyValue) if err != nil && k8serrors.IsNotFound(err) { resource := &api.KeyValue{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: api.KeyValueSpec{ Bucket: keyValueName, Replicas: 1, History: 10, TTL: "5m", Compression: true, Description: "test keyvalue", Storage: "file", }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) // Re-fetch KeyValue Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) } By("checking precondition: nats keyvalue does not exist") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("setting up the tested controller") controller = &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: baseController, } }) AfterEach(func(ctx SpecContext) { By("removing the test keyvalue resource") resource := &api.KeyValue{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err != nil { Expect(err).To(MatchError(k8serrors.IsNotFound, "Is not found")) } else { if controllerutil.ContainsFinalizer(resource, keyValueFinalizer) { By("removing the finalizer") controllerutil.RemoveFinalizer(resource, keyValueFinalizer) Expect(k8sClient.Update(ctx, resource)).To(Succeed()) } By("removing the keyvalue resource") Expect(k8sClient.Delete(ctx, resource)). To(SatisfyAny( Succeed(), MatchError(k8serrors.IsNotFound, "is not found"), )) } By("deleting the nats keyvalue store") Expect(jsClient.DeleteKeyValue(ctx, keyValueName)). To(SatisfyAny( Succeed(), MatchError(jetstream.ErrBucketNotFound), )) }) When("reconciling a not existing resource", func() { It("should stop reconciliation without error", func(ctx SpecContext) { By("reconciling the created resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: "fake", Name: "not-existing", }, }) Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) }) }) When("reconciling a not initialized resource", func() { It("should initialize a new resource", func(ctx SpecContext) { By("re-queueing until it is initialized") // Initialization can require multiple reconciliation loops Eventually(func(ctx SpecContext) *api.KeyValue { _, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) got := &api.KeyValue{} Expect(k8sClient.Get(ctx, typeNamespacedName, got)).To(Succeed()) return got }).WithContext(ctx). Should(SatisfyAll( HaveField("Finalizers", HaveExactElements(keyValueFinalizer)), HaveField("Status.Conditions", Not(BeEmpty())), )) By("validating the ready condition") // Fetch KeyValue Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) Expect(keyValue.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(keyValue.Status.Conditions[0], v1.ConditionUnknown, stateReconciling, "Starting reconciliation", time.Now()) }) }) When("reconciling a resource in a different namespace", func() { BeforeEach(func(ctx SpecContext) { By("creating a keyvalue resource in an alternate namespace while namespaced") alternateNamespaceResource := &api.KeyValue{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, Spec: api.KeyValueSpec{ Bucket: alternateResource, Replicas: 1, History: 10, TTL: "5m", Compression: true, Description: "keyvalue in alternate namespace", Storage: "file", }, } ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: alternateNamespace, }, } err := k8sClient.Create(ctx, ns) if err != nil && !k8serrors.IsAlreadyExists(err) { Expect(err).NotTo(HaveOccurred()) } Expect(k8sClient.Create(ctx, alternateNamespaceResource)).To(Succeed()) }) AfterEach(func(ctx SpecContext) { By("cleaning up the resource in alternate namespace") alternateKeyValue := &api.KeyValue{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, } err := k8sClient.Delete(ctx, alternateKeyValue) if err != nil && !k8serrors.IsNotFound(err) { Expect(err).NotTo(HaveOccurred()) } }) It("should not watch the resource in alternate namespace", func(ctx SpecContext) { By("reconciling with no explicit namespace restriction") alternateNamespacedName := types.NamespacedName{ Namespace: alternateNamespace, Name: alternateResource, } By("running reconciliation for the resource in alternate namespace") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: alternateNamespacedName, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) By("checking the keyvalue doesn't exist in NATS") _, err = jsClient.KeyValue(ctx, alternateResource) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("verifying the resource still exists in the alternate namespace") alternateKeyValue := &api.KeyValue{} Expect(k8sClient.Get(ctx, alternateNamespacedName, alternateKeyValue)).To(Succeed()) By("checking no conditions were set on the resource") Expect(alternateKeyValue.Status.Conditions).To(BeEmpty()) }) It("should watch the resource in alternate namespace when not namespaced", func(ctx SpecContext) { By("reconciling with a non-namespaced controller") testNatsConfig := &NatsConfig{ServerURL: clientUrl} alternateBaseController, err := NewJSController(k8sClient, testNatsConfig, &Config{}) Expect(err).NotTo(HaveOccurred()) alternateController := &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: alternateBaseController, } resourceNames := []types.NamespacedName{ typeNamespacedName, { Namespace: alternateNamespace, Name: alternateResource, }, } By("running reconciliation for the resources in all namespaces") for _, n := range resourceNames { result, err := alternateController.Reconcile(ctx, reconcile.Request{ NamespacedName: n, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).NotTo(Equal(ctrl.Result{})) } }) }) When("reconciling an initialized resource", func() { BeforeEach(func(ctx SpecContext) { By("initializing the keyvalue resource") By("setting the finalizer") Expect(controllerutil.AddFinalizer(keyValue, keyValueFinalizer)).To(BeTrue()) Expect(k8sClient.Update(ctx, keyValue)).To(Succeed()) By("setting an unknown ready state") keyValue.Status.Conditions = []api.Condition{{ Type: readyCondType, Status: v1.ConditionUnknown, Reason: "Test", Message: "start condition", LastTransitionTime: time.Now().Format(time.RFC3339Nano), }} Expect(k8sClient.Status().Update(ctx, keyValue)).To(Succeed()) }) It("should create a new keyvalue store", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) By("checking if the ready state was updated") Expect(keyValue.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(keyValue.Status.Conditions[0], v1.ConditionTrue, stateReady, "created or updated", time.Now()) By("checking if the observed generation matches") Expect(keyValue.Status.ObservedGeneration).To(Equal(keyValue.Generation)) By("checking if the keyvalue store was created") natsKeyValue, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) kvStatus, err := natsKeyValue.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(kvStatus.Bucket()).To(Equal(keyValueName)) Expect(kvStatus.History()).To(Equal(int64(10))) Expect(kvStatus.TTL()).To(Equal(5 * time.Minute)) Expect(kvStatus.IsCompressed()).To(BeTrue()) }) When("PreventUpdate is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") keyValue.Spec.PreventUpdate = true Expect(k8sClient.Update(ctx, keyValue)).To(Succeed()) }) It("should create the keyvalue", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that keyvalue was created") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) }) It("should not update the keyvalue", func(ctx SpecContext) { By("creating the keyvalue") _, err := jsClient.CreateKeyValue(ctx, emptyKeyValueConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that keyvalue was not updated") natsKeyValue, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) s, err := natsKeyValue.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) Expect(s.History()).To(BeEquivalentTo(int64(1))) }) }) When("read-only mode is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should not create the keyvalue", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no keyvalue was created") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) It("should not update the keyvalue", func(ctx SpecContext) { By("creating the keyvalue") _, err := jsClient.CreateKeyValue(ctx, emptyKeyValueConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that keyvalue was not updated") natsKeyValue, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) s, err := natsKeyValue.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) Expect(s.History()).To(BeEquivalentTo(int64(1))) }) }) When("namespace restriction is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting a namespace on the resource") namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not create the keyvalue", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no keyvalue was created") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) It("should not update the keyvalue", func(ctx SpecContext) { By("creating the keyvalue") _, err := jsClient.CreateKeyValue(ctx, emptyKeyValueConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that keyvalue was not updated") natsKeyValue, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) s, err := natsKeyValue.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) Expect(s.History()).To(BeEquivalentTo(int64(1))) }) }) It("should update an existing keyvalue", func(ctx SpecContext) { By("reconciling once to create the keyvalue") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) previousTransitionTime := keyValue.Status.Conditions[0].LastTransitionTime By("updating the resource") keyValue.Spec.Description = "new description" keyValue.Spec.History = 50 keyValue.Spec.TTL = "1h" Expect(k8sClient.Update(ctx, keyValue)).To(Succeed()) By("reconciling the updated resource") result, err = controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) By("checking if the state transition time was not updated") Expect(keyValue.Status.Conditions).To(HaveLen(1)) Expect(keyValue.Status.Conditions[0].LastTransitionTime).To(Equal(previousTransitionTime)) By("checking if the observed generation matches") Expect(keyValue.Status.ObservedGeneration).To(Equal(keyValue.Generation)) By("checking if the keyvalue was updated") natsKeyValue, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) keyValueStatus, err := natsKeyValue.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(keyValueStatus.Bucket()).To(Equal(keyValueName)) Expect(keyValueStatus.History()).To(Equal(int64(50))) Expect(keyValueStatus.TTL()).To(Equal(1 * time.Hour)) Expect(keyValueStatus.IsCompressed()).To(BeTrue()) }) It("should set an error state when the nats server is not available", func(ctx SpecContext) { By("setting up controller with unavailable nats server") // Setup client for not running server // Use actual test server to ensure port not used by other service on test instance sv := CreateTestServer() disconnectedController, err := NewJSController(k8sClient, &NatsConfig{ServerURL: sv.ClientURL()}, &Config{}) Expect(err).NotTo(HaveOccurred()) sv.Shutdown() controller := &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: disconnectedController, } By("reconciling resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(result).To(Equal(ctrl.Result{})) Expect(err).To(HaveOccurred()) // Will be re-queued with back-off // Fetch resource err = k8sClient.Get(ctx, typeNamespacedName, keyValue) Expect(err).NotTo(HaveOccurred()) By("checking if the status was updated") Expect(keyValue.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches( keyValue.Status.Conditions[0], v1.ConditionFalse, stateErrored, "create or update keyvalue:", time.Now(), ) By("checking if the observed generation does not match") Expect(keyValue.Status.ObservedGeneration).ToNot(Equal(keyValue.Generation)) }) When("the resource is marked for deletion", func() { BeforeEach(func(ctx SpecContext) { By("marking the resource for deletion") Expect(k8sClient.Delete(ctx, keyValue)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) // re-fetch after update }) It("should succeed deleting a not existing keyvalue", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, keyValue). ShouldNot(Succeed()) }) When("the underlying keyvalue exists", func() { BeforeEach(func(ctx SpecContext) { By("creating the keyvalue on the nats server") _, err := jsClient.CreateKeyValue(ctx, emptyKeyValueConfig) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func(ctx SpecContext) { err := jsClient.DeleteKeyValue(ctx, keyValueName) if err != nil { Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) } }) It("should delete the keyvalue", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the keyvalue is deleted") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, keyValue). ShouldNot(Succeed()) }) When("PreventDelete is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") keyValue.Spec.PreventDelete = true Expect(k8sClient.Update(ctx, keyValue)).To(Succeed()) }) It("Should delete the resource and not delete the nats keyvalue", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the keyvalue is not deleted") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, keyValue). ShouldNot(Succeed()) }) }) When("read only is set", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should delete the resource and not delete the keyvalue", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the keyvalue is not deleted") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, keyValue). ShouldNot(Succeed()) }) }) When("controller is restricted to different namespace", func() { BeforeEach(func(ctx SpecContext) { namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &KeyValueReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not delete the resource and keyvalue", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the keyvalue is not deleted") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) By("checking that the finalizer is not removed") Expect(k8sClient.Get(ctx, typeNamespacedName, keyValue)).To(Succeed()) Expect(keyValue.Finalizers).To(ContainElement(keyValueFinalizer)) }) }) }) }) It("should update keyvalue on different server as specified in spec", func(ctx SpecContext) { By("setting up the alternative server") // Setup altClient for alternate server altServer := CreateTestServer() defer altServer.Shutdown() By("setting the server in the keyvalue spec") keyValue.Spec.Servers = []string{altServer.ClientURL()} Expect(k8sClient.Update(ctx, keyValue)).To(Succeed()) By("checking precondition, that the keyvalue does not yet exist") _, err := jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("reconciling the resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) connPool := newConnPool(0) conn, err := connPool.Get(&NatsConfig{ServerURL: altServer.ClientURL()}, true) Expect(err).NotTo(HaveOccurred()) domain := "" By("checking if the keyvalue was created on the alternative server") altClient, err := CreateJetStreamClient(conn, true, domain) defer conn.Close() Expect(err).NotTo(HaveOccurred()) _, err = altClient.KeyValue(ctx, keyValueName) Expect(err).NotTo(HaveOccurred()) By("checking that the keyvalue was NOT created on the original server") _, err = jsClient.KeyValue(ctx, keyValueName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) }) }) func Test_mapKVSpecToConfig(t *testing.T) { date := time.Date(2024, 12, 3, 16, 55, 5, 0, time.UTC) dateString := date.Format(time.RFC3339) tests := []struct { name string spec *api.KeyValueSpec want jetstream.KeyValueConfig wantErr bool }{ { name: "empty spec", spec: &api.KeyValueSpec{}, want: jetstream.KeyValueConfig{}, wantErr: false, }, { name: "full spec", spec: &api.KeyValueSpec{ Description: "kv description", History: 20, MaxValueSize: 1024, MaxBytes: 1048576, TTL: "1h", LimitMarkerTTL: 2 * time.Hour, Mirror: &api.StreamSource{ Name: "mirror", OptStartSeq: 5, OptStartTime: dateString, FilterSubject: "orders", ExternalAPIPrefix: "api", ExternalDeliverPrefix: "deliver", SubjectTransforms: []*api.SubjectTransform{{ Source: "transform-source", Dest: "transform-dest", }}, }, Bucket: "kv-name", Placement: &api.StreamPlacement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Replicas: 3, RePublish: &api.RePublish{ Source: "re-publish-source", Destination: "re-publish-dest", HeadersOnly: true, }, Compression: true, Sources: []*api.StreamSource{{ Name: "source", OptStartSeq: 5, OptStartTime: dateString, FilterSubject: "orders", ExternalAPIPrefix: "api", ExternalDeliverPrefix: "deliver", SubjectTransforms: []*api.SubjectTransform{{ Source: "transform-source", Dest: "transform-dest", }}, }}, Storage: "memory", BaseStreamConfig: api.BaseStreamConfig{ PreventDelete: false, PreventUpdate: false, ConnectionOpts: api.ConnectionOpts{ Account: "", Creds: "", Nkey: "", Servers: nil, TLS: &api.TLS{}, }, }, }, want: jetstream.KeyValueConfig{ Bucket: "kv-name", Description: "kv description", MaxBytes: 1048576, TTL: time.Hour, LimitMarkerTTL: 2 * time.Hour, MaxValueSize: 1024, History: 20, Storage: jetstream.MemoryStorage, Replicas: 3, Placement: &jetstream.Placement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Mirror: &jetstream.StreamSource{ Name: "mirror", OptStartSeq: 5, OptStartTime: &date, FilterSubject: "orders", SubjectTransforms: []jetstream.SubjectTransformConfig{{ Source: "transform-source", Destination: "transform-dest", }}, External: &jetstream.ExternalStream{ APIPrefix: "api", DeliverPrefix: "deliver", }, Domain: "", }, Sources: []*jetstream.StreamSource{{ Name: "source", OptStartSeq: 5, OptStartTime: &date, FilterSubject: "orders", SubjectTransforms: []jetstream.SubjectTransformConfig{{ Source: "transform-source", Destination: "transform-dest", }}, External: &jetstream.ExternalStream{ APIPrefix: "api", DeliverPrefix: "deliver", }, Domain: "", }}, Compression: true, RePublish: &jetstream.RePublish{ Source: "re-publish-source", Destination: "re-publish-dest", HeadersOnly: true, }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) got, err := keyValueSpecToConfig(tt.spec) if (err != nil) != tt.wantErr { t.Errorf("keyValueSpecToConfig() error = %v, wantErr %v", err, tt.wantErr) return } // Compare nested structs assert.EqualValues(tt.want, got) }) } } ================================================ FILE: internal/controller/objectstore_controller.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/go-logr/logr" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/nats-io/nats.go/jetstream" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/predicate" ) const ( objStreamPrefix = "OBJ_" ) // ObjectStoreReconciler reconciles a ObjectStore object type ObjectStoreReconciler struct { Scheme *runtime.Scheme JetStreamController } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // It performs three main operations: // - Initialize finalizer and ready condition if not present // - Delete ObjectStore if it is marked for deletion. // - Create or Update the ObjectStore // // A call to reconcile may perform only one action, expecting the reconciliation to be triggered again by an update. // For example: Setting the finalizer triggers a second reconciliation. Reconcile returns after setting the finalizer, // to prevent parallel reconciliations performing the same steps. func (r *ObjectStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := klog.FromContext(ctx) if ok := r.ValidNamespace(req.Namespace); !ok { log.Info("Controller restricted to namespace, skipping reconciliation.") return ctrl.Result{}, nil } // Fetch ObjectStore resource objectStore := &api.ObjectStore{} if err := r.Get(ctx, req.NamespacedName, objectStore); err != nil { if apierrors.IsNotFound(err) { log.Info("ObjectStore resource deleted.", "objectStoreName", req.NamespacedName.String()) return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get objectstore resource '%s': %w", req.NamespacedName.String(), err) } log = log.WithValues("objectStoreName", objectStore.Spec.Bucket) // Update ready status to unknown when no status is set if len(objectStore.Status.Conditions) == 0 { log.Info("Setting initial ready condition to unknown.") objectStore.Status.Conditions = updateReadyCondition(objectStore.Status.Conditions, v1.ConditionUnknown, stateReconciling, "Starting reconciliation") err := r.Status().Update(ctx, objectStore) if err != nil { // If we get a conflict error, another reconciliation has already updated the status. // Just requeue and let the next reconciliation handle it. if apierrors.IsConflict(err) { return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("set condition unknown: %w", err) } return ctrl.Result{Requeue: true}, nil } // Check Deletion markedForDeletion := objectStore.GetDeletionTimestamp() != nil if markedForDeletion { if controllerutil.ContainsFinalizer(objectStore, objectStoreFinalizer) { err := r.deleteObjectStore(ctx, log, objectStore) if err != nil { return ctrl.Result{}, fmt.Errorf("delete objectstore: %w", err) } } else { log.Info("ObjectStore marked for deletion and already finalized. Ignoring.") } return ctrl.Result{}, nil } // Add finalizer if !controllerutil.ContainsFinalizer(objectStore, objectStoreFinalizer) { log.Info("Adding ObjectStore finalizer.") if ok := controllerutil.AddFinalizer(objectStore, objectStoreFinalizer); !ok { return ctrl.Result{}, errors.New("failed to add finalizer to objectstore resource") } if err := r.Update(ctx, objectStore); err != nil { return ctrl.Result{}, fmt.Errorf("update objectstore resource to add finalizer: %w", err) } return ctrl.Result{Requeue: true}, nil } // Create or update ObjectStore if err := r.createOrUpdate(ctx, log, objectStore); err != nil { return ctrl.Result{}, fmt.Errorf("create or update: %s", err) } return ctrl.Result{RequeueAfter: r.RequeueInterval()}, nil } func (r *ObjectStoreReconciler) deleteObjectStore(ctx context.Context, log logr.Logger, objectStore *api.ObjectStore) error { // Set status to false objectStore.Status.Conditions = updateReadyCondition(objectStore.Status.Conditions, v1.ConditionFalse, stateFinalizing, "Performing finalizer operations.") if err := r.Status().Update(ctx, objectStore); err != nil { return fmt.Errorf("update ready condition: %w", err) } storedState, err := getStoredObjectStoreState(objectStore) if err != nil { log.Error(err, "Failed to fetch stored state.") } if !objectStore.Spec.PreventDelete && !r.ReadOnly() { log.Info("Deleting ObjectStore.") err := r.WithJetStreamClient(objectStore.Spec.ConnectionOpts, objectStore.Namespace, func(js jetstream.JetStream) error { _, err := getServerObjectStoreState(ctx, js, objectStore) // If we have no known state for this object store it has never been reconciled. // If we are also receiving an error fetching state, either the object store does not exist // or this resource config is invalid. if err != nil && storedState == nil { return nil } return js.DeleteObjectStore(ctx, objectStore.Spec.Bucket) }) if errors.Is(err, jetstream.ErrStreamNotFound) || errors.Is(err, jetstream.ErrBucketNotFound) { log.Info("ObjectStore does not exist, unable to delete.", "objectStoreName", objectStore.Spec.Bucket) } else if err != nil && storedState == nil { log.Info("ObjectStore not reconciled and no state received from server. Removing finalizer.") } else if err != nil { return fmt.Errorf("delete objectstore during finalization: %w", err) } } else { log.Info("Skipping ObjectStore deletion.", "preventDelete", objectStore.Spec.PreventDelete, "read-only", r.ReadOnly(), ) } log.Info("Removing ObjectStore finalizer.") if ok := controllerutil.RemoveFinalizer(objectStore, objectStoreFinalizer); !ok { return errors.New("failed to remove objectstore finalizer") } if err := r.Update(ctx, objectStore); err != nil { return fmt.Errorf("remove finalizer: %w", err) } return nil } func (r *ObjectStoreReconciler) createOrUpdate(ctx context.Context, log logr.Logger, objectStore *api.ObjectStore) error { // Create or Update the ObjectStore based on the spec // Map spec to ObjectStore targetConfig targetConfig, err := objectStoreSpecToConfig(&objectStore.Spec) if err != nil { return fmt.Errorf("map spec to objectstore targetConfig: %w", err) } // UpdateObjectStore is called on every reconciliation when the stream is not to be deleted. err = r.WithJetStreamClient(objectStore.Spec.ConnectionOpts, objectStore.Namespace, func(js jetstream.JetStream) error { storedState, err := getStoredObjectStoreState(objectStore) if err != nil { log.Error(err, "Failed to fetch stored objectstore state") } serverState, err := getServerObjectStoreState(ctx, js, objectStore) if err != nil { return err } // Check against known state. Skip Update if converged. // Storing returned state from the server avoids have to // check default values or call Update on already converged resources if storedState != nil && serverState != nil && objectStore.Status.ObservedGeneration == objectStore.Generation { diff := compareConfigState(storedState, serverState) if diff == "" { return nil } log.Info("Object Store config drifted from desired state.", "diff", diff) } if r.ReadOnly() { log.Info("Skipping ObjectStore creation or update.", "read-only", r.ReadOnly(), ) return nil } var updatedObjectStore jetstream.ObjectStore err = nil if serverState == nil { log.Info("Creating ObjectStore.") updatedObjectStore, err = js.CreateObjectStore(ctx, targetConfig) if err != nil { return err } } else if !objectStore.Spec.PreventUpdate { log.Info("Updating ObjectStore.") updatedObjectStore, err = js.UpdateObjectStore(ctx, targetConfig) if err != nil { return err } updatedObjectStore, err := getServerObjectStoreState(ctx, js, objectStore) if err != nil { log.Error(err, "Failed to fetch updated objectstore state") } else { diff := compareConfigState(updatedObjectStore, serverState) log.Info("Updated ObjectStore.", "diff", diff) } } else { log.Info("Skipping ObjectStore update.", "preventUpdate", objectStore.Spec.PreventUpdate, ) } if updatedObjectStore != nil { // Store known state in annotation serverState, err = getServerObjectStoreState(ctx, js, objectStore) if err != nil { return err } updatedState, err := json.Marshal(serverState) if err != nil { return err } if objectStore.Annotations == nil { objectStore.Annotations = map[string]string{} } objectStore.Annotations[stateAnnotationObj] = string(updatedState) return r.Update(ctx, objectStore) } return nil }) if err != nil { err = fmt.Errorf("create or update objectstore: %w", err) objectStore.Status.Conditions = updateReadyCondition(objectStore.Status.Conditions, v1.ConditionFalse, stateErrored, err.Error()) if err := r.Status().Update(ctx, objectStore); err != nil { log.Error(err, "Failed to update ready condition to Errored.") } return err } // update the observed generation and ready status objectStore.Status.ObservedGeneration = objectStore.Generation objectStore.Status.Conditions = updateReadyCondition( objectStore.Status.Conditions, v1.ConditionTrue, stateReady, "ObjectStore successfully created or updated.", ) err = r.Status().Update(ctx, objectStore) if err != nil { return fmt.Errorf("update ready condition: %w", err) } return nil } func getStoredObjectStoreState(objectStore *api.ObjectStore) (*jetstream.StreamConfig, error) { var storedState *jetstream.StreamConfig if state, ok := objectStore.Annotations[stateAnnotationObj]; ok { err := json.Unmarshal([]byte(state), &storedState) if err != nil { return nil, err } } return storedState, nil } // Fetch the current state of the ObjectStore stream from the server. // ErrStreamNotFound is considered a valid response and does not return error func getServerObjectStoreState(ctx context.Context, js jetstream.JetStream, objectStore *api.ObjectStore) (*jetstream.StreamConfig, error) { s, err := js.Stream(ctx, fmt.Sprintf("%s%s", objStreamPrefix, objectStore.Spec.Bucket)) if errors.Is(err, jetstream.ErrStreamNotFound) { return nil, nil } if err != nil { return nil, err } return &s.CachedInfo().Config, nil } // objectStoreSpecToConfig creates a jetstream.ObjectStoreConfig matching the given ObjectStore resource spec func objectStoreSpecToConfig(spec *api.ObjectStoreSpec) (jetstream.ObjectStoreConfig, error) { // Set directly mapped fields config := jetstream.ObjectStoreConfig{ Bucket: spec.Bucket, Description: spec.Description, MaxBytes: int64(spec.MaxBytes), Replicas: spec.Replicas, Compression: spec.Compression, Metadata: spec.Metadata, } // TTL if spec.TTL != "" { t, err := time.ParseDuration(spec.TTL) if err != nil { return jetstream.ObjectStoreConfig{}, fmt.Errorf("invalid ttl: %w", err) } config.TTL = t } // storage if spec.Storage != "" { err := config.Storage.UnmarshalJSON(jsonString(spec.Storage)) if err != nil { return jetstream.ObjectStoreConfig{}, fmt.Errorf("invalid storage: %w", err) } } // placement if spec.Placement != nil { config.Placement = &jetstream.Placement{ Cluster: spec.Placement.Cluster, Tags: spec.Placement.Tags, } } return config, nil } // SetupWithManager sets up the controller with the Manager. func (r *ObjectStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.ObjectStore{}). WithEventFilter(predicate.GenerationChangedPredicate{}). WithOptions(controller.Options{ MaxConcurrentReconciles: 1, }). Complete(r) } ================================================ FILE: internal/controller/objectstore_controller_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "testing" "time" "github.com/nats-io/nats.go/jetstream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) var _ = Describe("ObjectStore Controller", func() { // The test objectStore resource const resourceName = "test-objectstore" const objectStoreName = "orders" const alternateResource = "alternate-objectstore" const alternateNamespace = "alternate-namespace" typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", } objectStore := &api.ObjectStore{} // The tested controller var controller *ObjectStoreReconciler // Config to create minimal nats ObjectStore store emptyObjectStoreConfig := jetstream.ObjectStoreConfig{ Bucket: objectStoreName, Replicas: 1, Storage: jetstream.FileStorage, } BeforeEach(func(ctx SpecContext) { By("creating a test objectstore resource") err := k8sClient.Get(ctx, typeNamespacedName, objectStore) if err != nil && k8serrors.IsNotFound(err) { resource := &api.ObjectStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: api.ObjectStoreSpec{ Bucket: objectStoreName, Replicas: 1, TTL: "5m", Compression: true, Description: "test objectstore", Storage: "file", }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) // Re-fetch ObjectStore Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) } By("checking precondition: nats objectstore does not exist") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("setting up the tested controller") controller = &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: baseController, } }) AfterEach(func(ctx SpecContext) { By("removing the test objectstore resource") resource := &api.ObjectStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err != nil { Expect(err).To(MatchError(k8serrors.IsNotFound, "Is not found")) } else { if controllerutil.ContainsFinalizer(resource, objectStoreFinalizer) { By("removing the finalizer") controllerutil.RemoveFinalizer(resource, objectStoreFinalizer) Expect(k8sClient.Update(ctx, resource)).To(Succeed()) } By("removing the objectstore resource") Expect(k8sClient.Delete(ctx, resource)). To(SatisfyAny( Succeed(), MatchError(k8serrors.IsNotFound, "is not found"), )) } By("deleting the nats objectstore store") Expect(jsClient.DeleteObjectStore(ctx, objectStoreName)). To(SatisfyAny( Succeed(), MatchError(jetstream.ErrStreamNotFound), )) }) When("reconciling a not existing resource", func() { It("should stop reconciliation without error", func(ctx SpecContext) { By("reconciling the created resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: "fake", Name: "not-existing", }, }) Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) }) }) When("reconciling a not initialized resource", func() { It("should initialize a new resource", func(ctx SpecContext) { By("re-queueing until it is initialized") // Initialization can require multiple reconciliation loops Eventually(func(ctx SpecContext) *api.ObjectStore { _, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) got := &api.ObjectStore{} Expect(k8sClient.Get(ctx, typeNamespacedName, got)).To(Succeed()) return got }).WithContext(ctx). Should(SatisfyAll( HaveField("Status.Conditions", Not(BeEmpty())), )) By("validating the ready condition") // Fetch ObjectStore Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) Expect(objectStore.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(objectStore.Status.Conditions[0], v1.ConditionUnknown, stateReconciling, "Starting reconciliation", time.Now()) }) }) When("reconciling a resource in a different namespace", func() { BeforeEach(func(ctx SpecContext) { By("creating an objectstore resource in an alternate namespace while namespaced") alternateNamespaceResource := &api.ObjectStore{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, Spec: api.ObjectStoreSpec{ Bucket: alternateResource, Replicas: 1, TTL: "5m", Description: "objectstore in alternate namespace", Storage: "file", }, } ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: alternateNamespace, }, } err := k8sClient.Create(ctx, ns) if err != nil && !k8serrors.IsAlreadyExists(err) { Expect(err).NotTo(HaveOccurred()) } Expect(k8sClient.Create(ctx, alternateNamespaceResource)).To(Succeed()) }) AfterEach(func(ctx SpecContext) { By("cleaning up the resource in alternate namespace") alternateObjectStore := &api.ObjectStore{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, } err := k8sClient.Delete(ctx, alternateObjectStore) if err != nil && !k8serrors.IsNotFound(err) { Expect(err).NotTo(HaveOccurred()) } }) It("should not watch the resource in alternate namespace", func(ctx SpecContext) { By("reconciling with no explicit namespace restriction") alternateNamespacedName := types.NamespacedName{ Namespace: alternateNamespace, Name: alternateResource, } By("running reconciliation for the resource in alternate namespace") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: alternateNamespacedName, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) By("checking the objectstore doesn't exist in NATS") _, err = jsClient.ObjectStore(ctx, alternateResource) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("verifying the resource still exists in the alternate namespace") alternateObjectStore := &api.ObjectStore{} Expect(k8sClient.Get(ctx, alternateNamespacedName, alternateObjectStore)).To(Succeed()) By("checking no conditions were set on the resource") Expect(alternateObjectStore.Status.Conditions).To(BeEmpty()) }) It("should watch the resource in alternate namespace when not namespaced", func(ctx SpecContext) { By("reconciling with a non-namespaced controller") testNatsConfig := &NatsConfig{ServerURL: clientUrl} alternateBaseController, err := NewJSController(k8sClient, testNatsConfig, &Config{}) Expect(err).NotTo(HaveOccurred()) alternateController := &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: alternateBaseController, } resourceNames := []types.NamespacedName{ typeNamespacedName, { Namespace: alternateNamespace, Name: alternateResource, }, } By("running reconciliation for the resources in all namespaces") for _, n := range resourceNames { result, err := alternateController.Reconcile(ctx, reconcile.Request{ NamespacedName: n, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).NotTo(Equal(ctrl.Result{})) } }) }) When("reconciling an initialized resource", func() { BeforeEach(func(ctx SpecContext) { By("initializing the objectstore resource") By("setting the finalizer") Expect(controllerutil.AddFinalizer(objectStore, objectStoreFinalizer)).To(BeTrue()) Expect(k8sClient.Update(ctx, objectStore)).To(Succeed()) By("setting an unknown ready state") objectStore.Status.Conditions = []api.Condition{{ Type: readyCondType, Status: v1.ConditionUnknown, Reason: "Test", Message: "start condition", LastTransitionTime: time.Now().Format(time.RFC3339Nano), }} Expect(k8sClient.Status().Update(ctx, objectStore)).To(Succeed()) }) It("should create a new objectstore store", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) By("checking if the ready state was updated") Expect(objectStore.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(objectStore.Status.Conditions[0], v1.ConditionTrue, stateReady, "created or updated", time.Now()) By("checking if the observed generation matches") Expect(objectStore.Status.ObservedGeneration).To(Equal(objectStore.Generation)) By("checking if the objectstore store was created") natsObjectStore, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) objectstoreStatus, err := natsObjectStore.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(objectstoreStatus.Bucket()).To(Equal(objectStoreName)) Expect(objectstoreStatus.TTL()).To(Equal(5 * time.Minute)) Expect(objectstoreStatus.IsCompressed()).To(BeTrue()) }) When("PreventUpdate is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") objectStore.Spec.PreventUpdate = true Expect(k8sClient.Update(ctx, objectStore)).To(Succeed()) }) It("should create the objectstore", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that objectstore was created") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) }) It("should not update the objectstore", func(ctx SpecContext) { By("creating the objectstore") _, err := jsClient.CreateObjectStore(ctx, emptyObjectStoreConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that objectstore was not updated") natsObjectStore, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) s, err := natsObjectStore.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) }) }) When("read-only mode is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should not create the objectstore", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no objectstore was created") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) It("should not update the objectstore", func(ctx SpecContext) { By("creating the objectstore") _, err := jsClient.CreateObjectStore(ctx, emptyObjectStoreConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that objectstore was not updated") natsObjectStore, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) s, err := natsObjectStore.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) }) }) When("namespace restriction is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting a namespace on the resource") namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not create the objectstore", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no objectstore was created") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) It("should not update the objectstore", func(ctx SpecContext) { By("creating the objectstore") _, err := jsClient.CreateObjectStore(ctx, emptyObjectStoreConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that objectstore was not updated") natsObjectStore, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) s, err := natsObjectStore.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(s.IsCompressed()).To(BeFalse()) }) }) It("should update an existing objectstore", func(ctx SpecContext) { By("reconciling once to create the objectstore") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) previousTransitionTime := objectStore.Status.Conditions[0].LastTransitionTime By("updating the resource") objectStore.Spec.Description = "new description" objectStore.Spec.TTL = "1h" Expect(k8sClient.Update(ctx, objectStore)).To(Succeed()) By("reconciling the updated resource") result, err = controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) By("checking if the state transition time was not updated") Expect(objectStore.Status.Conditions).To(HaveLen(1)) Expect(objectStore.Status.Conditions[0].LastTransitionTime).To(Equal(previousTransitionTime)) By("checking if the observed generation matches") Expect(objectStore.Status.ObservedGeneration).To(Equal(objectStore.Generation)) By("checking if the objectstore was updated") natsObjectStore, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) objectStoreStatus, err := natsObjectStore.Status(ctx) Expect(err).NotTo(HaveOccurred()) Expect(objectStoreStatus.Bucket()).To(Equal(objectStoreName)) Expect(objectStoreStatus.TTL()).To(Equal(1 * time.Hour)) Expect(objectStoreStatus.IsCompressed()).To(BeTrue()) }) It("should set an error state when the nats server is not available", func(ctx SpecContext) { By("setting up controller with unavailable nats server") // Setup client for not running server // Use actual test server to ensure port not used by other service on test instance sv := CreateTestServer() disconnectedController, err := NewJSController(k8sClient, &NatsConfig{ServerURL: sv.ClientURL()}, &Config{}) Expect(err).NotTo(HaveOccurred()) sv.Shutdown() controller := &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: disconnectedController, } By("reconciling resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(result).To(Equal(ctrl.Result{})) Expect(err).To(HaveOccurred()) // Will be re-queued with back-off // Fetch resource err = k8sClient.Get(ctx, typeNamespacedName, objectStore) Expect(err).NotTo(HaveOccurred()) By("checking if the status was updated") Expect(objectStore.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches( objectStore.Status.Conditions[0], v1.ConditionFalse, stateErrored, "create or update objectstore:", time.Now(), ) By("checking if the observed generation does not match") Expect(objectStore.Status.ObservedGeneration).ToNot(Equal(objectStore.Generation)) }) When("the resource is marked for deletion", func() { BeforeEach(func(ctx SpecContext) { By("marking the resource for deletion") Expect(k8sClient.Delete(ctx, objectStore)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) // re-fetch after update }) It("should succeed deleting a not existing objectstore", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, objectStore). ShouldNot(Succeed()) }) When("the underlying objectstore exists", func() { BeforeEach(func(ctx SpecContext) { By("creating the objectstore on the nats server") _, err := jsClient.CreateObjectStore(ctx, emptyObjectStoreConfig) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func(ctx SpecContext) { err := jsClient.DeleteObjectStore(ctx, objectStoreName) if err != nil { Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) } }) It("should delete the objectstore", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the objectstore is deleted") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, objectStore). ShouldNot(Succeed()) }) When("PreventDelete is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") objectStore.Spec.PreventDelete = true Expect(k8sClient.Update(ctx, objectStore)).To(Succeed()) }) It("Should delete the resource and not delete the nats objectstore", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the objectstore is not deleted") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, objectStore). ShouldNot(Succeed()) }) }) When("read only is set", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should delete the resource and not delete the objectstore", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the objectstore is not deleted") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, objectStore). ShouldNot(Succeed()) }) }) When("controller is restricted to different namespace", func() { BeforeEach(func(ctx SpecContext) { namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &ObjectStoreReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not delete the resource and objectstore", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the objectstore is not deleted") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) By("checking that the finalizer is not removed") Expect(k8sClient.Get(ctx, typeNamespacedName, objectStore)).To(Succeed()) Expect(objectStore.Finalizers).To(ContainElement(objectStoreFinalizer)) }) }) }) }) It("should update objectstore on different server as specified in spec", func(ctx SpecContext) { By("setting up the alternative server") // Setup altClient for alternate server altServer := CreateTestServer() defer altServer.Shutdown() By("setting the server in the objectstore spec") objectStore.Spec.Servers = []string{altServer.ClientURL()} Expect(k8sClient.Update(ctx, objectStore)).To(Succeed()) By("checking precondition, that the objectstore does not yet exist") _, err := jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) By("reconciling the resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) connPool := newConnPool(0) conn, err := connPool.Get(&NatsConfig{ServerURL: altServer.ClientURL()}, true) Expect(err).NotTo(HaveOccurred()) domain := "" By("checking if the objectstore was created on the alternative server") altClient, err := CreateJetStreamClient(conn, true, domain) defer conn.Close() Expect(err).NotTo(HaveOccurred()) _, err = altClient.ObjectStore(ctx, objectStoreName) Expect(err).NotTo(HaveOccurred()) By("checking that the objectstore was NOT created on the original server") _, err = jsClient.ObjectStore(ctx, objectStoreName) Expect(err).To(MatchError(jetstream.ErrBucketNotFound)) }) }) }) func Test_mapobjectstoreSpecToConfig(t *testing.T) { tests := []struct { name string spec *api.ObjectStoreSpec want jetstream.ObjectStoreConfig wantErr bool }{ { name: "empty spec", spec: &api.ObjectStoreSpec{}, want: jetstream.ObjectStoreConfig{}, wantErr: false, }, { name: "full spec", spec: &api.ObjectStoreSpec{ Description: "objectstore description", MaxBytes: 1048576, TTL: "1h", Bucket: "objectstore-name", Placement: &api.StreamPlacement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Replicas: 3, Compression: true, Storage: "memory", Metadata: map[string]string{ "foo": "bar", }, BaseStreamConfig: api.BaseStreamConfig{ PreventDelete: false, PreventUpdate: false, ConnectionOpts: api.ConnectionOpts{ Account: "", Creds: "", Nkey: "", Servers: nil, TLS: &api.TLS{}, }, }, }, want: jetstream.ObjectStoreConfig{ Bucket: "objectstore-name", Description: "objectstore description", MaxBytes: 1048576, TTL: time.Hour, Storage: jetstream.MemoryStorage, Replicas: 3, Placement: &jetstream.Placement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Compression: true, Metadata: map[string]string{ "foo": "bar", }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) got, err := objectStoreSpecToConfig(tt.spec) if (err != nil) != tt.wantErr { t.Errorf("objectStoreSpecToConfig() error = %v, wantErr %v", err, tt.wantErr) return } // Compare nested structs assert.EqualValues(tt.want, got) }) } } ================================================ FILE: internal/controller/register.go ================================================ package controller import ( "fmt" "time" ctrl "sigs.k8s.io/controller-runtime" ) // The Config contains parameters to be considered by the registered controllers. // // ReadOnly prevents controllers from actually applying changes NATS resources. // // Namespace restricts the controller to resources of the given namespace. type Config struct { ReadOnly bool Namespace string RequeueInterval time.Duration CacheDir string HealthProbeBindAddress string } // RegisterAll registers all available jetStream controllers to the manager. // natsCfg is specific to the nats server connection. // controllerCfg defines behaviour of the registered controllers. func RegisterAll(mgr ctrl.Manager, clientConfig *NatsConfig, config *Config) error { scheme := mgr.GetScheme() // Register controllers baseController, err := NewJSController(mgr.GetClient(), clientConfig, config) if err != nil { return fmt.Errorf("create base jetstream controller: %w", err) } if err := (&AccountReconciler{ Scheme: scheme, JetStreamController: baseController, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create account controller: %w", err) } if err := (&ConsumerReconciler{ Scheme: scheme, JetStreamController: baseController, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create consumer controller: %w", err) } if err := (&KeyValueReconciler{ Scheme: scheme, JetStreamController: baseController, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create key-value controller: %w", err) } if err := (&ObjectStoreReconciler{ Scheme: scheme, JetStreamController: baseController, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create object store controller: %w", err) } if err := (&StreamReconciler{ Scheme: scheme, JetStreamController: baseController, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create stream controller: %w", err) } return nil } ================================================ FILE: internal/controller/stream_controller.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/go-logr/logr" "github.com/nats-io/jsm.go" jsmapi "github.com/nats-io/jsm.go/api" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" "github.com/nats-io/nats.go/jetstream" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/predicate" ) // StreamReconciler reconciles a Stream object type StreamReconciler struct { Scheme *runtime.Scheme JetStreamController } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // It performs three main operations: // - Initialize finalizer and ready condition if not present // - Delete stream if it is marked for deletion. // - Create or Update the stream // // A call to reconcile may perform only one action, expecting the reconciliation to be triggered again by an update. // For example: Setting the finalizer triggers a second reconciliation. Reconcile returns after setting the finalizer, // to prevent parallel reconciliations performing the same steps. func (r *StreamReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := klog.FromContext(ctx) if ok := r.ValidNamespace(req.Namespace); !ok { log.Info("Controller restricted to namespace, skipping reconciliation.") return ctrl.Result{}, nil } // Fetch stream resource stream := &api.Stream{} if err := r.Get(ctx, req.NamespacedName, stream); err != nil { if apierrors.IsNotFound(err) { log.Info("Stream resource deleted.", "streamName", req.NamespacedName.String()) return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get stream resource '%s': %w", req.NamespacedName.String(), err) } log = log.WithValues("streamName", stream.Spec.Name) // Update ready status to unknown when no status is set if len(stream.Status.Conditions) == 0 { log.Info("Setting initial ready condition to unknown.") stream.Status.Conditions = updateReadyCondition(stream.Status.Conditions, v1.ConditionUnknown, stateReconciling, "Starting reconciliation") err := r.Status().Update(ctx, stream) if err != nil { // If we get a conflict error, another reconciliation has already updated the status. // Just requeue and let the next reconciliation handle it. if apierrors.IsConflict(err) { return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("set condition unknown: %w", err) } return ctrl.Result{Requeue: true}, nil } // Check Deletion markedForDeletion := stream.GetDeletionTimestamp() != nil if markedForDeletion { if controllerutil.ContainsFinalizer(stream, streamFinalizer) { err := r.deleteStream(ctx, log, stream) if err != nil { return ctrl.Result{}, fmt.Errorf("delete stream: %w", err) } } else { log.Info("Stream marked for deletion and already finalized. Ignoring.") } return ctrl.Result{}, nil } // Add finalizer if !controllerutil.ContainsFinalizer(stream, streamFinalizer) { log.Info("Adding stream finalizer.") if ok := controllerutil.AddFinalizer(stream, streamFinalizer); !ok { return ctrl.Result{}, errors.New("failed to add finalizer to stream resource") } if err := r.Update(ctx, stream); err != nil { return ctrl.Result{}, fmt.Errorf("update stream resource to add finalizer: %w", err) } // After we have added the finalizer, we need to requeue to make sure we reconcile the // rest of the object. Just updating metadata won't make the API server update generation // so the update above shouldn't trigger a new reconciliation. return ctrl.Result{Requeue: true}, nil } // Create or update stream if err := r.createOrUpdate(ctx, log, stream); err != nil { return ctrl.Result{}, fmt.Errorf("create or update: %s", err) } return ctrl.Result{RequeueAfter: r.RequeueInterval()}, nil } func (r *StreamReconciler) deleteStream(ctx context.Context, log logr.Logger, stream *api.Stream) error { // Set status to false stream.Status.Conditions = updateReadyCondition(stream.Status.Conditions, v1.ConditionFalse, stateFinalizing, "Performing finalizer operations.") if err := r.Status().Update(ctx, stream); err != nil { return fmt.Errorf("update ready condition: %w", err) } storedState, err := getStoredStreamState(stream) if err != nil { log.Error(err, "Failed to fetch stored state.") } if !stream.Spec.PreventDelete && !r.ReadOnly() { log.Info("Deleting stream.") err := r.WithJSMClient(stream.Spec.ConnectionOpts, stream.Namespace, func(js *jsm.Manager) error { _, err := getServerStreamState(js, stream) // If we have no known state for this stream it has never been reconciled. // If we are also receiving an error fetching state, either the stream does not exist // or this resource config is invalid. if err != nil && storedState == nil { return nil } return js.DeleteStream(stream.Spec.Name) }) if jsmapi.IsNatsErr(err, JSStreamNotFoundErr) { log.Info("Stream does not exist, unable to delete.", "streamName", stream.Spec.Name) } else if err != nil && storedState == nil { log.Info("Stream not reconciled and no state received from server. Removing finalizer.") } else if err != nil { return fmt.Errorf("delete stream during finalization: %w", err) } } else { log.Info("Skipping stream deletion.", "preventDelete", stream.Spec.PreventDelete, "read-only", r.ReadOnly(), ) } log.Info("Removing stream finalizer.") if ok := controllerutil.RemoveFinalizer(stream, streamFinalizer); !ok { return errors.New("failed to remove stream finalizer") } if err := r.Update(ctx, stream); err != nil { return fmt.Errorf("remove finalizer: %w", err) } return nil } func (r *StreamReconciler) createOrUpdate(ctx context.Context, log logr.Logger, stream *api.Stream) error { // CreateOrUpdateStream is called on every reconciliation when the stream is not to be deleted. err := r.WithJSMClient(stream.Spec.ConnectionOpts, stream.Namespace, func(js *jsm.Manager) error { storedState, err := getStoredStreamState(stream) if err != nil { log.Error(err, "Failed to fetch stored stream state") } serverState, err := getServerStreamState(js, stream) if err != nil { return err } // Map spec to stream targetConfig, passing current server state for context targetConfig, err := streamSpecToConfig(&stream.Spec, serverState) if err != nil { return fmt.Errorf("map spec to stream targetConfig: %w", err) } // Check against known state. Skip Update if converged. // Storing returned state from the server avoids have to // check default values or call Update on already converged resources if storedState != nil && serverState != nil && stream.Status.ObservedGeneration == stream.Generation { diff := compareConfigState(storedState, serverState) if diff == "" { return nil } log.Info("Stream config drifted from desired state.", "diff", diff) } if r.ReadOnly() { log.Info("Skipping stream creation or update.", "read-only", r.ReadOnly(), ) return nil } var updatedStream *jsm.Stream err = nil if serverState == nil { log.Info("Creating Stream.") updatedStream, err = js.NewStream(stream.Spec.Name, targetConfig...) if err != nil { return err } } else if !stream.Spec.PreventUpdate { log.Info("Updating Stream.") s, err := js.LoadStream(stream.Spec.Name) if err != nil { return err } err = s.UpdateConfiguration(*serverState, targetConfig...) if err != nil { return err } updatedStream, err = js.LoadStream(stream.Spec.Name) if err != nil { return err } diff := compareConfigState(updatedStream.Configuration(), *serverState) log.Info("Updated Stream.", "diff", diff) } else { log.Info("Skipping Stream update.", "preventUpdate", stream.Spec.PreventUpdate, ) } if updatedStream != nil { // Store known state in annotation updatedState, err := json.Marshal(updatedStream.Configuration()) if err != nil { return err } if stream.Annotations == nil { stream.Annotations = map[string]string{} } stream.Annotations[stateAnnotationStream] = string(updatedState) return r.Update(ctx, stream) } return nil }) if err != nil { err = fmt.Errorf("create or update stream: %w", err) stream.Status.Conditions = updateReadyCondition(stream.Status.Conditions, v1.ConditionFalse, stateErrored, err.Error()) if err := r.Status().Update(ctx, stream); err != nil { log.Error(err, "Failed to update ready condition to Errored.") } return err } // update the observed generation and ready status stream.Status.ObservedGeneration = stream.Generation stream.Status.Conditions = updateReadyCondition( stream.Status.Conditions, v1.ConditionTrue, stateReady, "Stream successfully created or updated.", ) err = r.Status().Update(ctx, stream) if err != nil { return fmt.Errorf("update ready condition: %w", err) } return nil } func getStoredStreamState(stream *api.Stream) (*jsmapi.StreamConfig, error) { var storedState *jsmapi.StreamConfig if state, ok := stream.Annotations[stateAnnotationStream]; ok { err := json.Unmarshal([]byte(state), &storedState) if err != nil { return nil, err } } return storedState, nil } // Fetch the current state of the stream from the server. // JSStreamNotFoundErr is considered a valid response and does not return error func getServerStreamState(jsm *jsm.Manager, stream *api.Stream) (*jsmapi.StreamConfig, error) { s, err := jsm.LoadStream(stream.Spec.Name) if jsmapi.IsNatsErr(err, JSStreamNotFoundErr) { return nil, nil } if err != nil { return nil, err } streamCfg := s.Configuration() return &streamCfg, nil } func streamSpecToConfig(spec *api.StreamSpec, currentConfig *jsmapi.StreamConfig) ([]jsm.StreamOption, error) { opts := []jsm.StreamOption{ jsm.StreamDescription(spec.Description), jsm.Subjects(spec.Subjects...), jsm.MaxConsumers(spec.MaxConsumers), jsm.MaxMessages(int64(spec.MaxMsgs)), jsm.MaxBytes(int64(spec.MaxBytes)), jsm.MaxMessageSize(int32(spec.MaxMsgSize)), jsm.Replicas(spec.Replicas), jsm.StreamMetadata(spec.Metadata), } // Set not directly mapped fields // retention switch spec.Retention { case "limits": opts = append(opts, jsm.LimitsRetention()) case "interest": opts = append(opts, jsm.InterestRetention()) case "workqueue": opts = append(opts, jsm.WorkQueueRetention()) } // maxMsgsPerSubject if spec.MaxMsgsPerSubject > 0 { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.MaxMsgsPer = int64(spec.MaxMsgsPerSubject) return nil }) } // maxAge if spec.MaxAge != "" { d, err := time.ParseDuration(spec.MaxAge) if err != nil { return nil, fmt.Errorf("parse max age: %w", err) } opts = append(opts, jsm.MaxAge(d)) } // storage switch spec.Storage { case "file": opts = append(opts, jsm.FileStorage()) case "memory": opts = append(opts, jsm.MemoryStorage()) } // discard switch spec.Discard { case "old": opts = append(opts, jsm.DiscardOld()) case "new": opts = append(opts, jsm.DiscardNew()) } // noAck if spec.NoAck { opts = append(opts, jsm.NoAck()) } // duplicateWindow if spec.DuplicateWindow != "" { d, err := time.ParseDuration(spec.DuplicateWindow) if err != nil { return nil, fmt.Errorf("parse duplicate window: %w", err) } opts = append(opts, jsm.DuplicateWindow(d)) } // placement if spec.Placement != nil { if spec.Placement.Cluster != "" { opts = append(opts, jsm.PlacementCluster(spec.Placement.Cluster)) } if spec.Placement.Tags != nil { opts = append(opts, jsm.PlacementTags(spec.Placement.Tags...)) } } else if currentConfig != nil && currentConfig.Placement != nil { // Only clear placement if the current config has placement set. // This avoids triggering NATS error 10123: "can not move and scale a stream in a single update" // when we're only trying to change replicas. opts = append(opts, jsm.PlacementCluster("")) } // If spec.Placement is nil and currentConfig.Placement is also nil/empty, // we don't set any placement option, avoiding unnecessary placement changes. // mirror if spec.Mirror != nil { ss, err := mapJSMStreamSource(spec.Mirror) if err != nil { return nil, fmt.Errorf("map mirror stream source: %w", err) } opts = append(opts, jsm.Mirror(ss)) } // sources if spec.Sources != nil { streamSources := make([]*jsmapi.StreamSource, 0) for _, source := range spec.Sources { ss, err := mapJSMStreamSource(source) if err != nil { return nil, fmt.Errorf("map stream source: %w", err) } streamSources = append(streamSources, ss) } opts = append(opts, jsm.Sources(streamSources...)) } // compression switch spec.Compression { case "s2": opts = append(opts, jsm.Compression(jsmapi.S2Compression)) case "none": opts = append(opts, jsm.Compression(jsmapi.NoCompression)) } // subjectTransform if spec.SubjectTransform != nil { st := &jsmapi.SubjectTransformConfig{ Source: spec.SubjectTransform.Source, Destination: spec.SubjectTransform.Dest, } opts = append(opts, jsm.SubjectTransform(st)) } // rePublish if spec.RePublish != nil { r := &jsmapi.RePublish{ Source: spec.RePublish.Source, Destination: spec.RePublish.Destination, HeadersOnly: spec.RePublish.HeadersOnly, } opts = append(opts, jsm.Republish(r)) } if spec.Sealed { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.Sealed = spec.Sealed return nil }) } // denyDelete if spec.DenyDelete { opts = append(opts, jsm.DenyDelete()) } // denyPurge if spec.DenyPurge { opts = append(opts, jsm.DenyPurge()) } // allowDirect if spec.AllowDirect { opts = append(opts, jsm.AllowDirect()) } // allowRollup if spec.AllowRollup { opts = append(opts, jsm.AllowRollup()) } // mirrorDirect if spec.MirrorDirect { opts = append(opts, jsm.MirrorDirect()) } // discardPerSubject if spec.DiscardPerSubject { opts = append(opts, jsm.DiscardNewPerSubject()) } // firstSequence if spec.FirstSequence > 0 { opts = append(opts, jsm.FirstSequence(spec.FirstSequence)) } // consumerLimits if spec.ConsumerLimits != nil { cl := jsmapi.StreamConsumerLimits{ MaxAckPending: spec.ConsumerLimits.MaxAckPending, } if spec.ConsumerLimits.InactiveThreshold != "" { inactiveThreshold, err := time.ParseDuration(spec.ConsumerLimits.InactiveThreshold) if err != nil { return nil, fmt.Errorf("parse inactive threshold: %w", err) } cl.InactiveThreshold = inactiveThreshold } opts = append(opts, jsm.ConsumerLimits(cl)) } // allowMsgTtl if spec.AllowMsgTTL { opts = append(opts, jsm.AllowMsgTTL()) } // subjectDeleteMarkerTtl if spec.SubjectDeleteMarkerTTL != "" { d, err := time.ParseDuration(spec.SubjectDeleteMarkerTTL) if err != nil { return nil, fmt.Errorf("parse subject delete marker TTL: %w", err) } opts = append(opts, jsm.SubjectDeleteMarkerTTL(d)) } // allowMsgCounter if spec.AllowMsgCounter { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowMsgCounter = true return nil }) } // allowAtomicPublish if spec.AllowAtomicPublish { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowAtomicPublish = true return nil }) } // allowMsgSchedules if spec.AllowMsgSchedules { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.AllowMsgSchedules = true return nil }) } // persistMode if spec.PersistMode == "async" { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.PersistMode = jsmapi.AsyncPersistMode return nil }) } else if spec.PersistMode == "default" { opts = append(opts, func(o *jsmapi.StreamConfig) error { o.PersistMode = jsmapi.DefaultPersistMode return nil }) } return opts, nil } func mapStreamSource(ss *api.StreamSource) (*jetstream.StreamSource, error) { jss := &jetstream.StreamSource{ Name: ss.Name, FilterSubject: ss.FilterSubject, } if ss.OptStartSeq > 0 { jss.OptStartSeq = uint64(ss.OptStartSeq) } if ss.OptStartTime != "" { t, err := time.Parse(time.RFC3339, ss.OptStartTime) if err != nil { return nil, fmt.Errorf("parse opt start time: %w", err) } jss.OptStartTime = &t } if ss.ExternalAPIPrefix != "" || ss.ExternalDeliverPrefix != "" { jss.External = &jetstream.ExternalStream{ APIPrefix: ss.ExternalAPIPrefix, DeliverPrefix: ss.ExternalDeliverPrefix, } } for _, transform := range ss.SubjectTransforms { jss.SubjectTransforms = append(jss.SubjectTransforms, jetstream.SubjectTransformConfig{ Source: transform.Source, Destination: transform.Dest, }) } return jss, nil } func mapJSMStreamSource(ss *api.StreamSource) (*jsmapi.StreamSource, error) { jss := &jsmapi.StreamSource{ Name: ss.Name, FilterSubject: ss.FilterSubject, } if ss.OptStartSeq > 0 { jss.OptStartSeq = uint64(ss.OptStartSeq) } if ss.OptStartTime != "" { t, err := time.Parse(time.RFC3339, ss.OptStartTime) if err != nil { return nil, fmt.Errorf("parse opt start time: %w", err) } jss.OptStartTime = &t } if ss.ExternalAPIPrefix != "" || ss.ExternalDeliverPrefix != "" { jss.External = &jsmapi.ExternalStream{ ApiPrefix: ss.ExternalAPIPrefix, DeliverPrefix: ss.ExternalDeliverPrefix, } } for _, transform := range ss.SubjectTransforms { jss.SubjectTransforms = append(jss.SubjectTransforms, jsmapi.SubjectTransformConfig{ Source: transform.Source, Destination: transform.Dest, }) } return jss, nil } // SetupWithManager sets up the controller with the Manager. func (r *StreamReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.Stream{}). WithEventFilter(predicate.GenerationChangedPredicate{}). WithOptions(controller.Options{ MaxConcurrentReconciles: 1, }). Complete(r) } ================================================ FILE: internal/controller/stream_controller_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "os" "testing" "time" jsmapi "github.com/nats-io/jsm.go/api" natsserver "github.com/nats-io/nats-server/v2/test" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) var _ = Describe("Stream Controller", func() { // The test stream resource const resourceName = "test-stream" const streamName = "orders" const alternateResource = "alternate-stream" const alternateNamespace = "alternate-namespace" typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", } stream := &api.Stream{} // The tested controller var controller *StreamReconciler // Config to create minimal nats stream emptyStreamConfig := jetstream.StreamConfig{ Name: streamName, Replicas: 1, Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardOld, Storage: jetstream.FileStorage, } BeforeEach(func(ctx SpecContext) { By("creating a test stream resource") err := k8sClient.Get(ctx, typeNamespacedName, stream) if err != nil && k8serrors.IsNotFound(err) { resource := &api.Stream{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: api.StreamSpec{ Name: streamName, Replicas: 1, Subjects: []string{"tests.*"}, Description: "test stream", Retention: "workqueue", Discard: "old", Storage: "file", }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) // Re-fetch stream Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) } By("checking precondition: nats stream does not exist") _, err = jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) By("setting up the tested controller") controller = &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: baseController, } }) AfterEach(func(ctx SpecContext) { By("removing the test stream resource") resource := &api.Stream{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err != nil { Expect(err).To(MatchError(k8serrors.IsNotFound, "Is not found")) } else { if controllerutil.ContainsFinalizer(resource, streamFinalizer) { By("removing the finalizer") controllerutil.RemoveFinalizer(resource, streamFinalizer) Expect(k8sClient.Update(ctx, resource)).To(Succeed()) } By("removing the stream resource") Expect(k8sClient.Delete(ctx, resource)). To(SatisfyAny( Succeed(), MatchError(k8serrors.IsNotFound, "is not found"), )) } By("deleting the nats stream") Expect(jsClient.DeleteStream(ctx, streamName)). To(SatisfyAny( Succeed(), MatchError(jetstream.ErrStreamNotFound), )) }) When("reconciling a not existing resource", func() { It("should stop reconciliation without error", func(ctx SpecContext) { By("reconciling the created resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: "fake", Name: "not-existing", }, }) Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) }) }) When("reconciling a not initialized resource", func() { It("should initialize a new resource", func(ctx SpecContext) { By("re-queueing until it is initialized") // Initialization can require multiple reconciliation loops Eventually(func(ctx SpecContext) *api.Stream { _, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) got := &api.Stream{} Expect(k8sClient.Get(ctx, typeNamespacedName, got)).To(Succeed()) return got }).WithContext(ctx). Should(SatisfyAll( HaveField("Finalizers", HaveExactElements(streamFinalizer)), HaveField("Status.Conditions", Not(BeEmpty())), )) By("validating the ready condition") // Fetch stream Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) Expect(stream.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(stream.Status.Conditions[0], v1.ConditionUnknown, stateReconciling, "Starting reconciliation", time.Now()) }) }) When("reconciling a resource in a different namespace", func() { BeforeEach(func(ctx SpecContext) { By("creating a stream resource in an alternate namespace while namespaced") alternateNamespaceResource := &api.Stream{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, Spec: api.StreamSpec{ Name: alternateResource, Replicas: 1, Subjects: []string{"alternate.*"}, Description: "stream in alternate namespace", Retention: "workqueue", Discard: "old", Storage: "file", }, } // Create the namespace if it doesn't exist ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: alternateNamespace, }, } err := k8sClient.Create(ctx, ns) if err != nil && !k8serrors.IsAlreadyExists(err) { Expect(err).NotTo(HaveOccurred()) } // Create the stream in the other namespace Expect(k8sClient.Create(ctx, alternateNamespaceResource)).To(Succeed()) }) AfterEach(func(ctx SpecContext) { By("cleaning up the resource in alternate namespace") alternateStream := &api.Stream{ ObjectMeta: metav1.ObjectMeta{ Name: alternateResource, Namespace: alternateNamespace, }, } err := k8sClient.Delete(ctx, alternateStream) if err != nil && !k8serrors.IsNotFound(err) { Expect(err).NotTo(HaveOccurred()) } }) It("should not watch the resource in alternate namespace", func(ctx SpecContext) { By("reconciling with no explicit namespace restriction") alternateNamespacedName := types.NamespacedName{ Namespace: alternateNamespace, Name: alternateResource, } By("running reconciliation for the resource in alternate namespace") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: alternateNamespacedName, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(ctrl.Result{})) By("checking the stream doesn't exist in NATS") _, err = jsClient.Stream(ctx, alternateResource) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) By("verifying the resource still exists in the alternate namespace") alternateStream := &api.Stream{} Expect(k8sClient.Get(ctx, alternateNamespacedName, alternateStream)).To(Succeed()) By("checking no conditions were set on the resource") Expect(alternateStream.Status.Conditions).To(BeEmpty()) }) It("should watch the resource in alternate namespace when not namespaced", func(ctx SpecContext) { By("reconciling with a non-namespaced controller") testNatsConfig := &NatsConfig{ServerURL: clientUrl} alternateBaseController, err := NewJSController(k8sClient, testNatsConfig, &Config{}) Expect(err).NotTo(HaveOccurred()) alternateController := &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: alternateBaseController, } resourceNames := []types.NamespacedName{ typeNamespacedName, { Namespace: alternateNamespace, Name: alternateResource, }, } By("running reconciliation for the resources in all namespaces") for _, n := range resourceNames { result, err := alternateController.Reconcile(ctx, reconcile.Request{ NamespacedName: n, }) By("verifying reconciliation completes without error") Expect(err).NotTo(HaveOccurred()) Expect(result).NotTo(Equal(ctrl.Result{})) } }) }) When("reconciling an initialized resource", func() { BeforeEach(func(ctx SpecContext) { By("initializing the stream resource") By("setting the finalizer") Expect(controllerutil.AddFinalizer(stream, streamFinalizer)).To(BeTrue()) Expect(k8sClient.Update(ctx, stream)).To(Succeed()) By("setting an unknown ready state") stream.Status.Conditions = []api.Condition{{ Type: readyCondType, Status: v1.ConditionUnknown, Reason: "Test", Message: "start condition", LastTransitionTime: time.Now().Format(time.RFC3339Nano), }} Expect(k8sClient.Status().Update(ctx, stream)).To(Succeed()) }) It("should create a new stream", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) By("checking if the ready state was updated") Expect(stream.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches(stream.Status.Conditions[0], v1.ConditionTrue, stateReady, "created or updated", time.Now()) By("checking if the observed generation matches") Expect(stream.Status.ObservedGeneration).To(Equal(stream.Generation)) By("checking if the stream was created") natsStream, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) streamInfo, err := natsStream.Info(ctx) Expect(err).NotTo(HaveOccurred()) Expect(streamInfo.Config.Name).To(Equal(streamName)) Expect(streamInfo.Config.Description).To(Equal("test stream")) Expect(streamInfo.Created).To(BeTemporally("~", time.Now(), time.Second)) }) When("sealed is true", func() { BeforeEach(func(ctx SpecContext) { By("setting sealed to true") stream.Spec.Name = "sealed" stream.Spec.Sealed = true Expect(k8sClient.Update(ctx, stream)).To(Succeed()) }) It("should not create the stream", func(ctx SpecContext) { _, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err.Error()).To(HaveSuffix("can not be sealed (10052)")) }) }) When("PreventUpdate is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") stream.Spec.PreventUpdate = true Expect(k8sClient.Update(ctx, stream)).To(Succeed()) }) It("should create the stream", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that stream was created") _, err = jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) }) It("should not update the stream", func(ctx SpecContext) { By("creating the stream") _, err := jsClient.CreateStream(ctx, emptyStreamConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that stream was not updated") s, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) Expect(s.CachedInfo().Config.Description).To(BeEmpty()) }) }) When("read-only mode is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should not create the stream", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no stream was created") _, err = jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) }) It("should not update the stream", func(ctx SpecContext) { By("creating the stream") _, err := jsClient.CreateStream(ctx, emptyStreamConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that stream was not updated") s, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) Expect(s.CachedInfo().Config.Description).To(BeEmpty()) }) }) When("namespace restriction is enabled", func() { BeforeEach(func(ctx SpecContext) { By("setting a namespace on the resource") namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not create the stream", func(ctx SpecContext) { By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that no stream was created") _, err = jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) }) It("should not update the stream", func(ctx SpecContext) { By("creating the stream") _, err := jsClient.CreateStream(ctx, emptyStreamConfig) Expect(err).NotTo(HaveOccurred()) By("running Reconcile") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that stream was not updated") s, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) Expect(s.CachedInfo().Config.Description).To(BeEmpty()) }) }) It("should update an existing stream", func(ctx SpecContext) { By("reconciling once to create the stream") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) previousTransitionTime := stream.Status.Conditions[0].LastTransitionTime By("updating the resource") stream.Spec.Description = "new description" stream.Spec.Sealed = true Expect(k8sClient.Update(ctx, stream)).To(Succeed()) By("reconciling the updated resource") result, err = controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) // Fetch resource Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) By("checking if the state transition time was not updated") Expect(stream.Status.Conditions).To(HaveLen(1)) Expect(stream.Status.Conditions[0].LastTransitionTime).To(Equal(previousTransitionTime)) By("checking if the observed generation matches") Expect(stream.Status.ObservedGeneration).To(Equal(stream.Generation)) By("checking if the stream was updated") natsStream, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) streamInfo, err := natsStream.Info(ctx) Expect(err).NotTo(HaveOccurred()) Expect(streamInfo.Config.Description).To(Equal("new description")) Expect(streamInfo.Config.Sealed).To(BeTrue()) // Other fields unchanged Expect(streamInfo.Config.Subjects).To(Equal([]string{"tests.*"})) }) It("should create stream with new feature flags on the server", func(ctx SpecContext) { By("updating the stream spec with new feature flags") err := k8sClient.Get(ctx, typeNamespacedName, stream) Expect(err).NotTo(HaveOccurred()) stream.Spec.AllowMsgCounter = true stream.Spec.AllowAtomicPublish = true stream.Spec.AllowMsgSchedules = true stream.Spec.AllowRollup = true stream.Spec.Retention = "limits" // Note: PersistMode "async" is not compatible with AllowAtomicPublish Expect(k8sClient.Update(ctx, stream)).To(Succeed()) By("reconciling the updated resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("fetching the updated stream from NATS") natsStream, err := jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) streamInfo, err := natsStream.Info(ctx) Expect(err).NotTo(HaveOccurred()) By("verifying new feature flags are set on server") Expect(streamInfo.Config.AllowMsgCounter).To(BeTrue()) Expect(streamInfo.Config.AllowAtomicPublish).To(BeTrue()) Expect(streamInfo.Config.AllowMsgSchedules).To(BeTrue()) }) It("should set an error state when the nats server is not available", func(ctx SpecContext) { By("setting up controller with unavailable nats server") // Setup client for not running server // Use actual test server to ensure port not used by other service on test instance sv := CreateTestServer() disconnectedController, err := NewJSController(k8sClient, &NatsConfig{ServerURL: sv.ClientURL()}, &Config{}) Expect(err).NotTo(HaveOccurred()) sv.Shutdown() controller := &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: disconnectedController, } By("reconciling resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(result).To(Equal(ctrl.Result{})) Expect(err).To(HaveOccurred()) // Will be re-queued with back-off // Fetch resource err = k8sClient.Get(ctx, typeNamespacedName, stream) Expect(err).NotTo(HaveOccurred()) By("checking if the status was updated") Expect(stream.Status.Conditions).To(HaveLen(1)) assertReadyStateMatches( stream.Status.Conditions[0], v1.ConditionFalse, stateErrored, "create or update stream:", time.Now(), ) By("checking if the observed generation does not match") Expect(stream.Status.ObservedGeneration).ToNot(Equal(stream.Generation)) }) When("the resource is marked for deletion", func() { BeforeEach(func(ctx SpecContext) { By("marking the resource for deletion") Expect(k8sClient.Delete(ctx, stream)).To(Succeed()) Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) // re-fetch after update }) It("should succeed deleting a not existing stream", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, stream). ShouldNot(Succeed()) }) When("the underlying stream exists", func() { BeforeEach(func(ctx SpecContext) { By("creating the stream on the nats server") _, err := jsClient.CreateStream(ctx, emptyStreamConfig) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func(ctx SpecContext) { err := jsClient.DeleteStream(ctx, streamName) if err != nil { Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) } }) It("should delete the stream", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the stream is deleted") _, err = jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, stream). ShouldNot(Succeed()) }) When("PreventDelete is set", func() { BeforeEach(func(ctx SpecContext) { By("setting preventDelete on the resource") stream.Spec.PreventDelete = true Expect(k8sClient.Update(ctx, stream)).To(Succeed()) }) It("Should delete the resource and not delete the nats stream", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the stream is not deleted") _, err = jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, stream). ShouldNot(Succeed()) }) }) When("read only is set", func() { BeforeEach(func(ctx SpecContext) { By("setting read only on the controller") readOnly, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{ReadOnly: true}) Expect(err).NotTo(HaveOccurred()) controller = &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: readOnly, } }) It("should delete the resource and not delete the stream", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the stream is not deleted") _, err = jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) By("checking that the resource is deleted") Eventually(k8sClient.Get). WithArguments(ctx, typeNamespacedName, stream). ShouldNot(Succeed()) }) }) When("controller is restricted to different namespace", func() { BeforeEach(func(ctx SpecContext) { namespaced, err := NewJSController(k8sClient, &NatsConfig{ServerURL: clientUrl}, &Config{Namespace: alternateNamespace}) Expect(err).NotTo(HaveOccurred()) controller = &StreamReconciler{ Scheme: k8sClient.Scheme(), JetStreamController: namespaced, } }) It("should not delete the resource and stream", func(ctx SpecContext) { By("reconciling") result, err := controller.Reconcile(ctx, ctrl.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) By("checking that the stream is not deleted") _, err = jsClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) By("checking that the finalizer is not removed") Expect(k8sClient.Get(ctx, typeNamespacedName, stream)).To(Succeed()) Expect(stream.Finalizers).To(ContainElement(streamFinalizer)) }) }) }) }) It("should update stream on different server as specified in spec", func(ctx SpecContext) { By("setting up the alternative server") // Setup altClient for alternate server altServer := CreateTestServer() defer altServer.Shutdown() By("setting the server in the stream spec") stream.Spec.Servers = []string{altServer.ClientURL()} Expect(k8sClient.Update(ctx, stream)).To(Succeed()) By("checking precondition, that the stream does not yet exist") _, err := jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) By("reconciling the resource") result, err := controller.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) Expect(result.IsZero()).To(BeTrue()) connPool := newConnPool(0) conn, err := connPool.Get(&NatsConfig{ServerURL: altServer.ClientURL()}, true) Expect(err).NotTo(HaveOccurred()) domain := "" By("checking if the stream was created on the alternative server") altClient, err := CreateJetStreamClient(conn, true, domain) defer conn.Close() Expect(err).NotTo(HaveOccurred()) got, err := altClient.Stream(ctx, streamName) Expect(err).NotTo(HaveOccurred()) Expect(got.CachedInfo().Created).To(BeTemporally("~", time.Now(), time.Second)) By("checking that the stream was NOT created on the original server") _, err = jsClient.Stream(ctx, streamName) Expect(err).To(MatchError(jetstream.ErrStreamNotFound)) }) }) }) func Test_mapSpecToConfig(t *testing.T) { date := time.Date(2024, 12, 3, 16, 55, 5, 0, time.UTC) dateString := date.Format(time.RFC3339) tests := []struct { name string spec *api.StreamSpec want jsmapi.StreamConfig wantErr bool }{ { name: "empty spec", spec: &api.StreamSpec{}, want: jsmapi.StreamConfig{ // Placement will be nil when no current config is provided }, wantErr: false, }, { name: "full spec", spec: &api.StreamSpec{ AllowDirect: true, AllowRollup: true, DenyDelete: true, DenyPurge: true, Description: "stream description", DiscardPerSubject: true, Discard: "new", DuplicateWindow: "5s", MaxAge: "30s", MaxBytes: -1, MaxConsumers: -1, MaxMsgs: -1, MaxMsgSize: -1, MaxMsgsPerSubject: 10, Mirror: &api.StreamSource{ Name: "mirror", OptStartSeq: 5, OptStartTime: dateString, FilterSubject: "orders", ExternalAPIPrefix: "api", ExternalDeliverPrefix: "deliver", SubjectTransforms: []*api.SubjectTransform{{ Source: "transform-source", Dest: "transform-dest", }}, }, NoAck: true, Placement: &api.StreamPlacement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Replicas: 3, RePublish: &api.RePublish{ Source: "re-publish-source", Destination: "re-publish-dest", HeadersOnly: true, }, SubjectTransform: &api.SubjectTransform{ Source: "transform-source", Dest: "transform-dest", }, FirstSequence: 42, Compression: "s2", Metadata: map[string]string{ "meta": "data", }, Retention: "interest", Sources: []*api.StreamSource{{ Name: "source", OptStartSeq: 5, OptStartTime: dateString, FilterSubject: "orders", ExternalAPIPrefix: "api", ExternalDeliverPrefix: "deliver", SubjectTransforms: []*api.SubjectTransform{{ Source: "transform-source", Dest: "transform-dest", }}, }}, Storage: "file", Subjects: []string{"orders.*"}, AllowMsgTTL: true, SubjectDeleteMarkerTTL: "10s", AllowMsgCounter: true, AllowAtomicPublish: true, AllowMsgSchedules: true, PersistMode: "async", BaseStreamConfig: api.BaseStreamConfig{ PreventDelete: false, PreventUpdate: false, ConnectionOpts: api.ConnectionOpts{ Account: "", Creds: "", Nkey: "", Servers: nil, TLS: &api.TLS{}, }, }, }, want: jsmapi.StreamConfig{ Description: "stream description", Subjects: []string{"orders.*"}, Retention: jsmapi.InterestPolicy, MaxConsumers: -1, MaxMsgs: -1, MaxBytes: -1, Discard: jsmapi.DiscardNew, DiscardNewPer: true, MaxAge: time.Second * 30, MaxMsgsPer: 10, MaxMsgSize: -1, Storage: jsmapi.FileStorage, Replicas: 3, NoAck: true, Duplicates: time.Second * 5, Placement: &jsmapi.Placement{ Cluster: "test-cluster", Tags: []string{"tag"}, }, Mirror: &jsmapi.StreamSource{ Name: "mirror", OptStartSeq: 5, OptStartTime: &date, FilterSubject: "orders", SubjectTransforms: []jsmapi.SubjectTransformConfig{{ Source: "transform-source", Destination: "transform-dest", }}, External: &jsmapi.ExternalStream{ ApiPrefix: "api", DeliverPrefix: "deliver", }, }, Sources: []*jsmapi.StreamSource{{ Name: "source", OptStartSeq: 5, OptStartTime: &date, FilterSubject: "orders", SubjectTransforms: []jsmapi.SubjectTransformConfig{{ Source: "transform-source", Destination: "transform-dest", }}, External: &jsmapi.ExternalStream{ ApiPrefix: "api", DeliverPrefix: "deliver", }, }}, Sealed: false, DenyDelete: true, DenyPurge: true, RollupAllowed: true, Compression: jsmapi.S2Compression, FirstSeq: 42, SubjectTransform: &jsmapi.SubjectTransformConfig{ Source: "transform-source", Destination: "transform-dest", }, RePublish: &jsmapi.RePublish{ Source: "re-publish-source", Destination: "re-publish-dest", HeadersOnly: true, }, AllowDirect: true, MirrorDirect: false, ConsumerLimits: jsmapi.StreamConsumerLimits{}, AllowMsgTTL: true, SubjectDeleteMarkerTTL: time.Second * 10, AllowMsgCounter: true, AllowAtomicPublish: true, AllowMsgSchedules: true, PersistMode: jsmapi.AsyncPersistMode, Metadata: map[string]string{ "meta": "data", }, Template: "", }, wantErr: false, }, { name: "persist mode default", spec: &api.StreamSpec{ PersistMode: "default", AllowMsgCounter: false, AllowAtomicPublish: false, AllowMsgSchedules: false, }, want: jsmapi.StreamConfig{ PersistMode: jsmapi.DefaultPersistMode, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) sOpts, err := streamSpecToConfig(tt.spec, nil) if (err != nil) != tt.wantErr { t.Errorf("streamSpecToConfig() error = %v, wantErr %v", err, tt.wantErr) return } got := &jsmapi.StreamConfig{} for _, o := range sOpts { o(got) } // Compare nested structs assert.EqualValues(tt.want, *got) }) } } // TestStreamUpdateWithoutPlacement verifies that updating replicas doesn't // inadvertently set an empty Placement field, which would cause error 10123: // "can not move and scale a stream in a single update" // This test reproduces issue #273 where enabling control loop resulted in failures // when updating streams without placement configuration. func TestStreamUpdateWithoutPlacement(t *testing.T) { // First test the config conversion logic t.Run("config conversion", func(t *testing.T) { tests := []struct { name string spec *api.StreamSpec currentConfig *jsmapi.StreamConfig expectPlacement bool expectedCluster string }{ { name: "spec without placement, no current placement - should not set placement", spec: &api.StreamSpec{ Name: "test-stream", Subjects: []string{"test.>"}, Storage: "file", Replicas: 3, }, currentConfig: nil, expectPlacement: false, }, { name: "spec without placement, current has placement - should clear placement", spec: &api.StreamSpec{ Name: "test-stream", Subjects: []string{"test.>"}, Storage: "file", Replicas: 3, }, currentConfig: &jsmapi.StreamConfig{ Placement: &jsmapi.Placement{ Cluster: "old-cluster", }, }, expectPlacement: true, expectedCluster: "", // Should be cleared }, { name: "spec with placement - should set placement", spec: &api.StreamSpec{ Name: "test-stream", Subjects: []string{"test.>"}, Storage: "file", Replicas: 3, Placement: &api.StreamPlacement{ Cluster: "test-cluster", Tags: []string{"tag1", "tag2"}, }, }, currentConfig: nil, expectPlacement: true, expectedCluster: "test-cluster", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) // Convert spec to config options with context opts, err := streamSpecToConfig(tt.spec, tt.currentConfig) assert.NoError(err) // Apply options to get the resulting config cfg := &jsmapi.StreamConfig{} for _, opt := range opts { opt(cfg) } // Verify placement is only set when expected if tt.expectPlacement { assert.NotNil(cfg.Placement, "Placement should be set") assert.Equal(tt.expectedCluster, cfg.Placement.Cluster) if tt.spec.Placement != nil && tt.spec.Placement.Tags != nil { assert.Equal(tt.spec.Placement.Tags, cfg.Placement.Tags) } } else { assert.Nil(cfg.Placement, "Placement should be nil") } }) } }) // Integration test that reproduces the actual NATS error scenario from issue #273 t.Run("scale replicas without placement error", func(t *testing.T) { // Create test NATS server without using Gomega opts := &natsserver.DefaultTestOptions opts.JetStream = true opts.Port = -1 opts.Debug = true dir, err := os.MkdirTemp("", "nats-*") assert.NoError(t, err) defer os.RemoveAll(dir) opts.StoreDir = dir srv := natsserver.RunServer(opts) assert.NotNil(t, srv) defer srv.Shutdown() // Create NATS connection and JetStream client nc, err := nats.Connect(srv.ClientURL()) assert.NoError(t, err) defer nc.Close() js, err := nc.JetStream() assert.NoError(t, err) // Create initial stream with 1 replica and no placement streamName := "test-scale-stream" initialConfig := &nats.StreamConfig{ Name: streamName, Subjects: []string{"test.>"}, Storage: nats.FileStorage, Replicas: 1, // Explicitly no Placement field set } _, err = js.AddStream(initialConfig) assert.NoError(t, err, "Failed to create initial stream") // Now attempt to scale replicas from 1 to 3 without setting placement // This should NOT fail with error 10123 updateConfig := &nats.StreamConfig{ Name: streamName, Subjects: []string{"test.>"}, Storage: nats.FileStorage, Replicas: 3, // Still no Placement field - this is the critical test } _, err = js.UpdateStream(updateConfig) assert.NoError(t, err, "Scaling replicas should not fail with error 10123") // Verify the stream was updated successfully info, err := js.StreamInfo(streamName) assert.NoError(t, err) assert.Equal(t, 3, info.Config.Replicas, "Replicas should be scaled to 3") }) } ================================================ FILE: internal/controller/suite_test.go ================================================ /* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "fmt" "os" "path/filepath" "runtime" "testing" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go/jetstream" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" api "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( cfg *rest.Config k8sClient client.Client testEnv *envtest.Environment testServer *server.Server clientUrl string jsClient jetstream.JetStream baseController JetStreamController ) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "deploy")}, ErrorIfCRDPathMissing: true, // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. // Note that you must have the required binaries setup under the bin directory to perform // the tests directly. When we run make test it will be setup and used automatically. BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", fmt.Sprintf("1.32.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) err = api.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) By("bootstrapping the test server") testServer = CreateTestServer() Expect(err).NotTo(HaveOccurred()) clientUrl = testServer.ClientURL() testNatsConfig := &NatsConfig{ServerURL: clientUrl} baseController, err = NewJSController(k8sClient, testNatsConfig, &Config{ Namespace: "default", }) Expect(err).NotTo(HaveOccurred()) connPool := newConnPool(0) conn, err := connPool.Get(testNatsConfig, true) Expect(err).NotTo(HaveOccurred()) domain := "" jsClient, err = CreateJetStreamClient(conn, true, domain) Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) By("tearing down the test server") storeDir := testServer.StoreDir() testServer.Shutdown() err = os.RemoveAll(storeDir) Expect(err).NotTo(HaveOccurred()) }) ================================================ FILE: internal/controller/types.go ================================================ package controller const ( readyCondType = "Ready" accountFinalizer = "account.nats.io/finalizer" streamFinalizer = "stream.nats.io/finalizer" keyValueFinalizer = "kv.nats.io/finalizer" objectStoreFinalizer = "objectstore.nats.io/finalizer" consumerFinalizer = "consumer.nats.io/finalizer" stateAnnotationConsumer = "consumer.nats.io/state" stateAnnotationKV = "kv.nats.io/state" stateAnnotationObj = "objectstore.nats.io/state" stateAnnotationStream = "stream.nats.io/state" stateReady = "Ready" stateReconciling = "Reconciling" stateErrored = "Errored" stateFinalizing = "Finalizing" ) ================================================ FILE: kuttl-test.yaml ================================================ apiVersion: kuttl.dev/v1beta1 kind: TestSuite testDirs: - tests/ commands: - command: kubectl apply -f ./deploy/crds.yml timeout: 120 ================================================ FILE: pkg/bootconfig/bootconfig.go ================================================ // Copyright 2018 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bootconfig import ( "context" "errors" "fmt" "os" log "github.com/sirupsen/logrus" k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sclient "k8s.io/client-go/kubernetes" k8srestapi "k8s.io/client-go/rest" k8sclientcmd "k8s.io/client-go/tools/clientcmd" ) type Options struct { // TargetTag is the tag that will be looked up to find // the public ip from the node. TargetTag string // ClientAdvertiseFileName is the name of the file where // the advertise configuration will be written into. ClientAdvertiseFileName string // GatewayAdvertiseFileName is the name of the file where // the advertise configuration will be written into for gateways. GatewayAdvertiseFileName string // NoSignals marks whether to enable the signal handler. NoSignals bool } // Controller for the boot config. type Controller struct { // Start/Stop cancellation. quit func() // Client to interact with Kubernetes resources. kc k8sclient.Interface // opts is the set of options. opts *Options } // NewController creates a new controller with the configuration. func NewController(opts *Options) *Controller { return &Controller{ opts: opts, } } // SetupClients takes the configuration and prepares the rest // clients that will be used to interact with the cluster objects. func (c *Controller) SetupClients(cfg *k8srestapi.Config) error { kc, err := k8sclient.NewForConfig(cfg) if err != nil { return err } c.kc = kc return nil } // Run starts the controller. func (c *Controller) Run(ctx context.Context) error { var err error var cfg *k8srestapi.Config if kubeconfig := os.Getenv("KUBERNETES_CONFIG_FILE"); kubeconfig != "" { cfg, err = k8sclientcmd.BuildConfigFromFlags("", kubeconfig) } else { cfg, err = k8srestapi.InClusterConfig() } if err != nil { return err } if err := c.SetupClients(cfg); err != nil { return err } nodeName := os.Getenv("KUBERNETES_NODE_NAME") if nodeName == "" { return errors.New("Target node name is missing") } log.Infof("Pod running on node %q", nodeName) node, err := c.kc.CoreV1().Nodes().Get(ctx, nodeName, k8smetav1.GetOptions{}) if err != nil { return err } var ok bool var externalAddress string for _, addr := range node.Status.Addresses { if addr.Type == "ExternalIP" { externalAddress = addr.Address ok = true break } } // Fallback to use a label to find the external address. if !ok { externalAddress, ok = node.Labels[c.opts.TargetTag] if !ok || len(externalAddress) == 0 { return errors.New("Could not find external IP address.") } } log.Infof("Pod is running on node with external IP: %s", externalAddress) clientAdvertiseConfig := fmt.Sprintf("\nclient_advertise = \"%s\"\n\n", externalAddress) err = os.WriteFile(c.opts.ClientAdvertiseFileName, []byte(clientAdvertiseConfig), 0o644) if err != nil { return fmt.Errorf("Could not write client advertise config: %s", err) } log.Infof("Successfully wrote client advertise config to %q", c.opts.ClientAdvertiseFileName) gatewayAdvertiseConfig := fmt.Sprintf("\nadvertise = \"%s\"\n\n", externalAddress) err = os.WriteFile(c.opts.GatewayAdvertiseFileName, []byte(gatewayAdvertiseConfig), 0o644) if err != nil { return fmt.Errorf("Could not write gateway advertise config: %s", err) } log.Infof("Successfully wrote gateway advertise config to %q", c.opts.GatewayAdvertiseFileName) return nil } ================================================ FILE: pkg/jetstream/apis/jetstream/register.go ================================================ package jetstream // GroupName is the group name used in this package const ( GroupName = "jetstream.nats.io" ) ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/consumertypes.go ================================================ package v1beta1 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Consumer is a specification for a Consumer resource type Consumer struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec ConsumerSpec `json:"spec"` Status Status `json:"status"` } func (c *Consumer) GetSpec() interface{} { return c.Spec } // ConsumerSpec is the spec for a Consumer resource type ConsumerSpec struct { AckPolicy string `json:"ackPolicy"` AckWait string `json:"ackWait"` DeliverGroup string `json:"deliverGroup"` DeliverPolicy string `json:"deliverPolicy"` DeliverSubject string `json:"deliverSubject"` Description string `json:"description"` DurableName string `json:"durableName"` FilterSubject string `json:"filterSubject"` FlowControl bool `json:"flowControl"` HeartbeatInterval string `json:"heartbeatInterval"` MaxAckPending int `json:"maxAckPending"` MaxDeliver int `json:"maxDeliver"` OptStartSeq int `json:"optStartSeq"` OptStartTime string `json:"optStartTime"` RateLimitBps int `json:"rateLimitBps"` ReplayPolicy string `json:"replayPolicy"` SampleFreq string `json:"sampleFreq"` StreamName string `json:"streamName"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ConsumerList is a list of Consumer resources type ConsumerList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []Consumer `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/doc.go ================================================ // +k8s:deepcopy-gen=package // +groupName=jetstream.nats.io // Package v1 is the v1 version of the API. package v1beta1 ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/register.go ================================================ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/nats-io/nack/pkg/jetstream/apis/jetstream" ) var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: jetstream.GroupName, Version: "v1beta1"} // SchemeBuilder initializes a scheme builder SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Stream{}, &StreamList{}, &Consumer{}, &ConsumerList{}, &StreamTemplate{}, &StreamTemplateList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/streamtemplatetypes.go ================================================ package v1beta1 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // StreamTemplate is a specification for a StreamTemplate resource type StreamTemplate struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec StreamTemplateSpec `json:"spec"` Status Status `json:"status"` } func (s *StreamTemplate) GetSpec() interface{} { return s.Spec } // StreamTemplateSpec is the spec for a StreamTemplate resource type StreamTemplateSpec struct { StreamSpec `json:",inline"` MaxStreams int `json:"maxStreams"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // StreamTemplateList is a list of StreamTemplate resources type StreamTemplateList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []StreamTemplate `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/streamtypes.go ================================================ package v1beta1 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Stream is a specification for a Stream resource type Stream struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec StreamSpec `json:"spec"` Status Status `json:"status"` } func (s *Stream) GetSpec() interface{} { return s.Spec } // StreamSpec is the spec for a Stream resource type StreamSpec struct { Description string `json:"description"` Discard string `json:"discard"` DuplicateWindow string `json:"duplicateWindow"` MaxAge string `json:"maxAge"` MaxBytes int `json:"maxBytes"` MaxConsumers int `json:"maxConsumers"` MaxMsgs int `json:"maxMsgs"` MaxMsgSize int `json:"maxMsgSize"` MaxMsgsPerSubject int `json:"maxMsgsPerSubject"` Mirror *StreamSource `json:"mirror"` Name string `json:"name"` NoAck bool `json:"noAck"` Placement *StreamPlacement `json:"placement"` Replicas int `json:"replicas"` Retention string `json:"retention"` Sources []*StreamSource `json:"sources"` Storage string `json:"storage"` Subjects []string `json:"subjects"` AllowMsgTTL bool `json:"allowMsgTtl"` SubjectDeleteMarkerTTL string `json:"subjectDeleteMarkerTtl"` } type StreamPlacement struct { Cluster string `json:"cluster"` Tags []string `json:"tags"` } type StreamSource struct { Name string `json:"name"` OptStartSeq int `json:"optStartSeq"` OptStartTime string `json:"optStartTime"` FilterSubject string `json:"filterSubject"` ExternalAPIPrefix string `json:"externalApiPrefix"` ExternalDeliverPrefix string `json:"externalDeliverPrefix"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // StreamList is a list of Stream resources type StreamList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []Stream `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/types.go ================================================ package v1beta1 import ( k8sapi "k8s.io/api/core/v1" ) type CredentialsSecret struct { Name string `json:"name"` Key string `json:"key"` } type Status struct { ObservedGeneration int64 `json:"observedGeneration"` Conditions []Condition `json:"conditions"` } type Condition struct { Type string `json:"type"` Status k8sapi.ConditionStatus `json:"status"` Reason string `json:"reason"` Message string `json:"message"` LastTransitionTime string `json:"lastTransitionTime"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by deepcopy-gen. DO NOT EDIT. package v1beta1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. func (in *Condition) DeepCopy() *Condition { if in == nil { return nil } out := new(Condition) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Consumer) DeepCopyInto(out *Consumer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Consumer. func (in *Consumer) DeepCopy() *Consumer { if in == nil { return nil } out := new(Consumer) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Consumer) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsumerList) DeepCopyInto(out *ConsumerList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Consumer, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsumerList. func (in *ConsumerList) DeepCopy() *ConsumerList { if in == nil { return nil } out := new(ConsumerList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ConsumerList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsumerSpec) DeepCopyInto(out *ConsumerSpec) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsumerSpec. func (in *ConsumerSpec) DeepCopy() *ConsumerSpec { if in == nil { return nil } out := new(ConsumerSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialsSecret) DeepCopyInto(out *CredentialsSecret) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialsSecret. func (in *CredentialsSecret) DeepCopy() *CredentialsSecret { if in == nil { return nil } out := new(CredentialsSecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Status) DeepCopyInto(out *Status) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]Condition, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. func (in *Status) DeepCopy() *Status { if in == nil { return nil } out := new(Status) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Stream) DeepCopyInto(out *Stream) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stream. func (in *Stream) DeepCopy() *Stream { if in == nil { return nil } out := new(Stream) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Stream) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamList) DeepCopyInto(out *StreamList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Stream, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamList. func (in *StreamList) DeepCopy() *StreamList { if in == nil { return nil } out := new(StreamList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *StreamList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamPlacement) DeepCopyInto(out *StreamPlacement) { *out = *in if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make([]string, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamPlacement. func (in *StreamPlacement) DeepCopy() *StreamPlacement { if in == nil { return nil } out := new(StreamPlacement) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamSource) DeepCopyInto(out *StreamSource) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamSource. func (in *StreamSource) DeepCopy() *StreamSource { if in == nil { return nil } out := new(StreamSource) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamSpec) DeepCopyInto(out *StreamSpec) { *out = *in if in.Mirror != nil { in, out := &in.Mirror, &out.Mirror *out = new(StreamSource) **out = **in } if in.Placement != nil { in, out := &in.Placement, &out.Placement *out = new(StreamPlacement) (*in).DeepCopyInto(*out) } if in.Sources != nil { in, out := &in.Sources, &out.Sources *out = make([]*StreamSource, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(StreamSource) **out = **in } } } if in.Subjects != nil { in, out := &in.Subjects, &out.Subjects *out = make([]string, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamSpec. func (in *StreamSpec) DeepCopy() *StreamSpec { if in == nil { return nil } out := new(StreamSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamTemplate) DeepCopyInto(out *StreamTemplate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamTemplate. func (in *StreamTemplate) DeepCopy() *StreamTemplate { if in == nil { return nil } out := new(StreamTemplate) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *StreamTemplate) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamTemplateList) DeepCopyInto(out *StreamTemplateList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]StreamTemplate, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamTemplateList. func (in *StreamTemplateList) DeepCopy() *StreamTemplateList { if in == nil { return nil } out := new(StreamTemplateList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *StreamTemplateList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamTemplateSpec) DeepCopyInto(out *StreamTemplateSpec) { *out = *in in.StreamSpec.DeepCopyInto(&out.StreamSpec) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamTemplateSpec. func (in *StreamTemplateSpec) DeepCopy() *StreamTemplateSpec { if in == nil { return nil } out := new(StreamTemplateSpec) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/accounttypes.go ================================================ package v1beta2 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Account is a specification for a Account resource. type Account struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec AccountSpec `json:"spec"` Status Status `json:"status"` } func (c *Account) GetSpec() interface{} { return c.Spec } // AccountSpec is the spec for a Account resource type AccountSpec struct { Servers []string `json:"servers,omitempty"` TLS *TLSSecret `json:"tls,omitempty"` Creds *CredsSecret `json:"creds,omitempty"` NKey *NKeySecret `json:"nkey,omitempty"` Token *TokenSecret `json:"token,omitempty"` User *User `json:"user,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // AccountList is a list of Account resources type AccountList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []Account `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/consumertypes.go ================================================ package v1beta2 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Consumer is a specification for a Consumer resource type Consumer struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec ConsumerSpec `json:"spec"` Status Status `json:"status"` } func (c *Consumer) GetSpec() interface{} { return c.Spec } // ConsumerSpec is the spec for a Consumer resource type ConsumerSpec struct { Description string `json:"description,omitempty"` AckPolicy string `json:"ackPolicy,omitempty"` AckWait string `json:"ackWait,omitempty"` DeliverPolicy string `json:"deliverPolicy,omitempty"` DeliverSubject string `json:"deliverSubject,omitempty"` DeliverGroup string `json:"deliverGroup,omitempty"` DurableName string `json:"durableName,omitempty"` // Maps to Durable FilterSubject string `json:"filterSubject,omitempty"` FilterSubjects []string `json:"filterSubjects,omitempty"` FlowControl bool `json:"flowControl,omitempty"` HeartbeatInterval string `json:"heartbeatInterval,omitempty"` // Maps to IdleHeartbeat MaxAckPending int `json:"maxAckPending,omitempty"` MaxDeliver int `json:"maxDeliver,omitempty"` BackOff []string `json:"backoff,omitempty"` MaxWaiting int `json:"maxWaiting,omitempty"` OptStartSeq int `json:"optStartSeq,omitempty"` OptStartTime string `json:"optStartTime,omitempty"` RateLimitBps int `json:"rateLimitBps,omitempty"` // Maps to RateLimit ReplayPolicy string `json:"replayPolicy,omitempty"` SampleFreq string `json:"sampleFreq,omitempty"` // Maps to SampleFrequency HeadersOnly bool `json:"headersOnly,omitempty"` MaxRequestBatch int `json:"maxRequestBatch,omitempty"` MaxRequestExpires string `json:"maxRequestExpires,omitempty"` MaxRequestMaxBytes int `json:"maxRequestMaxBytes,omitempty"` InactiveThreshold string `json:"inactiveThreshold,omitempty"` Replicas int `json:"replicas,omitempty"` MemStorage bool `json:"memStorage,omitempty"` // Maps to MemoryStorage Metadata map[string]string `json:"metadata,omitempty"` PauseUntil string `json:"pauseUntil,omitempty"` // RFC3339 timestamp for pausing consumer PriorityPolicy string `json:"priorityPolicy,omitempty"` // Priority policy: none, pinned_client, overflow, prioritized PinnedTTL string `json:"pinnedTtl,omitempty"` // Duration for pinned client timeout PriorityGroups []string `json:"priorityGroups,omitempty"` // List of priority groups StreamName string `json:"streamName"` BaseStreamConfig } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ConsumerList is a list of Consumer resources type ConsumerList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []Consumer `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/doc.go ================================================ // +k8s:deepcopy-gen=package // +groupName=jetstream.nats.io // Package v1 is the v1 version of the API. package v1beta2 ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/keyvaluetypes.go ================================================ package v1beta2 import ( "time" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Stream is a specification for a Stream resource type KeyValue struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec KeyValueSpec `json:"spec"` Status Status `json:"status"` } func (s *KeyValue) GetSpec() interface{} { return s.Spec } // StreamSpec is the spec for a Stream resource type KeyValueSpec struct { Bucket string `json:"bucket"` Description string `json:"description,omitempty"` MaxValueSize int `json:"maxValueSize,omitempty"` History int `json:"history,omitempty"` TTL string `json:"ttl,omitempty"` MaxBytes int `json:"maxBytes,omitempty"` Storage string `json:"storage,omitempty"` Replicas int `json:"replicas,omitempty"` Placement *StreamPlacement `json:"placement,omitempty"` RePublish *RePublish `json:"republish,omitempty"` Mirror *StreamSource `json:"mirror,omitempty"` Sources []*StreamSource `json:"sources,omitempty"` Compression bool `json:"compression,omitempty"` // LimitMarkerTTL is how long the bucket keeps markers when keys are removed by the TTL setting, 0 meaning markers are not supported // +optional LimitMarkerTTL time.Duration `json:"limitMarkerTtl,omitempty"` BaseStreamConfig } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // KeyValueList is a list of Stream resources type KeyValueList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []KeyValue `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/objectstoretypes.go ================================================ package v1beta2 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Stream is a specification for a Stream resource type ObjectStore struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec ObjectStoreSpec `json:"spec"` Status Status `json:"status"` } func (s *ObjectStore) GetSpec() interface{} { return s.Spec } // StreamSpec is the spec for a Stream resource type ObjectStoreSpec struct { Bucket string `json:"bucket"` Description string `json:"description,omitempty"` TTL string `json:"ttl,omitempty"` MaxBytes int `json:"maxBytes,omitempty"` Storage string `json:"storage,omitempty"` Replicas int `json:"replicas,omitempty"` Placement *StreamPlacement `json:"placement,omitempty"` Compression bool `json:"compression,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` BaseStreamConfig } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ObjectStoreList is a list of Stream resources type ObjectStoreList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []ObjectStore `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/register.go ================================================ package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/nats-io/nack/pkg/jetstream/apis/jetstream" ) var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: jetstream.GroupName, Version: "v1beta2"} // SchemeBuilder initializes a scheme builder SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Stream{}, &StreamList{}, &KeyValue{}, &KeyValueList{}, &ObjectStore{}, &ObjectStoreList{}, &Consumer{}, &ConsumerList{}, &Account{}, &AccountList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/streamtypes.go ================================================ package v1beta2 import ( k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Stream is a specification for a Stream resource type Stream struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ObjectMeta `json:"metadata,omitempty"` Spec StreamSpec `json:"spec"` Status Status `json:"status"` } func (s *Stream) GetSpec() interface{} { return s.Spec } // StreamSpec is the spec for a Stream resource type StreamSpec struct { Name string `json:"name"` Description string `json:"description,omitempty"` Subjects []string `json:"subjects,omitempty"` Retention string `json:"retention,omitempty"` MaxConsumers int `json:"maxConsumers,omitempty"` MaxMsgsPerSubject int `json:"maxMsgsPerSubject,omitempty"` MaxMsgs int `json:"maxMsgs,omitempty"` MaxBytes int `json:"maxBytes,omitempty"` MaxAge string `json:"maxAge,omitempty"` MaxMsgSize int `json:"maxMsgSize,omitempty"` Storage string `json:"storage,omitempty"` Discard string `json:"discard,omitempty"` Replicas int `json:"replicas,omitempty"` NoAck bool `json:"noAck,omitempty"` DuplicateWindow string `json:"duplicateWindow,omitempty"` // Maps to Duplicates Placement *StreamPlacement `json:"placement,omitempty"` Mirror *StreamSource `json:"mirror,omitempty"` Sources []*StreamSource `json:"sources,omitempty"` Compression string `json:"compression,omitempty"` SubjectTransform *SubjectTransform `json:"subjectTransform,omitempty"` RePublish *RePublish `json:"republish,omitempty"` Sealed bool `json:"sealed,omitempty"` DenyDelete bool `json:"denyDelete,omitempty"` DenyPurge bool `json:"denyPurge,omitempty"` AllowDirect bool `json:"allowDirect,omitempty"` AllowRollup bool `json:"allowRollup,omitempty"` // Maps to RollupAllowed MirrorDirect bool `json:"mirrorDirect,omitempty"` DiscardPerSubject bool `json:"discardPerSubject,omitempty"` // Maps to DiscardNewPer FirstSequence uint64 `json:"firstSequence,omitempty"` // Maps to FirstSeq Metadata map[string]string `json:"metadata,omitempty"` ConsumerLimits *ConsumerLimits `json:"consumerLimits,omitempty"` AllowMsgTTL bool `json:"allowMsgTtl,omitempty"` SubjectDeleteMarkerTTL string `json:"subjectDeleteMarkerTtl,omitempty"` AllowMsgCounter bool `json:"allowMsgCounter,omitempty"` AllowAtomicPublish bool `json:"allowAtomicPublish,omitempty"` AllowMsgSchedules bool `json:"allowMsgSchedules,omitempty"` PersistMode string `json:"persistMode,omitempty"` BaseStreamConfig } type SubjectTransform struct { Source string `json:"source"` Dest string `json:"dest"` } type StreamPlacement struct { Cluster string `json:"cluster"` Tags []string `json:"tags"` } type StreamSource struct { Name string `json:"name"` OptStartSeq int `json:"optStartSeq,omitempty"` OptStartTime string `json:"optStartTime,omitempty"` FilterSubject string `json:"filterSubject,omitempty"` ExternalAPIPrefix string `json:"externalApiPrefix,omitempty"` ExternalDeliverPrefix string `json:"externalDeliverPrefix,omitempty"` SubjectTransforms []*SubjectTransform `json:"subjectTransforms,omitempty"` } type RePublish struct { Source string `json:"source"` Destination string `json:"destination"` HeadersOnly bool `json:"headers_only,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // StreamList is a list of Stream resources type StreamList struct { k8smeta.TypeMeta `json:",inline"` k8smeta.ListMeta `json:"metadata"` Items []Stream `json:"items"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/types.go ================================================ package v1beta2 import ( k8sapi "k8s.io/api/core/v1" ) type CredentialsSecret struct { Name string `json:"name"` Key string `json:"key"` } type Status struct { ObservedGeneration int64 `json:"observedGeneration"` Conditions []Condition `json:"conditions"` } type Condition struct { Type string `json:"type"` Status k8sapi.ConditionStatus `json:"status"` Reason string `json:"reason"` Message string `json:"message"` LastTransitionTime string `json:"lastTransitionTime"` } type BaseStreamConfig struct { PreventDelete bool `json:"preventDelete"` PreventUpdate bool `json:"preventUpdate"` ConnectionOpts } type ConnectionOpts struct { Account string `json:"account,omitempty"` Creds string `json:"creds,omitempty"` Nkey string `json:"nkey,omitempty"` Servers []string `json:"servers,omitempty"` TLS *TLS `json:"tls,omitempty"` TLSFirst bool `json:"tlsFirst,omitempty"` JsDomain string `json:"jsDomain,omitempty"` } type ConsumerLimits struct { InactiveThreshold string `json:"inactiveThreshold,omitempty"` MaxAckPending int `json:"maxAckPending,omitempty"` } type TLS struct { ClientCert string `json:"clientCert"` ClientKey string `json:"clientKey"` RootCAs []string `json:"rootCas,omitempty"` } type TLSSecret struct { ClientCert string `json:"cert,omitempty"` ClientKey string `json:"key,omitempty"` RootCAs string `json:"ca,omitempty"` Secret *SecretRef `json:"secret"` } type CredsSecret struct { File string `json:"file,omitempty"` Secret *SecretRef `json:"secret"` } type NKeySecret struct { Seed string `json:"seed,omitempty"` Secret *SecretRef `json:"secret"` } type TokenSecret struct { Token string `json:"token,omitempty"` Secret SecretRef `json:"secret"` } type User struct { User string `json:"user,omitempty"` Password string `json:"password,omitempty"` Secret SecretRef `json:"secret"` } type SecretRef struct { Name string `json:"name"` } ================================================ FILE: pkg/jetstream/apis/jetstream/v1beta2/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by deepcopy-gen. DO NOT EDIT. package v1beta2 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Account) DeepCopyInto(out *Account) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Account. func (in *Account) DeepCopy() *Account { if in == nil { return nil } out := new(Account) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Account) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccountList) DeepCopyInto(out *AccountList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Account, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountList. func (in *AccountList) DeepCopy() *AccountList { if in == nil { return nil } out := new(AccountList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *AccountList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccountSpec) DeepCopyInto(out *AccountSpec) { *out = *in if in.Servers != nil { in, out := &in.Servers, &out.Servers *out = make([]string, len(*in)) copy(*out, *in) } if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSSecret) (*in).DeepCopyInto(*out) } if in.Creds != nil { in, out := &in.Creds, &out.Creds *out = new(CredsSecret) (*in).DeepCopyInto(*out) } if in.NKey != nil { in, out := &in.NKey, &out.NKey *out = new(NKeySecret) (*in).DeepCopyInto(*out) } if in.Token != nil { in, out := &in.Token, &out.Token *out = new(TokenSecret) **out = **in } if in.User != nil { in, out := &in.User, &out.User *out = new(User) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountSpec. func (in *AccountSpec) DeepCopy() *AccountSpec { if in == nil { return nil } out := new(AccountSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BaseStreamConfig) DeepCopyInto(out *BaseStreamConfig) { *out = *in in.ConnectionOpts.DeepCopyInto(&out.ConnectionOpts) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseStreamConfig. func (in *BaseStreamConfig) DeepCopy() *BaseStreamConfig { if in == nil { return nil } out := new(BaseStreamConfig) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. func (in *Condition) DeepCopy() *Condition { if in == nil { return nil } out := new(Condition) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectionOpts) DeepCopyInto(out *ConnectionOpts) { *out = *in if in.Servers != nil { in, out := &in.Servers, &out.Servers *out = make([]string, len(*in)) copy(*out, *in) } if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLS) (*in).DeepCopyInto(*out) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionOpts. func (in *ConnectionOpts) DeepCopy() *ConnectionOpts { if in == nil { return nil } out := new(ConnectionOpts) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Consumer) DeepCopyInto(out *Consumer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Consumer. func (in *Consumer) DeepCopy() *Consumer { if in == nil { return nil } out := new(Consumer) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Consumer) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsumerLimits) DeepCopyInto(out *ConsumerLimits) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsumerLimits. func (in *ConsumerLimits) DeepCopy() *ConsumerLimits { if in == nil { return nil } out := new(ConsumerLimits) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsumerList) DeepCopyInto(out *ConsumerList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Consumer, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsumerList. func (in *ConsumerList) DeepCopy() *ConsumerList { if in == nil { return nil } out := new(ConsumerList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ConsumerList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsumerSpec) DeepCopyInto(out *ConsumerSpec) { *out = *in if in.FilterSubjects != nil { in, out := &in.FilterSubjects, &out.FilterSubjects *out = make([]string, len(*in)) copy(*out, *in) } if in.BackOff != nil { in, out := &in.BackOff, &out.BackOff *out = make([]string, len(*in)) copy(*out, *in) } if in.Metadata != nil { in, out := &in.Metadata, &out.Metadata *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.PriorityGroups != nil { in, out := &in.PriorityGroups, &out.PriorityGroups *out = make([]string, len(*in)) copy(*out, *in) } in.BaseStreamConfig.DeepCopyInto(&out.BaseStreamConfig) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsumerSpec. func (in *ConsumerSpec) DeepCopy() *ConsumerSpec { if in == nil { return nil } out := new(ConsumerSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialsSecret) DeepCopyInto(out *CredentialsSecret) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialsSecret. func (in *CredentialsSecret) DeepCopy() *CredentialsSecret { if in == nil { return nil } out := new(CredentialsSecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredsSecret) DeepCopyInto(out *CredsSecret) { *out = *in if in.Secret != nil { in, out := &in.Secret, &out.Secret *out = new(SecretRef) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredsSecret. func (in *CredsSecret) DeepCopy() *CredsSecret { if in == nil { return nil } out := new(CredsSecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeyValue) DeepCopyInto(out *KeyValue) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyValue. func (in *KeyValue) DeepCopy() *KeyValue { if in == nil { return nil } out := new(KeyValue) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *KeyValue) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeyValueList) DeepCopyInto(out *KeyValueList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]KeyValue, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyValueList. func (in *KeyValueList) DeepCopy() *KeyValueList { if in == nil { return nil } out := new(KeyValueList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *KeyValueList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeyValueSpec) DeepCopyInto(out *KeyValueSpec) { *out = *in if in.Placement != nil { in, out := &in.Placement, &out.Placement *out = new(StreamPlacement) (*in).DeepCopyInto(*out) } if in.RePublish != nil { in, out := &in.RePublish, &out.RePublish *out = new(RePublish) **out = **in } if in.Mirror != nil { in, out := &in.Mirror, &out.Mirror *out = new(StreamSource) (*in).DeepCopyInto(*out) } if in.Sources != nil { in, out := &in.Sources, &out.Sources *out = make([]*StreamSource, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(StreamSource) (*in).DeepCopyInto(*out) } } } in.BaseStreamConfig.DeepCopyInto(&out.BaseStreamConfig) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyValueSpec. func (in *KeyValueSpec) DeepCopy() *KeyValueSpec { if in == nil { return nil } out := new(KeyValueSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NKeySecret) DeepCopyInto(out *NKeySecret) { *out = *in if in.Secret != nil { in, out := &in.Secret, &out.Secret *out = new(SecretRef) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NKeySecret. func (in *NKeySecret) DeepCopy() *NKeySecret { if in == nil { return nil } out := new(NKeySecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectStore) DeepCopyInto(out *ObjectStore) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStore. func (in *ObjectStore) DeepCopy() *ObjectStore { if in == nil { return nil } out := new(ObjectStore) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ObjectStore) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectStoreList) DeepCopyInto(out *ObjectStoreList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]ObjectStore, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreList. func (in *ObjectStoreList) DeepCopy() *ObjectStoreList { if in == nil { return nil } out := new(ObjectStoreList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ObjectStoreList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectStoreSpec) DeepCopyInto(out *ObjectStoreSpec) { *out = *in if in.Placement != nil { in, out := &in.Placement, &out.Placement *out = new(StreamPlacement) (*in).DeepCopyInto(*out) } if in.Metadata != nil { in, out := &in.Metadata, &out.Metadata *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } in.BaseStreamConfig.DeepCopyInto(&out.BaseStreamConfig) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreSpec. func (in *ObjectStoreSpec) DeepCopy() *ObjectStoreSpec { if in == nil { return nil } out := new(ObjectStoreSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RePublish) DeepCopyInto(out *RePublish) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RePublish. func (in *RePublish) DeepCopy() *RePublish { if in == nil { return nil } out := new(RePublish) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretRef) DeepCopyInto(out *SecretRef) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. func (in *SecretRef) DeepCopy() *SecretRef { if in == nil { return nil } out := new(SecretRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Status) DeepCopyInto(out *Status) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]Condition, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. func (in *Status) DeepCopy() *Status { if in == nil { return nil } out := new(Status) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Stream) DeepCopyInto(out *Stream) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stream. func (in *Stream) DeepCopy() *Stream { if in == nil { return nil } out := new(Stream) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Stream) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamList) DeepCopyInto(out *StreamList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Stream, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamList. func (in *StreamList) DeepCopy() *StreamList { if in == nil { return nil } out := new(StreamList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *StreamList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamPlacement) DeepCopyInto(out *StreamPlacement) { *out = *in if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make([]string, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamPlacement. func (in *StreamPlacement) DeepCopy() *StreamPlacement { if in == nil { return nil } out := new(StreamPlacement) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamSource) DeepCopyInto(out *StreamSource) { *out = *in if in.SubjectTransforms != nil { in, out := &in.SubjectTransforms, &out.SubjectTransforms *out = make([]*SubjectTransform, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(SubjectTransform) **out = **in } } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamSource. func (in *StreamSource) DeepCopy() *StreamSource { if in == nil { return nil } out := new(StreamSource) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StreamSpec) DeepCopyInto(out *StreamSpec) { *out = *in if in.Subjects != nil { in, out := &in.Subjects, &out.Subjects *out = make([]string, len(*in)) copy(*out, *in) } if in.Placement != nil { in, out := &in.Placement, &out.Placement *out = new(StreamPlacement) (*in).DeepCopyInto(*out) } if in.Mirror != nil { in, out := &in.Mirror, &out.Mirror *out = new(StreamSource) (*in).DeepCopyInto(*out) } if in.Sources != nil { in, out := &in.Sources, &out.Sources *out = make([]*StreamSource, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(StreamSource) (*in).DeepCopyInto(*out) } } } if in.SubjectTransform != nil { in, out := &in.SubjectTransform, &out.SubjectTransform *out = new(SubjectTransform) **out = **in } if in.RePublish != nil { in, out := &in.RePublish, &out.RePublish *out = new(RePublish) **out = **in } if in.Metadata != nil { in, out := &in.Metadata, &out.Metadata *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.ConsumerLimits != nil { in, out := &in.ConsumerLimits, &out.ConsumerLimits *out = new(ConsumerLimits) **out = **in } in.BaseStreamConfig.DeepCopyInto(&out.BaseStreamConfig) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamSpec. func (in *StreamSpec) DeepCopy() *StreamSpec { if in == nil { return nil } out := new(StreamSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SubjectTransform) DeepCopyInto(out *SubjectTransform) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubjectTransform. func (in *SubjectTransform) DeepCopy() *SubjectTransform { if in == nil { return nil } out := new(SubjectTransform) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLS) DeepCopyInto(out *TLS) { *out = *in if in.RootCAs != nil { in, out := &in.RootCAs, &out.RootCAs *out = make([]string, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. func (in *TLS) DeepCopy() *TLS { if in == nil { return nil } out := new(TLS) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSecret) DeepCopyInto(out *TLSSecret) { *out = *in if in.Secret != nil { in, out := &in.Secret, &out.Secret *out = new(SecretRef) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecret. func (in *TLSSecret) DeepCopy() *TLSSecret { if in == nil { return nil } out := new(TLSSecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenSecret) DeepCopyInto(out *TokenSecret) { *out = *in out.Secret = in.Secret return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenSecret. func (in *TokenSecret) DeepCopy() *TokenSecret { if in == nil { return nil } out := new(TokenSecret) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in out.Secret = in.Secret return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. func (in *User) DeepCopy() *User { if in == nil { return nil } out := new(User) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/internal/internal.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package internal import ( fmt "fmt" sync "sync" typed "sigs.k8s.io/structured-merge-diff/v6/typed" ) func Parser() *typed.Parser { parserOnce.Do(func() { var err error parser, err = typed.NewParser(schemaYAML) if err != nil { panic(fmt.Sprintf("Failed to parse schema: %v", err)) } }) return parser } var parserOnce sync.Once var parser *typed.Parser var schemaYAML = typed.YAMLObject(`types: - name: __untyped_atomic_ scalar: untyped list: elementType: namedType: __untyped_atomic_ elementRelationship: atomic map: elementType: namedType: __untyped_atomic_ elementRelationship: atomic - name: __untyped_deduced_ scalar: untyped list: elementType: namedType: __untyped_atomic_ elementRelationship: atomic map: elementType: namedType: __untyped_deduced_ elementRelationship: separable `) ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/account.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // AccountApplyConfiguration represents a declarative configuration of the Account type for use // with apply. type AccountApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` Spec *AccountSpecApplyConfiguration `json:"spec,omitempty"` Status *StatusApplyConfiguration `json:"status,omitempty"` } // Account constructs a declarative configuration of the Account type for use with // apply. func Account(name, namespace string) *AccountApplyConfiguration { b := &AccountApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) b.WithKind("Account") b.WithAPIVersion("jetstream.nats.io/v1beta2") return b } func (b AccountApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. func (b *AccountApplyConfiguration) WithKind(value string) *AccountApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. func (b *AccountApplyConfiguration) WithAPIVersion(value string) *AccountApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *AccountApplyConfiguration) WithName(value string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b } // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. func (b *AccountApplyConfiguration) WithGenerateName(value string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b } // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. func (b *AccountApplyConfiguration) WithNamespace(value string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b } // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. func (b *AccountApplyConfiguration) WithUID(value types.UID) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b } // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. func (b *AccountApplyConfiguration) WithResourceVersion(value string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b } // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. func (b *AccountApplyConfiguration) WithGeneration(value int64) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b } // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. func (b *AccountApplyConfiguration) WithCreationTimestamp(value metav1.Time) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. func (b *AccountApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. func (b *AccountApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b } // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. func (b *AccountApplyConfiguration) WithLabels(entries map[string]string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Labels[k] = v } return b } // WithAnnotations puts the entries into the Annotations field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. func (b *AccountApplyConfiguration) WithAnnotations(entries map[string]string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Annotations[k] = v } return b } // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. func (b *AccountApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { panic("nil value passed to WithOwnerReferences") } b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) } return b } // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. func (b *AccountApplyConfiguration) WithFinalizers(values ...string) *AccountApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) } return b } func (b *AccountApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } } // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. func (b *AccountApplyConfiguration) WithSpec(value *AccountSpecApplyConfiguration) *AccountApplyConfiguration { b.Spec = value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *AccountApplyConfiguration) WithStatus(value *StatusApplyConfiguration) *AccountApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. func (b *AccountApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. func (b *AccountApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. func (b *AccountApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. func (b *AccountApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/accountspec.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // AccountSpecApplyConfiguration represents a declarative configuration of the AccountSpec type for use // with apply. type AccountSpecApplyConfiguration struct { Servers []string `json:"servers,omitempty"` TLS *TLSSecretApplyConfiguration `json:"tls,omitempty"` Creds *CredsSecretApplyConfiguration `json:"creds,omitempty"` NKey *NKeySecretApplyConfiguration `json:"nkey,omitempty"` Token *TokenSecretApplyConfiguration `json:"token,omitempty"` User *UserApplyConfiguration `json:"user,omitempty"` } // AccountSpecApplyConfiguration constructs a declarative configuration of the AccountSpec type for use with // apply. func AccountSpec() *AccountSpecApplyConfiguration { return &AccountSpecApplyConfiguration{} } // WithServers adds the given value to the Servers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Servers field. func (b *AccountSpecApplyConfiguration) WithServers(values ...string) *AccountSpecApplyConfiguration { for i := range values { b.Servers = append(b.Servers, values[i]) } return b } // WithTLS sets the TLS field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the TLS field is set to the value of the last call. func (b *AccountSpecApplyConfiguration) WithTLS(value *TLSSecretApplyConfiguration) *AccountSpecApplyConfiguration { b.TLS = value return b } // WithCreds sets the Creds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Creds field is set to the value of the last call. func (b *AccountSpecApplyConfiguration) WithCreds(value *CredsSecretApplyConfiguration) *AccountSpecApplyConfiguration { b.Creds = value return b } // WithNKey sets the NKey field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the NKey field is set to the value of the last call. func (b *AccountSpecApplyConfiguration) WithNKey(value *NKeySecretApplyConfiguration) *AccountSpecApplyConfiguration { b.NKey = value return b } // WithToken sets the Token field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Token field is set to the value of the last call. func (b *AccountSpecApplyConfiguration) WithToken(value *TokenSecretApplyConfiguration) *AccountSpecApplyConfiguration { b.Token = value return b } // WithUser sets the User field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the User field is set to the value of the last call. func (b *AccountSpecApplyConfiguration) WithUser(value *UserApplyConfiguration) *AccountSpecApplyConfiguration { b.User = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/basestreamconfig.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // BaseStreamConfigApplyConfiguration represents a declarative configuration of the BaseStreamConfig type for use // with apply. type BaseStreamConfigApplyConfiguration struct { PreventDelete *bool `json:"preventDelete,omitempty"` PreventUpdate *bool `json:"preventUpdate,omitempty"` } // BaseStreamConfigApplyConfiguration constructs a declarative configuration of the BaseStreamConfig type for use with // apply. func BaseStreamConfig() *BaseStreamConfigApplyConfiguration { return &BaseStreamConfigApplyConfiguration{} } // WithPreventDelete sets the PreventDelete field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PreventDelete field is set to the value of the last call. func (b *BaseStreamConfigApplyConfiguration) WithPreventDelete(value bool) *BaseStreamConfigApplyConfiguration { b.PreventDelete = &value return b } // WithPreventUpdate sets the PreventUpdate field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PreventUpdate field is set to the value of the last call. func (b *BaseStreamConfigApplyConfiguration) WithPreventUpdate(value bool) *BaseStreamConfigApplyConfiguration { b.PreventUpdate = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/condition.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( v1 "k8s.io/api/core/v1" ) // ConditionApplyConfiguration represents a declarative configuration of the Condition type for use // with apply. type ConditionApplyConfiguration struct { Type *string `json:"type,omitempty"` Status *v1.ConditionStatus `json:"status,omitempty"` Reason *string `json:"reason,omitempty"` Message *string `json:"message,omitempty"` LastTransitionTime *string `json:"lastTransitionTime,omitempty"` } // ConditionApplyConfiguration constructs a declarative configuration of the Condition type for use with // apply. func Condition() *ConditionApplyConfiguration { return &ConditionApplyConfiguration{} } // WithType sets the Type field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Type field is set to the value of the last call. func (b *ConditionApplyConfiguration) WithType(value string) *ConditionApplyConfiguration { b.Type = &value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *ConditionApplyConfiguration) WithStatus(value v1.ConditionStatus) *ConditionApplyConfiguration { b.Status = &value return b } // WithReason sets the Reason field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Reason field is set to the value of the last call. func (b *ConditionApplyConfiguration) WithReason(value string) *ConditionApplyConfiguration { b.Reason = &value return b } // WithMessage sets the Message field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Message field is set to the value of the last call. func (b *ConditionApplyConfiguration) WithMessage(value string) *ConditionApplyConfiguration { b.Message = &value return b } // WithLastTransitionTime sets the LastTransitionTime field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the LastTransitionTime field is set to the value of the last call. func (b *ConditionApplyConfiguration) WithLastTransitionTime(value string) *ConditionApplyConfiguration { b.LastTransitionTime = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/connectionopts.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // ConnectionOptsApplyConfiguration represents a declarative configuration of the ConnectionOpts type for use // with apply. type ConnectionOptsApplyConfiguration struct { Account *string `json:"account,omitempty"` Creds *string `json:"creds,omitempty"` Nkey *string `json:"nkey,omitempty"` Servers []string `json:"servers,omitempty"` TLS *TLSApplyConfiguration `json:"tls,omitempty"` TLSFirst *bool `json:"tlsFirst,omitempty"` JsDomain *string `json:"jsDomain,omitempty"` } // ConnectionOptsApplyConfiguration constructs a declarative configuration of the ConnectionOpts type for use with // apply. func ConnectionOpts() *ConnectionOptsApplyConfiguration { return &ConnectionOptsApplyConfiguration{} } // WithAccount sets the Account field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Account field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithAccount(value string) *ConnectionOptsApplyConfiguration { b.Account = &value return b } // WithCreds sets the Creds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Creds field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithCreds(value string) *ConnectionOptsApplyConfiguration { b.Creds = &value return b } // WithNkey sets the Nkey field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Nkey field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithNkey(value string) *ConnectionOptsApplyConfiguration { b.Nkey = &value return b } // WithServers adds the given value to the Servers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Servers field. func (b *ConnectionOptsApplyConfiguration) WithServers(values ...string) *ConnectionOptsApplyConfiguration { for i := range values { b.Servers = append(b.Servers, values[i]) } return b } // WithTLS sets the TLS field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the TLS field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithTLS(value *TLSApplyConfiguration) *ConnectionOptsApplyConfiguration { b.TLS = value return b } // WithTLSFirst sets the TLSFirst field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the TLSFirst field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithTLSFirst(value bool) *ConnectionOptsApplyConfiguration { b.TLSFirst = &value return b } // WithJsDomain sets the JsDomain field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the JsDomain field is set to the value of the last call. func (b *ConnectionOptsApplyConfiguration) WithJsDomain(value string) *ConnectionOptsApplyConfiguration { b.JsDomain = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/consumer.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // ConsumerApplyConfiguration represents a declarative configuration of the Consumer type for use // with apply. type ConsumerApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` Spec *ConsumerSpecApplyConfiguration `json:"spec,omitempty"` Status *StatusApplyConfiguration `json:"status,omitempty"` } // Consumer constructs a declarative configuration of the Consumer type for use with // apply. func Consumer(name, namespace string) *ConsumerApplyConfiguration { b := &ConsumerApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) b.WithKind("Consumer") b.WithAPIVersion("jetstream.nats.io/v1beta2") return b } func (b ConsumerApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithKind(value string) *ConsumerApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithAPIVersion(value string) *ConsumerApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithName(value string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b } // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithGenerateName(value string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b } // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithNamespace(value string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b } // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithUID(value types.UID) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b } // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithResourceVersion(value string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b } // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithGeneration(value int64) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b } // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithCreationTimestamp(value metav1.Time) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b } // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. func (b *ConsumerApplyConfiguration) WithLabels(entries map[string]string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Labels[k] = v } return b } // WithAnnotations puts the entries into the Annotations field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. func (b *ConsumerApplyConfiguration) WithAnnotations(entries map[string]string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Annotations[k] = v } return b } // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. func (b *ConsumerApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { panic("nil value passed to WithOwnerReferences") } b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) } return b } // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. func (b *ConsumerApplyConfiguration) WithFinalizers(values ...string) *ConsumerApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) } return b } func (b *ConsumerApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } } // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithSpec(value *ConsumerSpecApplyConfiguration) *ConsumerApplyConfiguration { b.Spec = value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *ConsumerApplyConfiguration) WithStatus(value *StatusApplyConfiguration) *ConsumerApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. func (b *ConsumerApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. func (b *ConsumerApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. func (b *ConsumerApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. func (b *ConsumerApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/consumerlimits.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // ConsumerLimitsApplyConfiguration represents a declarative configuration of the ConsumerLimits type for use // with apply. type ConsumerLimitsApplyConfiguration struct { InactiveThreshold *string `json:"inactiveThreshold,omitempty"` MaxAckPending *int `json:"maxAckPending,omitempty"` } // ConsumerLimitsApplyConfiguration constructs a declarative configuration of the ConsumerLimits type for use with // apply. func ConsumerLimits() *ConsumerLimitsApplyConfiguration { return &ConsumerLimitsApplyConfiguration{} } // WithInactiveThreshold sets the InactiveThreshold field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the InactiveThreshold field is set to the value of the last call. func (b *ConsumerLimitsApplyConfiguration) WithInactiveThreshold(value string) *ConsumerLimitsApplyConfiguration { b.InactiveThreshold = &value return b } // WithMaxAckPending sets the MaxAckPending field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxAckPending field is set to the value of the last call. func (b *ConsumerLimitsApplyConfiguration) WithMaxAckPending(value int) *ConsumerLimitsApplyConfiguration { b.MaxAckPending = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/consumerspec.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // ConsumerSpecApplyConfiguration represents a declarative configuration of the ConsumerSpec type for use // with apply. type ConsumerSpecApplyConfiguration struct { Description *string `json:"description,omitempty"` AckPolicy *string `json:"ackPolicy,omitempty"` AckWait *string `json:"ackWait,omitempty"` DeliverPolicy *string `json:"deliverPolicy,omitempty"` DeliverSubject *string `json:"deliverSubject,omitempty"` DeliverGroup *string `json:"deliverGroup,omitempty"` DurableName *string `json:"durableName,omitempty"` FilterSubject *string `json:"filterSubject,omitempty"` FilterSubjects []string `json:"filterSubjects,omitempty"` FlowControl *bool `json:"flowControl,omitempty"` HeartbeatInterval *string `json:"heartbeatInterval,omitempty"` MaxAckPending *int `json:"maxAckPending,omitempty"` MaxDeliver *int `json:"maxDeliver,omitempty"` BackOff []string `json:"backoff,omitempty"` MaxWaiting *int `json:"maxWaiting,omitempty"` OptStartSeq *int `json:"optStartSeq,omitempty"` OptStartTime *string `json:"optStartTime,omitempty"` RateLimitBps *int `json:"rateLimitBps,omitempty"` ReplayPolicy *string `json:"replayPolicy,omitempty"` SampleFreq *string `json:"sampleFreq,omitempty"` HeadersOnly *bool `json:"headersOnly,omitempty"` MaxRequestBatch *int `json:"maxRequestBatch,omitempty"` MaxRequestExpires *string `json:"maxRequestExpires,omitempty"` MaxRequestMaxBytes *int `json:"maxRequestMaxBytes,omitempty"` InactiveThreshold *string `json:"inactiveThreshold,omitempty"` Replicas *int `json:"replicas,omitempty"` MemStorage *bool `json:"memStorage,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` PauseUntil *string `json:"pauseUntil,omitempty"` PriorityPolicy *string `json:"priorityPolicy,omitempty"` PinnedTTL *string `json:"pinnedTtl,omitempty"` PriorityGroups []string `json:"priorityGroups,omitempty"` StreamName *string `json:"streamName,omitempty"` } // ConsumerSpecApplyConfiguration constructs a declarative configuration of the ConsumerSpec type for use with // apply. func ConsumerSpec() *ConsumerSpecApplyConfiguration { return &ConsumerSpecApplyConfiguration{} } // WithDescription sets the Description field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Description field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithDescription(value string) *ConsumerSpecApplyConfiguration { b.Description = &value return b } // WithAckPolicy sets the AckPolicy field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AckPolicy field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithAckPolicy(value string) *ConsumerSpecApplyConfiguration { b.AckPolicy = &value return b } // WithAckWait sets the AckWait field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AckWait field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithAckWait(value string) *ConsumerSpecApplyConfiguration { b.AckWait = &value return b } // WithDeliverPolicy sets the DeliverPolicy field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeliverPolicy field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithDeliverPolicy(value string) *ConsumerSpecApplyConfiguration { b.DeliverPolicy = &value return b } // WithDeliverSubject sets the DeliverSubject field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeliverSubject field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithDeliverSubject(value string) *ConsumerSpecApplyConfiguration { b.DeliverSubject = &value return b } // WithDeliverGroup sets the DeliverGroup field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeliverGroup field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithDeliverGroup(value string) *ConsumerSpecApplyConfiguration { b.DeliverGroup = &value return b } // WithDurableName sets the DurableName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DurableName field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithDurableName(value string) *ConsumerSpecApplyConfiguration { b.DurableName = &value return b } // WithFilterSubject sets the FilterSubject field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the FilterSubject field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithFilterSubject(value string) *ConsumerSpecApplyConfiguration { b.FilterSubject = &value return b } // WithFilterSubjects adds the given value to the FilterSubjects field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the FilterSubjects field. func (b *ConsumerSpecApplyConfiguration) WithFilterSubjects(values ...string) *ConsumerSpecApplyConfiguration { for i := range values { b.FilterSubjects = append(b.FilterSubjects, values[i]) } return b } // WithFlowControl sets the FlowControl field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the FlowControl field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithFlowControl(value bool) *ConsumerSpecApplyConfiguration { b.FlowControl = &value return b } // WithHeartbeatInterval sets the HeartbeatInterval field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the HeartbeatInterval field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithHeartbeatInterval(value string) *ConsumerSpecApplyConfiguration { b.HeartbeatInterval = &value return b } // WithMaxAckPending sets the MaxAckPending field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxAckPending field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxAckPending(value int) *ConsumerSpecApplyConfiguration { b.MaxAckPending = &value return b } // WithMaxDeliver sets the MaxDeliver field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxDeliver field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxDeliver(value int) *ConsumerSpecApplyConfiguration { b.MaxDeliver = &value return b } // WithBackOff adds the given value to the BackOff field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the BackOff field. func (b *ConsumerSpecApplyConfiguration) WithBackOff(values ...string) *ConsumerSpecApplyConfiguration { for i := range values { b.BackOff = append(b.BackOff, values[i]) } return b } // WithMaxWaiting sets the MaxWaiting field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxWaiting field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxWaiting(value int) *ConsumerSpecApplyConfiguration { b.MaxWaiting = &value return b } // WithOptStartSeq sets the OptStartSeq field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the OptStartSeq field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithOptStartSeq(value int) *ConsumerSpecApplyConfiguration { b.OptStartSeq = &value return b } // WithOptStartTime sets the OptStartTime field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the OptStartTime field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithOptStartTime(value string) *ConsumerSpecApplyConfiguration { b.OptStartTime = &value return b } // WithRateLimitBps sets the RateLimitBps field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RateLimitBps field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithRateLimitBps(value int) *ConsumerSpecApplyConfiguration { b.RateLimitBps = &value return b } // WithReplayPolicy sets the ReplayPolicy field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ReplayPolicy field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithReplayPolicy(value string) *ConsumerSpecApplyConfiguration { b.ReplayPolicy = &value return b } // WithSampleFreq sets the SampleFreq field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the SampleFreq field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithSampleFreq(value string) *ConsumerSpecApplyConfiguration { b.SampleFreq = &value return b } // WithHeadersOnly sets the HeadersOnly field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the HeadersOnly field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithHeadersOnly(value bool) *ConsumerSpecApplyConfiguration { b.HeadersOnly = &value return b } // WithMaxRequestBatch sets the MaxRequestBatch field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxRequestBatch field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxRequestBatch(value int) *ConsumerSpecApplyConfiguration { b.MaxRequestBatch = &value return b } // WithMaxRequestExpires sets the MaxRequestExpires field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxRequestExpires field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxRequestExpires(value string) *ConsumerSpecApplyConfiguration { b.MaxRequestExpires = &value return b } // WithMaxRequestMaxBytes sets the MaxRequestMaxBytes field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxRequestMaxBytes field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMaxRequestMaxBytes(value int) *ConsumerSpecApplyConfiguration { b.MaxRequestMaxBytes = &value return b } // WithInactiveThreshold sets the InactiveThreshold field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the InactiveThreshold field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithInactiveThreshold(value string) *ConsumerSpecApplyConfiguration { b.InactiveThreshold = &value return b } // WithReplicas sets the Replicas field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Replicas field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithReplicas(value int) *ConsumerSpecApplyConfiguration { b.Replicas = &value return b } // WithMemStorage sets the MemStorage field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MemStorage field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithMemStorage(value bool) *ConsumerSpecApplyConfiguration { b.MemStorage = &value return b } // WithMetadata puts the entries into the Metadata field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Metadata field, // overwriting an existing map entries in Metadata field with the same key. func (b *ConsumerSpecApplyConfiguration) WithMetadata(entries map[string]string) *ConsumerSpecApplyConfiguration { if b.Metadata == nil && len(entries) > 0 { b.Metadata = make(map[string]string, len(entries)) } for k, v := range entries { b.Metadata[k] = v } return b } // WithPauseUntil sets the PauseUntil field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PauseUntil field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithPauseUntil(value string) *ConsumerSpecApplyConfiguration { b.PauseUntil = &value return b } // WithPriorityPolicy sets the PriorityPolicy field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PriorityPolicy field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithPriorityPolicy(value string) *ConsumerSpecApplyConfiguration { b.PriorityPolicy = &value return b } // WithPinnedTTL sets the PinnedTTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PinnedTTL field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithPinnedTTL(value string) *ConsumerSpecApplyConfiguration { b.PinnedTTL = &value return b } // WithPriorityGroups adds the given value to the PriorityGroups field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the PriorityGroups field. func (b *ConsumerSpecApplyConfiguration) WithPriorityGroups(values ...string) *ConsumerSpecApplyConfiguration { for i := range values { b.PriorityGroups = append(b.PriorityGroups, values[i]) } return b } // WithStreamName sets the StreamName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the StreamName field is set to the value of the last call. func (b *ConsumerSpecApplyConfiguration) WithStreamName(value string) *ConsumerSpecApplyConfiguration { b.StreamName = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/credssecret.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // CredsSecretApplyConfiguration represents a declarative configuration of the CredsSecret type for use // with apply. type CredsSecretApplyConfiguration struct { File *string `json:"file,omitempty"` Secret *SecretRefApplyConfiguration `json:"secret,omitempty"` } // CredsSecretApplyConfiguration constructs a declarative configuration of the CredsSecret type for use with // apply. func CredsSecret() *CredsSecretApplyConfiguration { return &CredsSecretApplyConfiguration{} } // WithFile sets the File field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the File field is set to the value of the last call. func (b *CredsSecretApplyConfiguration) WithFile(value string) *CredsSecretApplyConfiguration { b.File = &value return b } // WithSecret sets the Secret field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Secret field is set to the value of the last call. func (b *CredsSecretApplyConfiguration) WithSecret(value *SecretRefApplyConfiguration) *CredsSecretApplyConfiguration { b.Secret = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/keyvalue.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // KeyValueApplyConfiguration represents a declarative configuration of the KeyValue type for use // with apply. type KeyValueApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` Spec *KeyValueSpecApplyConfiguration `json:"spec,omitempty"` Status *StatusApplyConfiguration `json:"status,omitempty"` } // KeyValue constructs a declarative configuration of the KeyValue type for use with // apply. func KeyValue(name, namespace string) *KeyValueApplyConfiguration { b := &KeyValueApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) b.WithKind("KeyValue") b.WithAPIVersion("jetstream.nats.io/v1beta2") return b } func (b KeyValueApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithKind(value string) *KeyValueApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithAPIVersion(value string) *KeyValueApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithName(value string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b } // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithGenerateName(value string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b } // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithNamespace(value string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b } // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithUID(value types.UID) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b } // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithResourceVersion(value string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b } // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithGeneration(value int64) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b } // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithCreationTimestamp(value metav1.Time) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b } // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. func (b *KeyValueApplyConfiguration) WithLabels(entries map[string]string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Labels[k] = v } return b } // WithAnnotations puts the entries into the Annotations field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. func (b *KeyValueApplyConfiguration) WithAnnotations(entries map[string]string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Annotations[k] = v } return b } // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. func (b *KeyValueApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { panic("nil value passed to WithOwnerReferences") } b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) } return b } // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. func (b *KeyValueApplyConfiguration) WithFinalizers(values ...string) *KeyValueApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) } return b } func (b *KeyValueApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } } // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithSpec(value *KeyValueSpecApplyConfiguration) *KeyValueApplyConfiguration { b.Spec = value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *KeyValueApplyConfiguration) WithStatus(value *StatusApplyConfiguration) *KeyValueApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. func (b *KeyValueApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. func (b *KeyValueApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. func (b *KeyValueApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. func (b *KeyValueApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/keyvaluespec.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( time "time" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) // KeyValueSpecApplyConfiguration represents a declarative configuration of the KeyValueSpec type for use // with apply. type KeyValueSpecApplyConfiguration struct { Bucket *string `json:"bucket,omitempty"` Description *string `json:"description,omitempty"` MaxValueSize *int `json:"maxValueSize,omitempty"` History *int `json:"history,omitempty"` TTL *string `json:"ttl,omitempty"` MaxBytes *int `json:"maxBytes,omitempty"` Storage *string `json:"storage,omitempty"` Replicas *int `json:"replicas,omitempty"` Placement *StreamPlacementApplyConfiguration `json:"placement,omitempty"` RePublish *RePublishApplyConfiguration `json:"republish,omitempty"` Mirror *StreamSourceApplyConfiguration `json:"mirror,omitempty"` Sources []*jetstreamv1beta2.StreamSource `json:"sources,omitempty"` Compression *bool `json:"compression,omitempty"` LimitMarkerTTL *time.Duration `json:"limitMarkerTtl,omitempty"` } // KeyValueSpecApplyConfiguration constructs a declarative configuration of the KeyValueSpec type for use with // apply. func KeyValueSpec() *KeyValueSpecApplyConfiguration { return &KeyValueSpecApplyConfiguration{} } // WithBucket sets the Bucket field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Bucket field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithBucket(value string) *KeyValueSpecApplyConfiguration { b.Bucket = &value return b } // WithDescription sets the Description field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Description field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithDescription(value string) *KeyValueSpecApplyConfiguration { b.Description = &value return b } // WithMaxValueSize sets the MaxValueSize field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxValueSize field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithMaxValueSize(value int) *KeyValueSpecApplyConfiguration { b.MaxValueSize = &value return b } // WithHistory sets the History field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the History field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithHistory(value int) *KeyValueSpecApplyConfiguration { b.History = &value return b } // WithTTL sets the TTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the TTL field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithTTL(value string) *KeyValueSpecApplyConfiguration { b.TTL = &value return b } // WithMaxBytes sets the MaxBytes field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxBytes field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithMaxBytes(value int) *KeyValueSpecApplyConfiguration { b.MaxBytes = &value return b } // WithStorage sets the Storage field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Storage field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithStorage(value string) *KeyValueSpecApplyConfiguration { b.Storage = &value return b } // WithReplicas sets the Replicas field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Replicas field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithReplicas(value int) *KeyValueSpecApplyConfiguration { b.Replicas = &value return b } // WithPlacement sets the Placement field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Placement field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithPlacement(value *StreamPlacementApplyConfiguration) *KeyValueSpecApplyConfiguration { b.Placement = value return b } // WithRePublish sets the RePublish field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RePublish field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithRePublish(value *RePublishApplyConfiguration) *KeyValueSpecApplyConfiguration { b.RePublish = value return b } // WithMirror sets the Mirror field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Mirror field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithMirror(value *StreamSourceApplyConfiguration) *KeyValueSpecApplyConfiguration { b.Mirror = value return b } // WithSources adds the given value to the Sources field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Sources field. func (b *KeyValueSpecApplyConfiguration) WithSources(values ...**jetstreamv1beta2.StreamSource) *KeyValueSpecApplyConfiguration { for i := range values { if values[i] == nil { panic("nil value passed to WithSources") } b.Sources = append(b.Sources, *values[i]) } return b } // WithCompression sets the Compression field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Compression field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithCompression(value bool) *KeyValueSpecApplyConfiguration { b.Compression = &value return b } // WithLimitMarkerTTL sets the LimitMarkerTTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the LimitMarkerTTL field is set to the value of the last call. func (b *KeyValueSpecApplyConfiguration) WithLimitMarkerTTL(value time.Duration) *KeyValueSpecApplyConfiguration { b.LimitMarkerTTL = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/nkeysecret.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // NKeySecretApplyConfiguration represents a declarative configuration of the NKeySecret type for use // with apply. type NKeySecretApplyConfiguration struct { Seed *string `json:"seed,omitempty"` Secret *SecretRefApplyConfiguration `json:"secret,omitempty"` } // NKeySecretApplyConfiguration constructs a declarative configuration of the NKeySecret type for use with // apply. func NKeySecret() *NKeySecretApplyConfiguration { return &NKeySecretApplyConfiguration{} } // WithSeed sets the Seed field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Seed field is set to the value of the last call. func (b *NKeySecretApplyConfiguration) WithSeed(value string) *NKeySecretApplyConfiguration { b.Seed = &value return b } // WithSecret sets the Secret field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Secret field is set to the value of the last call. func (b *NKeySecretApplyConfiguration) WithSecret(value *SecretRefApplyConfiguration) *NKeySecretApplyConfiguration { b.Secret = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/objectstore.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // ObjectStoreApplyConfiguration represents a declarative configuration of the ObjectStore type for use // with apply. type ObjectStoreApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` Spec *ObjectStoreSpecApplyConfiguration `json:"spec,omitempty"` Status *StatusApplyConfiguration `json:"status,omitempty"` } // ObjectStore constructs a declarative configuration of the ObjectStore type for use with // apply. func ObjectStore(name, namespace string) *ObjectStoreApplyConfiguration { b := &ObjectStoreApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) b.WithKind("ObjectStore") b.WithAPIVersion("jetstream.nats.io/v1beta2") return b } func (b ObjectStoreApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithKind(value string) *ObjectStoreApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithAPIVersion(value string) *ObjectStoreApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithName(value string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b } // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithGenerateName(value string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b } // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithNamespace(value string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b } // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithUID(value types.UID) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b } // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithResourceVersion(value string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b } // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithGeneration(value int64) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b } // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithCreationTimestamp(value metav1.Time) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b } // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. func (b *ObjectStoreApplyConfiguration) WithLabels(entries map[string]string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Labels[k] = v } return b } // WithAnnotations puts the entries into the Annotations field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. func (b *ObjectStoreApplyConfiguration) WithAnnotations(entries map[string]string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Annotations[k] = v } return b } // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. func (b *ObjectStoreApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { panic("nil value passed to WithOwnerReferences") } b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) } return b } // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. func (b *ObjectStoreApplyConfiguration) WithFinalizers(values ...string) *ObjectStoreApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) } return b } func (b *ObjectStoreApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } } // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithSpec(value *ObjectStoreSpecApplyConfiguration) *ObjectStoreApplyConfiguration { b.Spec = value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *ObjectStoreApplyConfiguration) WithStatus(value *StatusApplyConfiguration) *ObjectStoreApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. func (b *ObjectStoreApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. func (b *ObjectStoreApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. func (b *ObjectStoreApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. func (b *ObjectStoreApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/objectstorespec.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // ObjectStoreSpecApplyConfiguration represents a declarative configuration of the ObjectStoreSpec type for use // with apply. type ObjectStoreSpecApplyConfiguration struct { Bucket *string `json:"bucket,omitempty"` Description *string `json:"description,omitempty"` TTL *string `json:"ttl,omitempty"` MaxBytes *int `json:"maxBytes,omitempty"` Storage *string `json:"storage,omitempty"` Replicas *int `json:"replicas,omitempty"` Placement *StreamPlacementApplyConfiguration `json:"placement,omitempty"` Compression *bool `json:"compression,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } // ObjectStoreSpecApplyConfiguration constructs a declarative configuration of the ObjectStoreSpec type for use with // apply. func ObjectStoreSpec() *ObjectStoreSpecApplyConfiguration { return &ObjectStoreSpecApplyConfiguration{} } // WithBucket sets the Bucket field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Bucket field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithBucket(value string) *ObjectStoreSpecApplyConfiguration { b.Bucket = &value return b } // WithDescription sets the Description field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Description field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithDescription(value string) *ObjectStoreSpecApplyConfiguration { b.Description = &value return b } // WithTTL sets the TTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the TTL field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithTTL(value string) *ObjectStoreSpecApplyConfiguration { b.TTL = &value return b } // WithMaxBytes sets the MaxBytes field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxBytes field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithMaxBytes(value int) *ObjectStoreSpecApplyConfiguration { b.MaxBytes = &value return b } // WithStorage sets the Storage field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Storage field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithStorage(value string) *ObjectStoreSpecApplyConfiguration { b.Storage = &value return b } // WithReplicas sets the Replicas field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Replicas field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithReplicas(value int) *ObjectStoreSpecApplyConfiguration { b.Replicas = &value return b } // WithPlacement sets the Placement field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Placement field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithPlacement(value *StreamPlacementApplyConfiguration) *ObjectStoreSpecApplyConfiguration { b.Placement = value return b } // WithCompression sets the Compression field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Compression field is set to the value of the last call. func (b *ObjectStoreSpecApplyConfiguration) WithCompression(value bool) *ObjectStoreSpecApplyConfiguration { b.Compression = &value return b } // WithMetadata puts the entries into the Metadata field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Metadata field, // overwriting an existing map entries in Metadata field with the same key. func (b *ObjectStoreSpecApplyConfiguration) WithMetadata(entries map[string]string) *ObjectStoreSpecApplyConfiguration { if b.Metadata == nil && len(entries) > 0 { b.Metadata = make(map[string]string, len(entries)) } for k, v := range entries { b.Metadata[k] = v } return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/republish.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // RePublishApplyConfiguration represents a declarative configuration of the RePublish type for use // with apply. type RePublishApplyConfiguration struct { Source *string `json:"source,omitempty"` Destination *string `json:"destination,omitempty"` HeadersOnly *bool `json:"headers_only,omitempty"` } // RePublishApplyConfiguration constructs a declarative configuration of the RePublish type for use with // apply. func RePublish() *RePublishApplyConfiguration { return &RePublishApplyConfiguration{} } // WithSource sets the Source field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Source field is set to the value of the last call. func (b *RePublishApplyConfiguration) WithSource(value string) *RePublishApplyConfiguration { b.Source = &value return b } // WithDestination sets the Destination field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Destination field is set to the value of the last call. func (b *RePublishApplyConfiguration) WithDestination(value string) *RePublishApplyConfiguration { b.Destination = &value return b } // WithHeadersOnly sets the HeadersOnly field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the HeadersOnly field is set to the value of the last call. func (b *RePublishApplyConfiguration) WithHeadersOnly(value bool) *RePublishApplyConfiguration { b.HeadersOnly = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/secretref.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // SecretRefApplyConfiguration represents a declarative configuration of the SecretRef type for use // with apply. type SecretRefApplyConfiguration struct { Name *string `json:"name,omitempty"` } // SecretRefApplyConfiguration constructs a declarative configuration of the SecretRef type for use with // apply. func SecretRef() *SecretRefApplyConfiguration { return &SecretRefApplyConfiguration{} } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *SecretRefApplyConfiguration) WithName(value string) *SecretRefApplyConfiguration { b.Name = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/status.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // StatusApplyConfiguration represents a declarative configuration of the Status type for use // with apply. type StatusApplyConfiguration struct { ObservedGeneration *int64 `json:"observedGeneration,omitempty"` Conditions []ConditionApplyConfiguration `json:"conditions,omitempty"` } // StatusApplyConfiguration constructs a declarative configuration of the Status type for use with // apply. func Status() *StatusApplyConfiguration { return &StatusApplyConfiguration{} } // WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ObservedGeneration field is set to the value of the last call. func (b *StatusApplyConfiguration) WithObservedGeneration(value int64) *StatusApplyConfiguration { b.ObservedGeneration = &value return b } // WithConditions adds the given value to the Conditions field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Conditions field. func (b *StatusApplyConfiguration) WithConditions(values ...*ConditionApplyConfiguration) *StatusApplyConfiguration { for i := range values { if values[i] == nil { panic("nil value passed to WithConditions") } b.Conditions = append(b.Conditions, *values[i]) } return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/stream.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // StreamApplyConfiguration represents a declarative configuration of the Stream type for use // with apply. type StreamApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` Spec *StreamSpecApplyConfiguration `json:"spec,omitempty"` Status *StatusApplyConfiguration `json:"status,omitempty"` } // Stream constructs a declarative configuration of the Stream type for use with // apply. func Stream(name, namespace string) *StreamApplyConfiguration { b := &StreamApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) b.WithKind("Stream") b.WithAPIVersion("jetstream.nats.io/v1beta2") return b } func (b StreamApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. func (b *StreamApplyConfiguration) WithKind(value string) *StreamApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. func (b *StreamApplyConfiguration) WithAPIVersion(value string) *StreamApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *StreamApplyConfiguration) WithName(value string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b } // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. func (b *StreamApplyConfiguration) WithGenerateName(value string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b } // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. func (b *StreamApplyConfiguration) WithNamespace(value string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b } // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. func (b *StreamApplyConfiguration) WithUID(value types.UID) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b } // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. func (b *StreamApplyConfiguration) WithResourceVersion(value string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b } // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. func (b *StreamApplyConfiguration) WithGeneration(value int64) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b } // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. func (b *StreamApplyConfiguration) WithCreationTimestamp(value metav1.Time) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. func (b *StreamApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. func (b *StreamApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b } // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. func (b *StreamApplyConfiguration) WithLabels(entries map[string]string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Labels[k] = v } return b } // WithAnnotations puts the entries into the Annotations field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. func (b *StreamApplyConfiguration) WithAnnotations(entries map[string]string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) } for k, v := range entries { b.ObjectMetaApplyConfiguration.Annotations[k] = v } return b } // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. func (b *StreamApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { panic("nil value passed to WithOwnerReferences") } b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) } return b } // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. func (b *StreamApplyConfiguration) WithFinalizers(values ...string) *StreamApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) } return b } func (b *StreamApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } } // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. func (b *StreamApplyConfiguration) WithSpec(value *StreamSpecApplyConfiguration) *StreamApplyConfiguration { b.Spec = value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. func (b *StreamApplyConfiguration) WithStatus(value *StatusApplyConfiguration) *StreamApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. func (b *StreamApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. func (b *StreamApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. func (b *StreamApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. func (b *StreamApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/streamplacement.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // StreamPlacementApplyConfiguration represents a declarative configuration of the StreamPlacement type for use // with apply. type StreamPlacementApplyConfiguration struct { Cluster *string `json:"cluster,omitempty"` Tags []string `json:"tags,omitempty"` } // StreamPlacementApplyConfiguration constructs a declarative configuration of the StreamPlacement type for use with // apply. func StreamPlacement() *StreamPlacementApplyConfiguration { return &StreamPlacementApplyConfiguration{} } // WithCluster sets the Cluster field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Cluster field is set to the value of the last call. func (b *StreamPlacementApplyConfiguration) WithCluster(value string) *StreamPlacementApplyConfiguration { b.Cluster = &value return b } // WithTags adds the given value to the Tags field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Tags field. func (b *StreamPlacementApplyConfiguration) WithTags(values ...string) *StreamPlacementApplyConfiguration { for i := range values { b.Tags = append(b.Tags, values[i]) } return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/streamsource.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) // StreamSourceApplyConfiguration represents a declarative configuration of the StreamSource type for use // with apply. type StreamSourceApplyConfiguration struct { Name *string `json:"name,omitempty"` OptStartSeq *int `json:"optStartSeq,omitempty"` OptStartTime *string `json:"optStartTime,omitempty"` FilterSubject *string `json:"filterSubject,omitempty"` ExternalAPIPrefix *string `json:"externalApiPrefix,omitempty"` ExternalDeliverPrefix *string `json:"externalDeliverPrefix,omitempty"` SubjectTransforms []*jetstreamv1beta2.SubjectTransform `json:"subjectTransforms,omitempty"` } // StreamSourceApplyConfiguration constructs a declarative configuration of the StreamSource type for use with // apply. func StreamSource() *StreamSourceApplyConfiguration { return &StreamSourceApplyConfiguration{} } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithName(value string) *StreamSourceApplyConfiguration { b.Name = &value return b } // WithOptStartSeq sets the OptStartSeq field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the OptStartSeq field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithOptStartSeq(value int) *StreamSourceApplyConfiguration { b.OptStartSeq = &value return b } // WithOptStartTime sets the OptStartTime field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the OptStartTime field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithOptStartTime(value string) *StreamSourceApplyConfiguration { b.OptStartTime = &value return b } // WithFilterSubject sets the FilterSubject field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the FilterSubject field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithFilterSubject(value string) *StreamSourceApplyConfiguration { b.FilterSubject = &value return b } // WithExternalAPIPrefix sets the ExternalAPIPrefix field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ExternalAPIPrefix field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithExternalAPIPrefix(value string) *StreamSourceApplyConfiguration { b.ExternalAPIPrefix = &value return b } // WithExternalDeliverPrefix sets the ExternalDeliverPrefix field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ExternalDeliverPrefix field is set to the value of the last call. func (b *StreamSourceApplyConfiguration) WithExternalDeliverPrefix(value string) *StreamSourceApplyConfiguration { b.ExternalDeliverPrefix = &value return b } // WithSubjectTransforms adds the given value to the SubjectTransforms field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the SubjectTransforms field. func (b *StreamSourceApplyConfiguration) WithSubjectTransforms(values ...**jetstreamv1beta2.SubjectTransform) *StreamSourceApplyConfiguration { for i := range values { if values[i] == nil { panic("nil value passed to WithSubjectTransforms") } b.SubjectTransforms = append(b.SubjectTransforms, *values[i]) } return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/streamspec.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" ) // StreamSpecApplyConfiguration represents a declarative configuration of the StreamSpec type for use // with apply. type StreamSpecApplyConfiguration struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Subjects []string `json:"subjects,omitempty"` Retention *string `json:"retention,omitempty"` MaxConsumers *int `json:"maxConsumers,omitempty"` MaxMsgsPerSubject *int `json:"maxMsgsPerSubject,omitempty"` MaxMsgs *int `json:"maxMsgs,omitempty"` MaxBytes *int `json:"maxBytes,omitempty"` MaxAge *string `json:"maxAge,omitempty"` MaxMsgSize *int `json:"maxMsgSize,omitempty"` Storage *string `json:"storage,omitempty"` Discard *string `json:"discard,omitempty"` Replicas *int `json:"replicas,omitempty"` NoAck *bool `json:"noAck,omitempty"` DuplicateWindow *string `json:"duplicateWindow,omitempty"` Placement *StreamPlacementApplyConfiguration `json:"placement,omitempty"` Mirror *StreamSourceApplyConfiguration `json:"mirror,omitempty"` Sources []*jetstreamv1beta2.StreamSource `json:"sources,omitempty"` Compression *string `json:"compression,omitempty"` SubjectTransform *SubjectTransformApplyConfiguration `json:"subjectTransform,omitempty"` RePublish *RePublishApplyConfiguration `json:"republish,omitempty"` Sealed *bool `json:"sealed,omitempty"` DenyDelete *bool `json:"denyDelete,omitempty"` DenyPurge *bool `json:"denyPurge,omitempty"` AllowDirect *bool `json:"allowDirect,omitempty"` AllowRollup *bool `json:"allowRollup,omitempty"` MirrorDirect *bool `json:"mirrorDirect,omitempty"` DiscardPerSubject *bool `json:"discardPerSubject,omitempty"` FirstSequence *uint64 `json:"firstSequence,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` ConsumerLimits *ConsumerLimitsApplyConfiguration `json:"consumerLimits,omitempty"` AllowMsgTTL *bool `json:"allowMsgTtl,omitempty"` SubjectDeleteMarkerTTL *string `json:"subjectDeleteMarkerTtl,omitempty"` AllowMsgCounter *bool `json:"allowMsgCounter,omitempty"` AllowAtomicPublish *bool `json:"allowAtomicPublish,omitempty"` AllowMsgSchedules *bool `json:"allowMsgSchedules,omitempty"` PersistMode *string `json:"persistMode,omitempty"` } // StreamSpecApplyConfiguration constructs a declarative configuration of the StreamSpec type for use with // apply. func StreamSpec() *StreamSpecApplyConfiguration { return &StreamSpecApplyConfiguration{} } // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithName(value string) *StreamSpecApplyConfiguration { b.Name = &value return b } // WithDescription sets the Description field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Description field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDescription(value string) *StreamSpecApplyConfiguration { b.Description = &value return b } // WithSubjects adds the given value to the Subjects field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Subjects field. func (b *StreamSpecApplyConfiguration) WithSubjects(values ...string) *StreamSpecApplyConfiguration { for i := range values { b.Subjects = append(b.Subjects, values[i]) } return b } // WithRetention sets the Retention field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Retention field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithRetention(value string) *StreamSpecApplyConfiguration { b.Retention = &value return b } // WithMaxConsumers sets the MaxConsumers field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxConsumers field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxConsumers(value int) *StreamSpecApplyConfiguration { b.MaxConsumers = &value return b } // WithMaxMsgsPerSubject sets the MaxMsgsPerSubject field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxMsgsPerSubject field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxMsgsPerSubject(value int) *StreamSpecApplyConfiguration { b.MaxMsgsPerSubject = &value return b } // WithMaxMsgs sets the MaxMsgs field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxMsgs field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxMsgs(value int) *StreamSpecApplyConfiguration { b.MaxMsgs = &value return b } // WithMaxBytes sets the MaxBytes field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxBytes field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxBytes(value int) *StreamSpecApplyConfiguration { b.MaxBytes = &value return b } // WithMaxAge sets the MaxAge field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxAge field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxAge(value string) *StreamSpecApplyConfiguration { b.MaxAge = &value return b } // WithMaxMsgSize sets the MaxMsgSize field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MaxMsgSize field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMaxMsgSize(value int) *StreamSpecApplyConfiguration { b.MaxMsgSize = &value return b } // WithStorage sets the Storage field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Storage field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithStorage(value string) *StreamSpecApplyConfiguration { b.Storage = &value return b } // WithDiscard sets the Discard field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Discard field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDiscard(value string) *StreamSpecApplyConfiguration { b.Discard = &value return b } // WithReplicas sets the Replicas field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Replicas field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithReplicas(value int) *StreamSpecApplyConfiguration { b.Replicas = &value return b } // WithNoAck sets the NoAck field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the NoAck field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithNoAck(value bool) *StreamSpecApplyConfiguration { b.NoAck = &value return b } // WithDuplicateWindow sets the DuplicateWindow field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DuplicateWindow field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDuplicateWindow(value string) *StreamSpecApplyConfiguration { b.DuplicateWindow = &value return b } // WithPlacement sets the Placement field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Placement field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithPlacement(value *StreamPlacementApplyConfiguration) *StreamSpecApplyConfiguration { b.Placement = value return b } // WithMirror sets the Mirror field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Mirror field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMirror(value *StreamSourceApplyConfiguration) *StreamSpecApplyConfiguration { b.Mirror = value return b } // WithSources adds the given value to the Sources field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Sources field. func (b *StreamSpecApplyConfiguration) WithSources(values ...**jetstreamv1beta2.StreamSource) *StreamSpecApplyConfiguration { for i := range values { if values[i] == nil { panic("nil value passed to WithSources") } b.Sources = append(b.Sources, *values[i]) } return b } // WithCompression sets the Compression field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Compression field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithCompression(value string) *StreamSpecApplyConfiguration { b.Compression = &value return b } // WithSubjectTransform sets the SubjectTransform field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the SubjectTransform field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithSubjectTransform(value *SubjectTransformApplyConfiguration) *StreamSpecApplyConfiguration { b.SubjectTransform = value return b } // WithRePublish sets the RePublish field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RePublish field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithRePublish(value *RePublishApplyConfiguration) *StreamSpecApplyConfiguration { b.RePublish = value return b } // WithSealed sets the Sealed field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sealed field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithSealed(value bool) *StreamSpecApplyConfiguration { b.Sealed = &value return b } // WithDenyDelete sets the DenyDelete field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DenyDelete field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDenyDelete(value bool) *StreamSpecApplyConfiguration { b.DenyDelete = &value return b } // WithDenyPurge sets the DenyPurge field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DenyPurge field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDenyPurge(value bool) *StreamSpecApplyConfiguration { b.DenyPurge = &value return b } // WithAllowDirect sets the AllowDirect field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowDirect field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowDirect(value bool) *StreamSpecApplyConfiguration { b.AllowDirect = &value return b } // WithAllowRollup sets the AllowRollup field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowRollup field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowRollup(value bool) *StreamSpecApplyConfiguration { b.AllowRollup = &value return b } // WithMirrorDirect sets the MirrorDirect field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the MirrorDirect field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithMirrorDirect(value bool) *StreamSpecApplyConfiguration { b.MirrorDirect = &value return b } // WithDiscardPerSubject sets the DiscardPerSubject field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DiscardPerSubject field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithDiscardPerSubject(value bool) *StreamSpecApplyConfiguration { b.DiscardPerSubject = &value return b } // WithFirstSequence sets the FirstSequence field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the FirstSequence field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithFirstSequence(value uint64) *StreamSpecApplyConfiguration { b.FirstSequence = &value return b } // WithMetadata puts the entries into the Metadata field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Metadata field, // overwriting an existing map entries in Metadata field with the same key. func (b *StreamSpecApplyConfiguration) WithMetadata(entries map[string]string) *StreamSpecApplyConfiguration { if b.Metadata == nil && len(entries) > 0 { b.Metadata = make(map[string]string, len(entries)) } for k, v := range entries { b.Metadata[k] = v } return b } // WithConsumerLimits sets the ConsumerLimits field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ConsumerLimits field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithConsumerLimits(value *ConsumerLimitsApplyConfiguration) *StreamSpecApplyConfiguration { b.ConsumerLimits = value return b } // WithAllowMsgTTL sets the AllowMsgTTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowMsgTTL field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowMsgTTL(value bool) *StreamSpecApplyConfiguration { b.AllowMsgTTL = &value return b } // WithSubjectDeleteMarkerTTL sets the SubjectDeleteMarkerTTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the SubjectDeleteMarkerTTL field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithSubjectDeleteMarkerTTL(value string) *StreamSpecApplyConfiguration { b.SubjectDeleteMarkerTTL = &value return b } // WithAllowMsgCounter sets the AllowMsgCounter field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowMsgCounter field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowMsgCounter(value bool) *StreamSpecApplyConfiguration { b.AllowMsgCounter = &value return b } // WithAllowAtomicPublish sets the AllowAtomicPublish field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowAtomicPublish field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowAtomicPublish(value bool) *StreamSpecApplyConfiguration { b.AllowAtomicPublish = &value return b } // WithAllowMsgSchedules sets the AllowMsgSchedules field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AllowMsgSchedules field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithAllowMsgSchedules(value bool) *StreamSpecApplyConfiguration { b.AllowMsgSchedules = &value return b } // WithPersistMode sets the PersistMode field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the PersistMode field is set to the value of the last call. func (b *StreamSpecApplyConfiguration) WithPersistMode(value string) *StreamSpecApplyConfiguration { b.PersistMode = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/subjecttransform.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // SubjectTransformApplyConfiguration represents a declarative configuration of the SubjectTransform type for use // with apply. type SubjectTransformApplyConfiguration struct { Source *string `json:"source,omitempty"` Dest *string `json:"dest,omitempty"` } // SubjectTransformApplyConfiguration constructs a declarative configuration of the SubjectTransform type for use with // apply. func SubjectTransform() *SubjectTransformApplyConfiguration { return &SubjectTransformApplyConfiguration{} } // WithSource sets the Source field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Source field is set to the value of the last call. func (b *SubjectTransformApplyConfiguration) WithSource(value string) *SubjectTransformApplyConfiguration { b.Source = &value return b } // WithDest sets the Dest field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Dest field is set to the value of the last call. func (b *SubjectTransformApplyConfiguration) WithDest(value string) *SubjectTransformApplyConfiguration { b.Dest = &value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/tls.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // TLSApplyConfiguration represents a declarative configuration of the TLS type for use // with apply. type TLSApplyConfiguration struct { ClientCert *string `json:"clientCert,omitempty"` ClientKey *string `json:"clientKey,omitempty"` RootCAs []string `json:"rootCas,omitempty"` } // TLSApplyConfiguration constructs a declarative configuration of the TLS type for use with // apply. func TLS() *TLSApplyConfiguration { return &TLSApplyConfiguration{} } // WithClientCert sets the ClientCert field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ClientCert field is set to the value of the last call. func (b *TLSApplyConfiguration) WithClientCert(value string) *TLSApplyConfiguration { b.ClientCert = &value return b } // WithClientKey sets the ClientKey field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ClientKey field is set to the value of the last call. func (b *TLSApplyConfiguration) WithClientKey(value string) *TLSApplyConfiguration { b.ClientKey = &value return b } // WithRootCAs adds the given value to the RootCAs field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the RootCAs field. func (b *TLSApplyConfiguration) WithRootCAs(values ...string) *TLSApplyConfiguration { for i := range values { b.RootCAs = append(b.RootCAs, values[i]) } return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/tlssecret.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // TLSSecretApplyConfiguration represents a declarative configuration of the TLSSecret type for use // with apply. type TLSSecretApplyConfiguration struct { ClientCert *string `json:"cert,omitempty"` ClientKey *string `json:"key,omitempty"` RootCAs *string `json:"ca,omitempty"` Secret *SecretRefApplyConfiguration `json:"secret,omitempty"` } // TLSSecretApplyConfiguration constructs a declarative configuration of the TLSSecret type for use with // apply. func TLSSecret() *TLSSecretApplyConfiguration { return &TLSSecretApplyConfiguration{} } // WithClientCert sets the ClientCert field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ClientCert field is set to the value of the last call. func (b *TLSSecretApplyConfiguration) WithClientCert(value string) *TLSSecretApplyConfiguration { b.ClientCert = &value return b } // WithClientKey sets the ClientKey field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ClientKey field is set to the value of the last call. func (b *TLSSecretApplyConfiguration) WithClientKey(value string) *TLSSecretApplyConfiguration { b.ClientKey = &value return b } // WithRootCAs sets the RootCAs field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RootCAs field is set to the value of the last call. func (b *TLSSecretApplyConfiguration) WithRootCAs(value string) *TLSSecretApplyConfiguration { b.RootCAs = &value return b } // WithSecret sets the Secret field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Secret field is set to the value of the last call. func (b *TLSSecretApplyConfiguration) WithSecret(value *SecretRefApplyConfiguration) *TLSSecretApplyConfiguration { b.Secret = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/tokensecret.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // TokenSecretApplyConfiguration represents a declarative configuration of the TokenSecret type for use // with apply. type TokenSecretApplyConfiguration struct { Token *string `json:"token,omitempty"` Secret *SecretRefApplyConfiguration `json:"secret,omitempty"` } // TokenSecretApplyConfiguration constructs a declarative configuration of the TokenSecret type for use with // apply. func TokenSecret() *TokenSecretApplyConfiguration { return &TokenSecretApplyConfiguration{} } // WithToken sets the Token field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Token field is set to the value of the last call. func (b *TokenSecretApplyConfiguration) WithToken(value string) *TokenSecretApplyConfiguration { b.Token = &value return b } // WithSecret sets the Secret field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Secret field is set to the value of the last call. func (b *TokenSecretApplyConfiguration) WithSecret(value *SecretRefApplyConfiguration) *TokenSecretApplyConfiguration { b.Secret = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2/user.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package v1beta2 // UserApplyConfiguration represents a declarative configuration of the User type for use // with apply. type UserApplyConfiguration struct { User *string `json:"user,omitempty"` Password *string `json:"password,omitempty"` Secret *SecretRefApplyConfiguration `json:"secret,omitempty"` } // UserApplyConfiguration constructs a declarative configuration of the User type for use with // apply. func User() *UserApplyConfiguration { return &UserApplyConfiguration{} } // WithUser sets the User field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the User field is set to the value of the last call. func (b *UserApplyConfiguration) WithUser(value string) *UserApplyConfiguration { b.User = &value return b } // WithPassword sets the Password field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Password field is set to the value of the last call. func (b *UserApplyConfiguration) WithPassword(value string) *UserApplyConfiguration { b.Password = &value return b } // WithSecret sets the Secret field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Secret field is set to the value of the last call. func (b *UserApplyConfiguration) WithSecret(value *SecretRefApplyConfiguration) *UserApplyConfiguration { b.Secret = value return b } ================================================ FILE: pkg/jetstream/generated/applyconfiguration/utils.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by applyconfiguration-gen. DO NOT EDIT. package applyconfiguration import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" internal "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/internal" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" managedfields "k8s.io/apimachinery/pkg/util/managedfields" ) // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no // apply configuration type exists for the given GroupVersionKind. func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=jetstream.nats.io, Version=v1beta2 case v1beta2.SchemeGroupVersion.WithKind("Account"): return &jetstreamv1beta2.AccountApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("AccountSpec"): return &jetstreamv1beta2.AccountSpecApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("BaseStreamConfig"): return &jetstreamv1beta2.BaseStreamConfigApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("Condition"): return &jetstreamv1beta2.ConditionApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("ConnectionOpts"): return &jetstreamv1beta2.ConnectionOptsApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("Consumer"): return &jetstreamv1beta2.ConsumerApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("ConsumerLimits"): return &jetstreamv1beta2.ConsumerLimitsApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("ConsumerSpec"): return &jetstreamv1beta2.ConsumerSpecApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("CredsSecret"): return &jetstreamv1beta2.CredsSecretApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("KeyValue"): return &jetstreamv1beta2.KeyValueApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("KeyValueSpec"): return &jetstreamv1beta2.KeyValueSpecApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("NKeySecret"): return &jetstreamv1beta2.NKeySecretApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("ObjectStore"): return &jetstreamv1beta2.ObjectStoreApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("ObjectStoreSpec"): return &jetstreamv1beta2.ObjectStoreSpecApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("RePublish"): return &jetstreamv1beta2.RePublishApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("SecretRef"): return &jetstreamv1beta2.SecretRefApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("Status"): return &jetstreamv1beta2.StatusApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("Stream"): return &jetstreamv1beta2.StreamApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("StreamPlacement"): return &jetstreamv1beta2.StreamPlacementApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("StreamSource"): return &jetstreamv1beta2.StreamSourceApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("StreamSpec"): return &jetstreamv1beta2.StreamSpecApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("SubjectTransform"): return &jetstreamv1beta2.SubjectTransformApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("TLS"): return &jetstreamv1beta2.TLSApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("TLSSecret"): return &jetstreamv1beta2.TLSSecretApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("TokenSecret"): return &jetstreamv1beta2.TokenSecretApplyConfiguration{} case v1beta2.SchemeGroupVersion.WithKind("User"): return &jetstreamv1beta2.UserApplyConfiguration{} } return nil } func NewTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { return managedfields.NewSchemeTypeConverter(scheme, internal.Parser()) } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/clientset.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package versioned import ( fmt "fmt" http "net/http" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { Discovery() discovery.DiscoveryInterface JetstreamV1beta2() jetstreamv1beta2.JetstreamV1beta2Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient jetstreamV1beta2 *jetstreamv1beta2.JetstreamV1beta2Client } // JetstreamV1beta2 retrieves the JetstreamV1beta2Client func (c *Clientset) JetstreamV1beta2() jetstreamv1beta2.JetstreamV1beta2Interface { return c.jetstreamV1beta2 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { return nil } return c.DiscoveryClient } // NewForConfig creates a new Clientset for the given config. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfig will generate a rate-limiter in configShallowCopy. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.UserAgent == "" { configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() } // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { return nil, err } return NewForConfigAndClient(&configShallowCopy, httpClient) } // NewForConfigAndClient creates a new Clientset for the given config and http client. // Note the http client provided takes precedence over the configured transport values. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { if configShallowCopy.Burst <= 0 { return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset var err error cs.jetstreamV1beta2, err = jetstreamv1beta2.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { cs, err := NewForConfig(c) if err != nil { panic(err) } return cs } // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset cs.jetstreamV1beta2 = jetstreamv1beta2.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/fake/clientset_generated.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( applyconfiguration "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration" clientset "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" fakejetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. // // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves // server side apply testing. NewClientset is only available when apply configurations are generated (e.g. // via --with-applyconfig). func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { var opts metav1.ListOptions if watchActcion, ok := action.(testing.WatchActionImpl); ok { opts = watchActcion.ListOptions } gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns, opts) if err != nil { return false, nil, err } return true, watch, nil }) return cs } // Clientset implements clientset.Interface. Meant to be embedded into a // struct to get a default implementation. This makes faking out just the method // you want to test easier. type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } // NewClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. func NewClientset(objects ...runtime.Object) *Clientset { o := testing.NewFieldManagedObjectTracker( scheme, codecs.UniversalDecoder(), applyconfiguration.NewTypeConverter(scheme), ) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { var opts metav1.ListOptions if watchAction, ok := action.(testing.WatchActionImpl); ok { opts = watchAction.ListOptions } gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns, opts) if err != nil { return false, nil, err } return true, watch, nil }) return cs } var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} ) // JetstreamV1beta2 retrieves the JetstreamV1beta2Client func (c *Clientset) JetstreamV1beta2() jetstreamv1beta2.JetstreamV1beta2Interface { return &fakejetstreamv1beta2.FakeJetstreamV1beta2{Fake: &c.Fake} } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/fake/doc.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated fake clientset. package fake ================================================ FILE: pkg/jetstream/generated/clientset/versioned/fake/register.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ jetstreamv1beta2.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(scheme)) } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/scheme/doc.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. // This package contains the scheme of the automatically generated clientset. package scheme ================================================ FILE: pkg/jetstream/generated/clientset/versioned/scheme/register.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package scheme import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ jetstreamv1beta2.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/account.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( context "context" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" applyconfigurationjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) // AccountsGetter has a method to return a AccountInterface. // A group's client should implement this interface. type AccountsGetter interface { Accounts(namespace string) AccountInterface } // AccountInterface has methods to work with Account resources. type AccountInterface interface { Create(ctx context.Context, account *jetstreamv1beta2.Account, opts v1.CreateOptions) (*jetstreamv1beta2.Account, error) Update(ctx context.Context, account *jetstreamv1beta2.Account, opts v1.UpdateOptions) (*jetstreamv1beta2.Account, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, account *jetstreamv1beta2.Account, opts v1.UpdateOptions) (*jetstreamv1beta2.Account, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*jetstreamv1beta2.Account, error) List(ctx context.Context, opts v1.ListOptions) (*jetstreamv1beta2.AccountList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *jetstreamv1beta2.Account, err error) Apply(ctx context.Context, account *applyconfigurationjetstreamv1beta2.AccountApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Account, err error) // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). ApplyStatus(ctx context.Context, account *applyconfigurationjetstreamv1beta2.AccountApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Account, err error) AccountExpansion } // accounts implements AccountInterface type accounts struct { *gentype.ClientWithListAndApply[*jetstreamv1beta2.Account, *jetstreamv1beta2.AccountList, *applyconfigurationjetstreamv1beta2.AccountApplyConfiguration] } // newAccounts returns a Accounts func newAccounts(c *JetstreamV1beta2Client, namespace string) *accounts { return &accounts{ gentype.NewClientWithListAndApply[*jetstreamv1beta2.Account, *jetstreamv1beta2.AccountList, *applyconfigurationjetstreamv1beta2.AccountApplyConfiguration]( "accounts", c.RESTClient(), scheme.ParameterCodec, namespace, func() *jetstreamv1beta2.Account { return &jetstreamv1beta2.Account{} }, func() *jetstreamv1beta2.AccountList { return &jetstreamv1beta2.AccountList{} }, ), } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/consumer.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( context "context" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" applyconfigurationjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) // ConsumersGetter has a method to return a ConsumerInterface. // A group's client should implement this interface. type ConsumersGetter interface { Consumers(namespace string) ConsumerInterface } // ConsumerInterface has methods to work with Consumer resources. type ConsumerInterface interface { Create(ctx context.Context, consumer *jetstreamv1beta2.Consumer, opts v1.CreateOptions) (*jetstreamv1beta2.Consumer, error) Update(ctx context.Context, consumer *jetstreamv1beta2.Consumer, opts v1.UpdateOptions) (*jetstreamv1beta2.Consumer, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, consumer *jetstreamv1beta2.Consumer, opts v1.UpdateOptions) (*jetstreamv1beta2.Consumer, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*jetstreamv1beta2.Consumer, error) List(ctx context.Context, opts v1.ListOptions) (*jetstreamv1beta2.ConsumerList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *jetstreamv1beta2.Consumer, err error) Apply(ctx context.Context, consumer *applyconfigurationjetstreamv1beta2.ConsumerApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Consumer, err error) // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). ApplyStatus(ctx context.Context, consumer *applyconfigurationjetstreamv1beta2.ConsumerApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Consumer, err error) ConsumerExpansion } // consumers implements ConsumerInterface type consumers struct { *gentype.ClientWithListAndApply[*jetstreamv1beta2.Consumer, *jetstreamv1beta2.ConsumerList, *applyconfigurationjetstreamv1beta2.ConsumerApplyConfiguration] } // newConsumers returns a Consumers func newConsumers(c *JetstreamV1beta2Client, namespace string) *consumers { return &consumers{ gentype.NewClientWithListAndApply[*jetstreamv1beta2.Consumer, *jetstreamv1beta2.ConsumerList, *applyconfigurationjetstreamv1beta2.ConsumerApplyConfiguration]( "consumers", c.RESTClient(), scheme.ParameterCodec, namespace, func() *jetstreamv1beta2.Consumer { return &jetstreamv1beta2.Consumer{} }, func() *jetstreamv1beta2.ConsumerList { return &jetstreamv1beta2.ConsumerList{} }, ), } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/doc.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v1beta2 ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/doc.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_account.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" typedjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" gentype "k8s.io/client-go/gentype" ) // fakeAccounts implements AccountInterface type fakeAccounts struct { *gentype.FakeClientWithListAndApply[*v1beta2.Account, *v1beta2.AccountList, *jetstreamv1beta2.AccountApplyConfiguration] Fake *FakeJetstreamV1beta2 } func newFakeAccounts(fake *FakeJetstreamV1beta2, namespace string) typedjetstreamv1beta2.AccountInterface { return &fakeAccounts{ gentype.NewFakeClientWithListAndApply[*v1beta2.Account, *v1beta2.AccountList, *jetstreamv1beta2.AccountApplyConfiguration]( fake.Fake, namespace, v1beta2.SchemeGroupVersion.WithResource("accounts"), v1beta2.SchemeGroupVersion.WithKind("Account"), func() *v1beta2.Account { return &v1beta2.Account{} }, func() *v1beta2.AccountList { return &v1beta2.AccountList{} }, func(dst, src *v1beta2.AccountList) { dst.ListMeta = src.ListMeta }, func(list *v1beta2.AccountList) []*v1beta2.Account { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta2.AccountList, items []*v1beta2.Account) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_consumer.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" typedjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" gentype "k8s.io/client-go/gentype" ) // fakeConsumers implements ConsumerInterface type fakeConsumers struct { *gentype.FakeClientWithListAndApply[*v1beta2.Consumer, *v1beta2.ConsumerList, *jetstreamv1beta2.ConsumerApplyConfiguration] Fake *FakeJetstreamV1beta2 } func newFakeConsumers(fake *FakeJetstreamV1beta2, namespace string) typedjetstreamv1beta2.ConsumerInterface { return &fakeConsumers{ gentype.NewFakeClientWithListAndApply[*v1beta2.Consumer, *v1beta2.ConsumerList, *jetstreamv1beta2.ConsumerApplyConfiguration]( fake.Fake, namespace, v1beta2.SchemeGroupVersion.WithResource("consumers"), v1beta2.SchemeGroupVersion.WithKind("Consumer"), func() *v1beta2.Consumer { return &v1beta2.Consumer{} }, func() *v1beta2.ConsumerList { return &v1beta2.ConsumerList{} }, func(dst, src *v1beta2.ConsumerList) { dst.ListMeta = src.ListMeta }, func(list *v1beta2.ConsumerList) []*v1beta2.Consumer { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta2.ConsumerList, items []*v1beta2.Consumer) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_jetstream_client.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) type FakeJetstreamV1beta2 struct { *testing.Fake } func (c *FakeJetstreamV1beta2) Accounts(namespace string) v1beta2.AccountInterface { return newFakeAccounts(c, namespace) } func (c *FakeJetstreamV1beta2) Consumers(namespace string) v1beta2.ConsumerInterface { return newFakeConsumers(c, namespace) } func (c *FakeJetstreamV1beta2) KeyValues(namespace string) v1beta2.KeyValueInterface { return newFakeKeyValues(c, namespace) } func (c *FakeJetstreamV1beta2) ObjectStores(namespace string) v1beta2.ObjectStoreInterface { return newFakeObjectStores(c, namespace) } func (c *FakeJetstreamV1beta2) Streams(namespace string) v1beta2.StreamInterface { return newFakeStreams(c, namespace) } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeJetstreamV1beta2) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_keyvalue.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" typedjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" gentype "k8s.io/client-go/gentype" ) // fakeKeyValues implements KeyValueInterface type fakeKeyValues struct { *gentype.FakeClientWithListAndApply[*v1beta2.KeyValue, *v1beta2.KeyValueList, *jetstreamv1beta2.KeyValueApplyConfiguration] Fake *FakeJetstreamV1beta2 } func newFakeKeyValues(fake *FakeJetstreamV1beta2, namespace string) typedjetstreamv1beta2.KeyValueInterface { return &fakeKeyValues{ gentype.NewFakeClientWithListAndApply[*v1beta2.KeyValue, *v1beta2.KeyValueList, *jetstreamv1beta2.KeyValueApplyConfiguration]( fake.Fake, namespace, v1beta2.SchemeGroupVersion.WithResource("keyvalues"), v1beta2.SchemeGroupVersion.WithKind("KeyValue"), func() *v1beta2.KeyValue { return &v1beta2.KeyValue{} }, func() *v1beta2.KeyValueList { return &v1beta2.KeyValueList{} }, func(dst, src *v1beta2.KeyValueList) { dst.ListMeta = src.ListMeta }, func(list *v1beta2.KeyValueList) []*v1beta2.KeyValue { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta2.KeyValueList, items []*v1beta2.KeyValue) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_objectstore.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" typedjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" gentype "k8s.io/client-go/gentype" ) // fakeObjectStores implements ObjectStoreInterface type fakeObjectStores struct { *gentype.FakeClientWithListAndApply[*v1beta2.ObjectStore, *v1beta2.ObjectStoreList, *jetstreamv1beta2.ObjectStoreApplyConfiguration] Fake *FakeJetstreamV1beta2 } func newFakeObjectStores(fake *FakeJetstreamV1beta2, namespace string) typedjetstreamv1beta2.ObjectStoreInterface { return &fakeObjectStores{ gentype.NewFakeClientWithListAndApply[*v1beta2.ObjectStore, *v1beta2.ObjectStoreList, *jetstreamv1beta2.ObjectStoreApplyConfiguration]( fake.Fake, namespace, v1beta2.SchemeGroupVersion.WithResource("objectstores"), v1beta2.SchemeGroupVersion.WithKind("ObjectStore"), func() *v1beta2.ObjectStore { return &v1beta2.ObjectStore{} }, func() *v1beta2.ObjectStoreList { return &v1beta2.ObjectStoreList{} }, func(dst, src *v1beta2.ObjectStoreList) { dst.ListMeta = src.ListMeta }, func(list *v1beta2.ObjectStoreList) []*v1beta2.ObjectStore { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta2.ObjectStoreList, items []*v1beta2.ObjectStore) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/fake/fake_stream.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package fake import ( v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" typedjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2" gentype "k8s.io/client-go/gentype" ) // fakeStreams implements StreamInterface type fakeStreams struct { *gentype.FakeClientWithListAndApply[*v1beta2.Stream, *v1beta2.StreamList, *jetstreamv1beta2.StreamApplyConfiguration] Fake *FakeJetstreamV1beta2 } func newFakeStreams(fake *FakeJetstreamV1beta2, namespace string) typedjetstreamv1beta2.StreamInterface { return &fakeStreams{ gentype.NewFakeClientWithListAndApply[*v1beta2.Stream, *v1beta2.StreamList, *jetstreamv1beta2.StreamApplyConfiguration]( fake.Fake, namespace, v1beta2.SchemeGroupVersion.WithResource("streams"), v1beta2.SchemeGroupVersion.WithKind("Stream"), func() *v1beta2.Stream { return &v1beta2.Stream{} }, func() *v1beta2.StreamList { return &v1beta2.StreamList{} }, func(dst, src *v1beta2.StreamList) { dst.ListMeta = src.ListMeta }, func(list *v1beta2.StreamList) []*v1beta2.Stream { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta2.StreamList, items []*v1beta2.Stream) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/generated_expansion.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 type AccountExpansion interface{} type ConsumerExpansion interface{} type KeyValueExpansion interface{} type ObjectStoreExpansion interface{} type StreamExpansion interface{} ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/jetstream_client.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( http "net/http" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) type JetstreamV1beta2Interface interface { RESTClient() rest.Interface AccountsGetter ConsumersGetter KeyValuesGetter ObjectStoresGetter StreamsGetter } // JetstreamV1beta2Client is used to interact with features provided by the jetstream.nats.io group. type JetstreamV1beta2Client struct { restClient rest.Interface } func (c *JetstreamV1beta2Client) Accounts(namespace string) AccountInterface { return newAccounts(c, namespace) } func (c *JetstreamV1beta2Client) Consumers(namespace string) ConsumerInterface { return newConsumers(c, namespace) } func (c *JetstreamV1beta2Client) KeyValues(namespace string) KeyValueInterface { return newKeyValues(c, namespace) } func (c *JetstreamV1beta2Client) ObjectStores(namespace string) ObjectStoreInterface { return newObjectStores(c, namespace) } func (c *JetstreamV1beta2Client) Streams(namespace string) StreamInterface { return newStreams(c, namespace) } // NewForConfig creates a new JetstreamV1beta2Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*JetstreamV1beta2Client, error) { config := *c setConfigDefaults(&config) httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err } return NewForConfigAndClient(&config, httpClient) } // NewForConfigAndClient creates a new JetstreamV1beta2Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*JetstreamV1beta2Client, error) { config := *c setConfigDefaults(&config) client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } return &JetstreamV1beta2Client{client}, nil } // NewForConfigOrDie creates a new JetstreamV1beta2Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *JetstreamV1beta2Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new JetstreamV1beta2Client for the given RESTClient. func New(c rest.Interface) *JetstreamV1beta2Client { return &JetstreamV1beta2Client{c} } func setConfigDefaults(config *rest.Config) { gv := jetstreamv1beta2.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *JetstreamV1beta2Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/keyvalue.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( context "context" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" applyconfigurationjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) // KeyValuesGetter has a method to return a KeyValueInterface. // A group's client should implement this interface. type KeyValuesGetter interface { KeyValues(namespace string) KeyValueInterface } // KeyValueInterface has methods to work with KeyValue resources. type KeyValueInterface interface { Create(ctx context.Context, keyValue *jetstreamv1beta2.KeyValue, opts v1.CreateOptions) (*jetstreamv1beta2.KeyValue, error) Update(ctx context.Context, keyValue *jetstreamv1beta2.KeyValue, opts v1.UpdateOptions) (*jetstreamv1beta2.KeyValue, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, keyValue *jetstreamv1beta2.KeyValue, opts v1.UpdateOptions) (*jetstreamv1beta2.KeyValue, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*jetstreamv1beta2.KeyValue, error) List(ctx context.Context, opts v1.ListOptions) (*jetstreamv1beta2.KeyValueList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *jetstreamv1beta2.KeyValue, err error) Apply(ctx context.Context, keyValue *applyconfigurationjetstreamv1beta2.KeyValueApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.KeyValue, err error) // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). ApplyStatus(ctx context.Context, keyValue *applyconfigurationjetstreamv1beta2.KeyValueApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.KeyValue, err error) KeyValueExpansion } // keyValues implements KeyValueInterface type keyValues struct { *gentype.ClientWithListAndApply[*jetstreamv1beta2.KeyValue, *jetstreamv1beta2.KeyValueList, *applyconfigurationjetstreamv1beta2.KeyValueApplyConfiguration] } // newKeyValues returns a KeyValues func newKeyValues(c *JetstreamV1beta2Client, namespace string) *keyValues { return &keyValues{ gentype.NewClientWithListAndApply[*jetstreamv1beta2.KeyValue, *jetstreamv1beta2.KeyValueList, *applyconfigurationjetstreamv1beta2.KeyValueApplyConfiguration]( "keyvalues", c.RESTClient(), scheme.ParameterCodec, namespace, func() *jetstreamv1beta2.KeyValue { return &jetstreamv1beta2.KeyValue{} }, func() *jetstreamv1beta2.KeyValueList { return &jetstreamv1beta2.KeyValueList{} }, ), } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/objectstore.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( context "context" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" applyconfigurationjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) // ObjectStoresGetter has a method to return a ObjectStoreInterface. // A group's client should implement this interface. type ObjectStoresGetter interface { ObjectStores(namespace string) ObjectStoreInterface } // ObjectStoreInterface has methods to work with ObjectStore resources. type ObjectStoreInterface interface { Create(ctx context.Context, objectStore *jetstreamv1beta2.ObjectStore, opts v1.CreateOptions) (*jetstreamv1beta2.ObjectStore, error) Update(ctx context.Context, objectStore *jetstreamv1beta2.ObjectStore, opts v1.UpdateOptions) (*jetstreamv1beta2.ObjectStore, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, objectStore *jetstreamv1beta2.ObjectStore, opts v1.UpdateOptions) (*jetstreamv1beta2.ObjectStore, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*jetstreamv1beta2.ObjectStore, error) List(ctx context.Context, opts v1.ListOptions) (*jetstreamv1beta2.ObjectStoreList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *jetstreamv1beta2.ObjectStore, err error) Apply(ctx context.Context, objectStore *applyconfigurationjetstreamv1beta2.ObjectStoreApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.ObjectStore, err error) // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). ApplyStatus(ctx context.Context, objectStore *applyconfigurationjetstreamv1beta2.ObjectStoreApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.ObjectStore, err error) ObjectStoreExpansion } // objectStores implements ObjectStoreInterface type objectStores struct { *gentype.ClientWithListAndApply[*jetstreamv1beta2.ObjectStore, *jetstreamv1beta2.ObjectStoreList, *applyconfigurationjetstreamv1beta2.ObjectStoreApplyConfiguration] } // newObjectStores returns a ObjectStores func newObjectStores(c *JetstreamV1beta2Client, namespace string) *objectStores { return &objectStores{ gentype.NewClientWithListAndApply[*jetstreamv1beta2.ObjectStore, *jetstreamv1beta2.ObjectStoreList, *applyconfigurationjetstreamv1beta2.ObjectStoreApplyConfiguration]( "objectstores", c.RESTClient(), scheme.ParameterCodec, namespace, func() *jetstreamv1beta2.ObjectStore { return &jetstreamv1beta2.ObjectStore{} }, func() *jetstreamv1beta2.ObjectStoreList { return &jetstreamv1beta2.ObjectStoreList{} }, ), } } ================================================ FILE: pkg/jetstream/generated/clientset/versioned/typed/jetstream/v1beta2/stream.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1beta2 import ( context "context" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" applyconfigurationjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/applyconfiguration/jetstream/v1beta2" scheme "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" ) // StreamsGetter has a method to return a StreamInterface. // A group's client should implement this interface. type StreamsGetter interface { Streams(namespace string) StreamInterface } // StreamInterface has methods to work with Stream resources. type StreamInterface interface { Create(ctx context.Context, stream *jetstreamv1beta2.Stream, opts v1.CreateOptions) (*jetstreamv1beta2.Stream, error) Update(ctx context.Context, stream *jetstreamv1beta2.Stream, opts v1.UpdateOptions) (*jetstreamv1beta2.Stream, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, stream *jetstreamv1beta2.Stream, opts v1.UpdateOptions) (*jetstreamv1beta2.Stream, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*jetstreamv1beta2.Stream, error) List(ctx context.Context, opts v1.ListOptions) (*jetstreamv1beta2.StreamList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *jetstreamv1beta2.Stream, err error) Apply(ctx context.Context, stream *applyconfigurationjetstreamv1beta2.StreamApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Stream, err error) // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). ApplyStatus(ctx context.Context, stream *applyconfigurationjetstreamv1beta2.StreamApplyConfiguration, opts v1.ApplyOptions) (result *jetstreamv1beta2.Stream, err error) StreamExpansion } // streams implements StreamInterface type streams struct { *gentype.ClientWithListAndApply[*jetstreamv1beta2.Stream, *jetstreamv1beta2.StreamList, *applyconfigurationjetstreamv1beta2.StreamApplyConfiguration] } // newStreams returns a Streams func newStreams(c *JetstreamV1beta2Client, namespace string) *streams { return &streams{ gentype.NewClientWithListAndApply[*jetstreamv1beta2.Stream, *jetstreamv1beta2.StreamList, *applyconfigurationjetstreamv1beta2.StreamApplyConfiguration]( "streams", c.RESTClient(), scheme.ParameterCodec, namespace, func() *jetstreamv1beta2.Stream { return &jetstreamv1beta2.Stream{} }, func() *jetstreamv1beta2.StreamList { return &jetstreamv1beta2.StreamList{} }, ), } } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/factory.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( reflect "reflect" sync "sync" time "time" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstream "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/jetstream" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory type sharedInformerFactory struct { client versioned.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration transform cache.TransformFunc informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool // wg tracks how many goroutines were started. wg sync.WaitGroup // shuttingDown is true when Shutdown has been called. It may still be running // because it needs to wait for goroutines. shuttingDown bool } // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for k, v := range resyncConfig { factory.customResync[reflect.TypeOf(k)] = v } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // WithNamespace limits the SharedInformerFactory to the specified namespace. func WithNamespace(namespace string) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.namespace = namespace return factory } } // WithTransform sets a transform on all informers. func WithTransform(transform cache.TransformFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.transform = transform return factory } } // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) } // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // Apply all options for _, opt := range options { factory = opt(factory) } return factory } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() if f.shuttingDown { return } for informerType, informer := range f.informers { if !f.startedInformers[informerType] { f.wg.Add(1) // We need a new variable in each loop iteration, // otherwise the goroutine would use the loop variable // and that keeps changing. informer := informer go func() { defer f.wg.Done() informer.Run(stopCh) }() f.startedInformers[informerType] = true } } } func (f *sharedInformerFactory) Shutdown() { f.lock.Lock() f.shuttingDown = true f.lock.Unlock() // Will return immediately if there is nothing to wait for. f.wg.Wait() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[reflect.Type]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer } } return informers }() res := map[reflect.Type]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } // InformerFor returns the SharedIndexInformer for obj using an internal // client. func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) informer.SetTransform(f.transform) f.informers[informerType] = informer return informer } // SharedInformerFactory provides shared informers for resources in all known // API group versions. // // It is typically used like this: // // ctx, cancel := context.Background() // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() // factory.Start(ctx.Done()) // Start processing these informers. // synced := factory.WaitForCacheSync(ctx.Done()) // for v, ok := range synced { // if !ok { // fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) // return // } // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) // factory.Start(ctx.Done()) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. Start(stopCh <-chan struct{}) // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. // // In addition, Shutdown blocks until all goroutines have terminated. For that // to happen, the close channel(s) that they were started with must be closed, // either before Shutdown gets called or while it is waiting. // // Shutdown may be called multiple times, even concurrently. All such calls will // block until all goroutines have terminated. Shutdown() // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) // InformerFor returns the SharedIndexInformer for obj using an internal // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer Jetstream() jetstream.Interface } func (f *sharedInformerFactory) Jetstream() jetstream.Interface { return jetstream.New(f, f.namespace, f.tweakListOptions) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/generic.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( fmt "fmt" v1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other // sharedInformers based on type type GenericInformer interface { Informer() cache.SharedIndexInformer Lister() cache.GenericLister } type genericInformer struct { informer cache.SharedIndexInformer resource schema.GroupResource } // Informer returns the SharedIndexInformer. func (f *genericInformer) Informer() cache.SharedIndexInformer { return f.informer } // Lister returns the GenericLister. func (f *genericInformer) Lister() cache.GenericLister { return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) } // ForResource gives generic access to a shared informer of the matching type // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=jetstream.nats.io, Version=v1beta2 case v1beta2.SchemeGroupVersion.WithResource("accounts"): return &genericInformer{resource: resource.GroupResource(), informer: f.Jetstream().V1beta2().Accounts().Informer()}, nil case v1beta2.SchemeGroupVersion.WithResource("consumers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Jetstream().V1beta2().Consumers().Informer()}, nil case v1beta2.SchemeGroupVersion.WithResource("keyvalues"): return &genericInformer{resource: resource.GroupResource(), informer: f.Jetstream().V1beta2().KeyValues().Informer()}, nil case v1beta2.SchemeGroupVersion.WithResource("objectstores"): return &genericInformer{resource: resource.GroupResource(), informer: f.Jetstream().V1beta2().ObjectStores().Informer()}, nil case v1beta2.SchemeGroupVersion.WithResource("streams"): return &genericInformer{resource: resource.GroupResource(), informer: f.Jetstream().V1beta2().Streams().Informer()}, nil } return nil, fmt.Errorf("no informer found for %v", resource) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/internalinterfaces/factory_interfaces.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package internalinterfaces import ( time "time" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/interface.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package jetstream import ( internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" v1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2" ) // Interface provides access to each of this group's versions. type Interface interface { // V1beta2 provides access to shared informers for resources in V1beta2. V1beta2() v1beta2.Interface } type group struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // V1beta2 returns a new v1beta2.Interface. func (g *group) V1beta2() v1beta2.Interface { return v1beta2.New(g.factory, g.namespace, g.tweakListOptions) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/account.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( context "context" time "time" apisjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // AccountInformer provides access to a shared informer and lister for // Accounts. type AccountInformer interface { Informer() cache.SharedIndexInformer Lister() jetstreamv1beta2.AccountLister } type accountInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewAccountInformer constructs a new informer for Account type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewAccountInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredAccountInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredAccountInformer constructs a new informer for Account type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredAccountInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Accounts(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Accounts(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Accounts(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Accounts(namespace).Watch(ctx, options) }, }, &apisjetstreamv1beta2.Account{}, resyncPeriod, indexers, ) } func (f *accountInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredAccountInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *accountInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&apisjetstreamv1beta2.Account{}, f.defaultInformer) } func (f *accountInformer) Lister() jetstreamv1beta2.AccountLister { return jetstreamv1beta2.NewAccountLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/consumer.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( context "context" time "time" apisjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // ConsumerInformer provides access to a shared informer and lister for // Consumers. type ConsumerInformer interface { Informer() cache.SharedIndexInformer Lister() jetstreamv1beta2.ConsumerLister } type consumerInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewConsumerInformer constructs a new informer for Consumer type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewConsumerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredConsumerInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredConsumerInformer constructs a new informer for Consumer type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredConsumerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Consumers(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Consumers(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Consumers(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Consumers(namespace).Watch(ctx, options) }, }, &apisjetstreamv1beta2.Consumer{}, resyncPeriod, indexers, ) } func (f *consumerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredConsumerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *consumerInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&apisjetstreamv1beta2.Consumer{}, f.defaultInformer) } func (f *consumerInformer) Lister() jetstreamv1beta2.ConsumerLister { return jetstreamv1beta2.NewConsumerLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/interface.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // Accounts returns a AccountInformer. Accounts() AccountInformer // Consumers returns a ConsumerInformer. Consumers() ConsumerInformer // KeyValues returns a KeyValueInformer. KeyValues() KeyValueInformer // ObjectStores returns a ObjectStoreInformer. ObjectStores() ObjectStoreInformer // Streams returns a StreamInformer. Streams() StreamInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // Accounts returns a AccountInformer. func (v *version) Accounts() AccountInformer { return &accountInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // Consumers returns a ConsumerInformer. func (v *version) Consumers() ConsumerInformer { return &consumerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // KeyValues returns a KeyValueInformer. func (v *version) KeyValues() KeyValueInformer { return &keyValueInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // ObjectStores returns a ObjectStoreInformer. func (v *version) ObjectStores() ObjectStoreInformer { return &objectStoreInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // Streams returns a StreamInformer. func (v *version) Streams() StreamInformer { return &streamInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/keyvalue.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( context "context" time "time" apisjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // KeyValueInformer provides access to a shared informer and lister for // KeyValues. type KeyValueInformer interface { Informer() cache.SharedIndexInformer Lister() jetstreamv1beta2.KeyValueLister } type keyValueInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewKeyValueInformer constructs a new informer for KeyValue type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewKeyValueInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredKeyValueInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredKeyValueInformer constructs a new informer for KeyValue type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredKeyValueInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().KeyValues(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().KeyValues(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().KeyValues(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().KeyValues(namespace).Watch(ctx, options) }, }, &apisjetstreamv1beta2.KeyValue{}, resyncPeriod, indexers, ) } func (f *keyValueInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredKeyValueInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *keyValueInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&apisjetstreamv1beta2.KeyValue{}, f.defaultInformer) } func (f *keyValueInformer) Lister() jetstreamv1beta2.KeyValueLister { return jetstreamv1beta2.NewKeyValueLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/objectstore.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( context "context" time "time" apisjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // ObjectStoreInformer provides access to a shared informer and lister for // ObjectStores. type ObjectStoreInformer interface { Informer() cache.SharedIndexInformer Lister() jetstreamv1beta2.ObjectStoreLister } type objectStoreInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewObjectStoreInformer constructs a new informer for ObjectStore type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewObjectStoreInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredObjectStoreInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredObjectStoreInformer constructs a new informer for ObjectStore type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredObjectStoreInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().ObjectStores(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().ObjectStores(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().ObjectStores(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().ObjectStores(namespace).Watch(ctx, options) }, }, &apisjetstreamv1beta2.ObjectStore{}, resyncPeriod, indexers, ) } func (f *objectStoreInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredObjectStoreInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *objectStoreInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&apisjetstreamv1beta2.ObjectStore{}, f.defaultInformer) } func (f *objectStoreInformer) Lister() jetstreamv1beta2.ObjectStoreLister { return jetstreamv1beta2.NewObjectStoreLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/jetstream/generated/informers/externalversions/jetstream/v1beta2/stream.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by informer-gen. DO NOT EDIT. package v1beta2 import ( context "context" time "time" apisjetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" versioned "github.com/nats-io/nack/pkg/jetstream/generated/clientset/versioned" internalinterfaces "github.com/nats-io/nack/pkg/jetstream/generated/informers/externalversions/internalinterfaces" jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/generated/listers/jetstream/v1beta2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // StreamInformer provides access to a shared informer and lister for // Streams. type StreamInformer interface { Informer() cache.SharedIndexInformer Lister() jetstreamv1beta2.StreamLister } type streamInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewStreamInformer constructs a new informer for Stream type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewStreamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredStreamInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredStreamInformer constructs a new informer for Stream type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredStreamInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Streams(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Streams(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Streams(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.JetstreamV1beta2().Streams(namespace).Watch(ctx, options) }, }, &apisjetstreamv1beta2.Stream{}, resyncPeriod, indexers, ) } func (f *streamInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredStreamInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *streamInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&apisjetstreamv1beta2.Stream{}, f.defaultInformer) } func (f *streamInformer) Lister() jetstreamv1beta2.StreamLister { return jetstreamv1beta2.NewStreamLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/account.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" ) // AccountLister helps list Accounts. // All objects returned here must be treated as read-only. type AccountLister interface { // List lists all Accounts in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Account, err error) // Accounts returns an object that can list and get Accounts. Accounts(namespace string) AccountNamespaceLister AccountListerExpansion } // accountLister implements the AccountLister interface. type accountLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Account] } // NewAccountLister returns a new AccountLister. func NewAccountLister(indexer cache.Indexer) AccountLister { return &accountLister{listers.New[*jetstreamv1beta2.Account](indexer, jetstreamv1beta2.Resource("account"))} } // Accounts returns an object that can list and get Accounts. func (s *accountLister) Accounts(namespace string) AccountNamespaceLister { return accountNamespaceLister{listers.NewNamespaced[*jetstreamv1beta2.Account](s.ResourceIndexer, namespace)} } // AccountNamespaceLister helps list and get Accounts. // All objects returned here must be treated as read-only. type AccountNamespaceLister interface { // List lists all Accounts in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Account, err error) // Get retrieves the Account from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*jetstreamv1beta2.Account, error) AccountNamespaceListerExpansion } // accountNamespaceLister implements the AccountNamespaceLister // interface. type accountNamespaceLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Account] } ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/consumer.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" ) // ConsumerLister helps list Consumers. // All objects returned here must be treated as read-only. type ConsumerLister interface { // List lists all Consumers in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Consumer, err error) // Consumers returns an object that can list and get Consumers. Consumers(namespace string) ConsumerNamespaceLister ConsumerListerExpansion } // consumerLister implements the ConsumerLister interface. type consumerLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Consumer] } // NewConsumerLister returns a new ConsumerLister. func NewConsumerLister(indexer cache.Indexer) ConsumerLister { return &consumerLister{listers.New[*jetstreamv1beta2.Consumer](indexer, jetstreamv1beta2.Resource("consumer"))} } // Consumers returns an object that can list and get Consumers. func (s *consumerLister) Consumers(namespace string) ConsumerNamespaceLister { return consumerNamespaceLister{listers.NewNamespaced[*jetstreamv1beta2.Consumer](s.ResourceIndexer, namespace)} } // ConsumerNamespaceLister helps list and get Consumers. // All objects returned here must be treated as read-only. type ConsumerNamespaceLister interface { // List lists all Consumers in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Consumer, err error) // Get retrieves the Consumer from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*jetstreamv1beta2.Consumer, error) ConsumerNamespaceListerExpansion } // consumerNamespaceLister implements the ConsumerNamespaceLister // interface. type consumerNamespaceLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Consumer] } ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/expansion_generated.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 // AccountListerExpansion allows custom methods to be added to // AccountLister. type AccountListerExpansion interface{} // AccountNamespaceListerExpansion allows custom methods to be added to // AccountNamespaceLister. type AccountNamespaceListerExpansion interface{} // ConsumerListerExpansion allows custom methods to be added to // ConsumerLister. type ConsumerListerExpansion interface{} // ConsumerNamespaceListerExpansion allows custom methods to be added to // ConsumerNamespaceLister. type ConsumerNamespaceListerExpansion interface{} // KeyValueListerExpansion allows custom methods to be added to // KeyValueLister. type KeyValueListerExpansion interface{} // KeyValueNamespaceListerExpansion allows custom methods to be added to // KeyValueNamespaceLister. type KeyValueNamespaceListerExpansion interface{} // ObjectStoreListerExpansion allows custom methods to be added to // ObjectStoreLister. type ObjectStoreListerExpansion interface{} // ObjectStoreNamespaceListerExpansion allows custom methods to be added to // ObjectStoreNamespaceLister. type ObjectStoreNamespaceListerExpansion interface{} // StreamListerExpansion allows custom methods to be added to // StreamLister. type StreamListerExpansion interface{} // StreamNamespaceListerExpansion allows custom methods to be added to // StreamNamespaceLister. type StreamNamespaceListerExpansion interface{} ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/keyvalue.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" ) // KeyValueLister helps list KeyValues. // All objects returned here must be treated as read-only. type KeyValueLister interface { // List lists all KeyValues in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.KeyValue, err error) // KeyValues returns an object that can list and get KeyValues. KeyValues(namespace string) KeyValueNamespaceLister KeyValueListerExpansion } // keyValueLister implements the KeyValueLister interface. type keyValueLister struct { listers.ResourceIndexer[*jetstreamv1beta2.KeyValue] } // NewKeyValueLister returns a new KeyValueLister. func NewKeyValueLister(indexer cache.Indexer) KeyValueLister { return &keyValueLister{listers.New[*jetstreamv1beta2.KeyValue](indexer, jetstreamv1beta2.Resource("keyvalue"))} } // KeyValues returns an object that can list and get KeyValues. func (s *keyValueLister) KeyValues(namespace string) KeyValueNamespaceLister { return keyValueNamespaceLister{listers.NewNamespaced[*jetstreamv1beta2.KeyValue](s.ResourceIndexer, namespace)} } // KeyValueNamespaceLister helps list and get KeyValues. // All objects returned here must be treated as read-only. type KeyValueNamespaceLister interface { // List lists all KeyValues in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.KeyValue, err error) // Get retrieves the KeyValue from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*jetstreamv1beta2.KeyValue, error) KeyValueNamespaceListerExpansion } // keyValueNamespaceLister implements the KeyValueNamespaceLister // interface. type keyValueNamespaceLister struct { listers.ResourceIndexer[*jetstreamv1beta2.KeyValue] } ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/objectstore.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" ) // ObjectStoreLister helps list ObjectStores. // All objects returned here must be treated as read-only. type ObjectStoreLister interface { // List lists all ObjectStores in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.ObjectStore, err error) // ObjectStores returns an object that can list and get ObjectStores. ObjectStores(namespace string) ObjectStoreNamespaceLister ObjectStoreListerExpansion } // objectStoreLister implements the ObjectStoreLister interface. type objectStoreLister struct { listers.ResourceIndexer[*jetstreamv1beta2.ObjectStore] } // NewObjectStoreLister returns a new ObjectStoreLister. func NewObjectStoreLister(indexer cache.Indexer) ObjectStoreLister { return &objectStoreLister{listers.New[*jetstreamv1beta2.ObjectStore](indexer, jetstreamv1beta2.Resource("objectstore"))} } // ObjectStores returns an object that can list and get ObjectStores. func (s *objectStoreLister) ObjectStores(namespace string) ObjectStoreNamespaceLister { return objectStoreNamespaceLister{listers.NewNamespaced[*jetstreamv1beta2.ObjectStore](s.ResourceIndexer, namespace)} } // ObjectStoreNamespaceLister helps list and get ObjectStores. // All objects returned here must be treated as read-only. type ObjectStoreNamespaceLister interface { // List lists all ObjectStores in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.ObjectStore, err error) // Get retrieves the ObjectStore from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*jetstreamv1beta2.ObjectStore, error) ObjectStoreNamespaceListerExpansion } // objectStoreNamespaceLister implements the ObjectStoreNamespaceLister // interface. type objectStoreNamespaceLister struct { listers.ResourceIndexer[*jetstreamv1beta2.ObjectStore] } ================================================ FILE: pkg/jetstream/generated/listers/jetstream/v1beta2/stream.go ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by lister-gen. DO NOT EDIT. package v1beta2 import ( jetstreamv1beta2 "github.com/nats-io/nack/pkg/jetstream/apis/jetstream/v1beta2" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" ) // StreamLister helps list Streams. // All objects returned here must be treated as read-only. type StreamLister interface { // List lists all Streams in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Stream, err error) // Streams returns an object that can list and get Streams. Streams(namespace string) StreamNamespaceLister StreamListerExpansion } // streamLister implements the StreamLister interface. type streamLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Stream] } // NewStreamLister returns a new StreamLister. func NewStreamLister(indexer cache.Indexer) StreamLister { return &streamLister{listers.New[*jetstreamv1beta2.Stream](indexer, jetstreamv1beta2.Resource("stream"))} } // Streams returns an object that can list and get Streams. func (s *streamLister) Streams(namespace string) StreamNamespaceLister { return streamNamespaceLister{listers.NewNamespaced[*jetstreamv1beta2.Stream](s.ResourceIndexer, namespace)} } // StreamNamespaceLister helps list and get Streams. // All objects returned here must be treated as read-only. type StreamNamespaceLister interface { // List lists all Streams in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*jetstreamv1beta2.Stream, err error) // Get retrieves the Stream from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*jetstreamv1beta2.Stream, error) StreamNamespaceListerExpansion } // streamNamespaceLister implements the StreamNamespaceLister // interface. type streamNamespaceLister struct { listers.ResourceIndexer[*jetstreamv1beta2.Stream] } ================================================ FILE: pkg/k8scodegen/file-header.txt ================================================ // Copyright 2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. ================================================ FILE: pkg/k8scodegen/k8scodegen.go ================================================ package k8scodegen import _ "k8s.io/code-generator" ================================================ FILE: pkg/natsreloader/natsreloader.go ================================================ // Copyright 2020-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package natsreloader import ( "bytes" "context" "crypto/sha256" "fmt" "io" "log" "math/rand" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "syscall" "time" "github.com/fsnotify/fsnotify" ) const errorFmt = "Error: %s\n" func isInotifyExhausted(err error) bool { if err == nil { return false } errStr := strings.ToLower(err.Error()) return strings.Contains(errStr, "no space left on device") || strings.Contains(errStr, "too many open files") || strings.Contains(errStr, "inotify") || strings.Contains(errStr, "watch limit") } func createInotifyExhaustedError(originalErr error, watchedFileCount int) error { return fmt.Errorf(`inotify file watching system is exhausted (original error: %v) This typically occurs on high-density Kubernetes nodes where many pods are using file watchers. DIAGNOSIS: - Trying to watch %d files - System may have exhausted inotify watches or instances - Check current limits: cat /proc/sys/fs/inotify/max_user_watches - Check current usage: find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l SOLUTIONS: 1. Increase inotify limits (cluster admin): echo 'fs.inotify.max_user_watches=1048576' >> /etc/sysctl.conf sysctl -p 2. Use polling fallback mode: Add --force-poll flag to use polling instead of inotify 3. Reduce file watching: Minimize the number of configuration files being watched For more information, see: https://github.com/nats-io/nack/issues/264`, originalErr, watchedFileCount) } // Config represents the configuration of the reloader. type Config struct { PidFile string WatchedFiles []string MaxRetries int RetryWaitSecs int Signal os.Signal ForcePoll bool PollInterval time.Duration MaxWatcherRetries int } // Reloader monitors the state from a single server config file // and sends signal on updates. type Reloader struct { *Config // proc represents the NATS Server process which will // be signaled. proc *os.Process // pid is the last known PID from the NATS Server. pid int // quit shutsdown the reloader. quit func() } func (r *Reloader) waitForProcess() error { var proc *os.Process var pid int attempts := 0 startTime := time.Now() for { pidfile, err := os.ReadFile(r.PidFile) if err != nil { goto WaitAndRetry } pid, err = strconv.Atoi(string(pidfile)) if err != nil { goto WaitAndRetry } // This always succeeds regardless of the process existing or not. proc, err = os.FindProcess(pid) if err != nil { goto WaitAndRetry } // Check if the process is still alive. err = proc.Signal(syscall.Signal(0)) if err != nil { goto WaitAndRetry } break WaitAndRetry: log.Printf("Error while monitoring pid %v: %v", pid, err) attempts++ if attempts > r.MaxRetries { return fmt.Errorf("too many errors attempting to find server process") } time.Sleep(time.Duration(r.RetryWaitSecs) * time.Second) } if attempts > 0 { log.Printf("Found pid from pidfile %q after %v failed attempts (took %.3fs)", r.PidFile, attempts, time.Since(startTime).Seconds()) } r.proc = proc return nil } func removeDuplicateStrings(s []string) []string { if len(s) < 1 { return s } sort.Strings(s) prev := 1 for curr := 1; curr < len(s); curr++ { if s[curr-1] != s[curr] { s[prev] = s[curr] prev++ } } return s[:prev] } func getFileDigest(filePath string) ([]byte, error) { h := sha256.New() f, err := os.Open(filePath) if err != nil { return nil, err } defer f.Close() if _, err := io.Copy(h, f); err != nil { return nil, err } return h.Sum(nil), nil } func handleEvent(event fsnotify.Event, lastConfigAppliedCache map[string][]byte, updatedFiles, deletedFiles []string) ([]string, []string) { if event.Has(fsnotify.Remove) { // We don't get a Remove event for the directory itself, so // we need to detect that separately. return updatedFiles, append(deletedFiles, event.Name) } _, err := os.Stat(event.Name) if err != nil { // Beware that this means that we won't reconfigure if a file // is permanently removed. We want to support transient // disappearance, waiting for the new content, and have not set // up any sort of longer-term timers to detect permanent // deletion. // If you really need this, then switch a file to be empty // before removing if afterwards. return updatedFiles, deletedFiles } if len(updatedFiles) > 0 { return updatedFiles, deletedFiles } digest, err := getFileDigest(event.Name) if err != nil { log.Printf(errorFmt, err) return updatedFiles, deletedFiles } lastConfigHash, ok := lastConfigAppliedCache[event.Name] if ok && bytes.Equal(lastConfigHash, digest) { return updatedFiles, deletedFiles } log.Printf("Changed config; file=%q existing=%v total-files=%d", event.Name, ok, len(lastConfigAppliedCache)) lastConfigAppliedCache[event.Name] = digest return append(updatedFiles, event.Name), deletedFiles } // handleEvents handles all events in the queue. It returns the updated and deleted files and can contain duplicates. func handleEvents(configWatcher *fsnotify.Watcher, event fsnotify.Event, lastConfigAppliedCache map[string][]byte) ([]string, []string) { updatedFiles, deletedFiles := handleEvent(event, lastConfigAppliedCache, make([]string, 0, 16), make([]string, 0, 16)) for { select { case event := <-configWatcher.Events: updatedFiles, deletedFiles = handleEvent(event, lastConfigAppliedCache, updatedFiles, deletedFiles) default: return updatedFiles, deletedFiles } } } func handleDeletedFiles(deletedFiles []string, configWatcher *fsnotify.Watcher, lastConfigAppliedCache map[string][]byte) ([]string, []string) { if len(deletedFiles) > 0 { log.Printf("Tracking files %v", deletedFiles) } newDeletedFiles := make([]string, 0, len(deletedFiles)) updated := make([]string, 0, len(deletedFiles)) for _, f := range deletedFiles { if err := configWatcher.Add(f); err != nil { newDeletedFiles = append(newDeletedFiles, f) } else { updated, _ = handleEvent(fsnotify.Event{Name: f, Op: fsnotify.Create}, lastConfigAppliedCache, updated, nil) } } return removeDuplicateStrings(updated), newDeletedFiles } func (r *Reloader) createWatcherWithRetry() (*fsnotify.Watcher, error) { maxRetries := r.MaxWatcherRetries if maxRetries == 0 { maxRetries = 3 } var lastErr error for attempt := 0; attempt <= maxRetries; attempt++ { if attempt > 0 { waitTime := time.Duration(r.RetryWaitSecs) * time.Second if waitTime == 0 { waitTime = 4 * time.Second } waitTime = retryJitter(waitTime) log.Printf("Retrying watcher creation in %.2fs (attempt %d/%d)", waitTime.Seconds(), attempt+1, maxRetries+1) time.Sleep(waitTime) } watcher, err := fsnotify.NewWatcher() if err == nil { if attempt > 0 { log.Printf("Successfully created watcher after %d retries", attempt) } return watcher, nil } lastErr = err log.Printf("Failed to create watcher (attempt %d/%d): %v", attempt+1, maxRetries+1, err) if isInotifyExhausted(err) { return nil, createInotifyExhaustedError(err, len(r.WatchedFiles)) } } // All retries failed return nil, fmt.Errorf("failed to create watcher after %d attempts, last error: %v", maxRetries+1, lastErr) } func (r *Reloader) init() (*fsnotify.Watcher, map[string][]byte, error) { err := r.waitForProcess() if err != nil { return nil, nil, err } if r.ForcePoll { log.Printf("Using polling mode (forced)") return nil, nil, nil } configWatcher, err := r.createWatcherWithRetry() if err != nil { return nil, nil, err } watchedFiles := make([]string, 0) for _, c := range r.WatchedFiles { if !strings.HasSuffix(c, ".conf") { continue } childFiles, err := getServerFiles(c) if err != nil { return nil, nil, err } watchedFiles = append(watchedFiles, childFiles...) } r.WatchedFiles = append(r.WatchedFiles, watchedFiles...) // Follow configuration updates in the directory where // the config file is located and trigger reload when // it is either recreated or written into. for i := range r.WatchedFiles { // Ensure our paths are canonical r.WatchedFiles[i], _ = filepath.Abs(r.WatchedFiles[i]) } r.WatchedFiles = removeDuplicateStrings(r.WatchedFiles) // Follow configuration file updates and trigger reload when // it is either recreated or written into. for i := range r.WatchedFiles { // Watch files individually for https://github.com/kubernetes/kubernetes/issues/112677 if err := configWatcher.Add(r.WatchedFiles[i]); err != nil { _ = configWatcher.Close() // Check if this is an inotify exhaustion error if isInotifyExhausted(err) { return nil, nil, createInotifyExhaustedError(err, len(r.WatchedFiles)) } return nil, nil, err } log.Printf("Watching file: %v", r.WatchedFiles[i]) } // lastConfigAppliedCache is the last config update // applied by us. lastConfigAppliedCache := make(map[string][]byte) // Preload config hashes, so we know their digests // up front and avoid potentially reloading when unnecessary. for _, configFile := range r.WatchedFiles { digest, err := getFileDigest(configFile) if err != nil { _ = configWatcher.Close() return nil, nil, err } lastConfigAppliedCache[configFile] = digest } log.Printf("Live, ready to kick pid %v on config changes (files=%d)", r.proc.Pid, len(lastConfigAppliedCache)) if len(lastConfigAppliedCache) == 0 { log.Printf("Error: no watched config files cached; input spec was: %#v", r.WatchedFiles) } return configWatcher, lastConfigAppliedCache, nil } func (r *Reloader) reload(updatedFiles []string) error { attempts := 0 for { err := r.waitForProcess() if err != nil { goto Retry } log.Printf("Sending pid %v '%s' signal to reload changes from: %s", r.proc.Pid, r.Signal.String(), updatedFiles) err = r.proc.Signal(r.Signal) if err == nil { return nil } Retry: if err != nil { log.Printf("Error during reload: %s", err) } if attempts > r.MaxRetries { return fmt.Errorf("too many errors (%v) attempting to signal server to reload: %w", attempts, err) } delay := retryJitter(time.Duration(r.RetryWaitSecs) * time.Second) log.Printf("Wait and retrying in %.3fs ...", delay.Seconds()) time.Sleep(delay) attempts++ } } // Run starts the main loop. func (r *Reloader) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) r.quit = func() { cancel() } configWatcher, lastConfigAppliedCache, err := r.init() if err != nil { if isInotifyExhausted(err) { log.Printf("inotify unavailable, falling back to polling mode") r.ForcePoll = true return r.runPollingMode(ctx) } return err } if r.ForcePoll || configWatcher == nil { return r.runPollingMode(ctx) } defer configWatcher.Close() // We use a ticker to re-add deleted files to the watcher t := time.NewTicker(time.Second) t.Stop() defer t.Stop() var tickerRunning bool var deletedFiles []string var updatedFiles []string for { select { case <-ctx.Done(): return nil case <-t.C: updatedFiles, deletedFiles = handleDeletedFiles(deletedFiles, configWatcher, lastConfigAppliedCache) if len(deletedFiles) == 0 { log.Printf("All monitored files detected.") t.Stop() tickerRunning = false } if len(updatedFiles) > 0 { // Send signal to reload the config. log.Printf("Updated files: %v", updatedFiles) break } continue case event := <-configWatcher.Events: updated, deleted := handleEvents(configWatcher, event, lastConfigAppliedCache) updatedFiles = removeDuplicateStrings(updated) deletedFiles = removeDuplicateStrings(append(deletedFiles, deleted...)) if !tickerRunning { // Start the ticker to re-add deleted files. log.Printf("Starting ticker to re-add all tracked files.") t.Reset(time.Second) tickerRunning = true } if len(updatedFiles) > 0 { // Send signal to reload the config log.Printf("Updated files: %v", updatedFiles) break } continue case err := <-configWatcher.Errors: log.Printf(errorFmt, err) continue } // Configuration was updated, try to do reload for a few times // otherwise give up and wait for next event. err := r.reload(updatedFiles) if err != nil { return err } updatedFiles = nil } } // Stop shutsdown the process. func (r *Reloader) Stop() error { log.Println("Shutting down...") r.quit() return nil } // NewReloader returns a configured NATS server reloader. func NewReloader(config *Config) (*Reloader, error) { return &Reloader{ Config: config, }, nil } // retryJitter helps avoid trying things at synchronized times, thus improving // resiliency in aggregate. func retryJitter(base time.Duration) time.Duration { b := float64(base) // 10% +/- offset := rand.Float64()*0.2 - 0.1 return time.Duration(b + offset) } func (r *Reloader) pollForChanges(lastConfigAppliedCache map[string][]byte) ([]string, error) { var updatedFiles []string for _, configFile := range r.WatchedFiles { // Check if file still exists if _, err := os.Stat(configFile); os.IsNotExist(err) { // File was deleted, remove from cache and treat as an update // to trigger a reload with the remaining configuration files if _, exists := lastConfigAppliedCache[configFile]; exists { log.Printf("Detected deleted config file (polling); file=%q", configFile) delete(lastConfigAppliedCache, configFile) updatedFiles = append(updatedFiles, configFile) } continue } digest, err := getFileDigest(configFile) if err != nil { log.Printf("Error reading file %s: %v", configFile, err) continue } // Check if file has changed lastDigest, exists := lastConfigAppliedCache[configFile] if !exists || !bytes.Equal(lastDigest, digest) { log.Printf("Changed config (polling); file=%q existing=%v", configFile, exists) lastConfigAppliedCache[configFile] = digest updatedFiles = append(updatedFiles, configFile) } } return updatedFiles, nil } func (r *Reloader) runPollingMode(ctx context.Context) error { lastConfigAppliedCache := make(map[string][]byte) watchedFiles := make([]string, 0) for _, c := range r.WatchedFiles { // Only try to parse config files if !strings.HasSuffix(c, ".conf") { continue } childFiles, err := getServerFiles(c) if err != nil { return err } watchedFiles = append(watchedFiles, childFiles...) } r.WatchedFiles = append(r.WatchedFiles, watchedFiles...) // Ensure our paths are canonical for i := range r.WatchedFiles { r.WatchedFiles[i], _ = filepath.Abs(r.WatchedFiles[i]) } r.WatchedFiles = removeDuplicateStrings(r.WatchedFiles) for _, configFile := range r.WatchedFiles { digest, err := getFileDigest(configFile) if err != nil { return err } lastConfigAppliedCache[configFile] = digest log.Printf("Polling file: %v", configFile) } log.Printf("Live, ready to kick pid %v on config changes (files=%d, polling mode)", r.proc.Pid, len(lastConfigAppliedCache)) pollInterval := r.PollInterval if pollInterval == 0 { pollInterval = 5 * time.Second } ticker := time.NewTicker(pollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return nil case <-ticker.C: updatedFiles, err := r.pollForChanges(lastConfigAppliedCache) if err != nil { log.Printf("Error polling for changes: %v", err) continue } if len(updatedFiles) > 0 { log.Printf("Updated files (polling): %v", updatedFiles) err := r.reload(updatedFiles) if err != nil { return err } } } } } func getServerFiles(configFile string) ([]string, error) { filePaths, err := getIncludePaths(configFile, make(map[string]interface{})) if err != nil { return nil, err } certPaths, err := getCertPaths(filePaths) if err != nil { return nil, err } filePaths = append(filePaths, certPaths...) sort.Strings(filePaths) return filePaths, nil } func getIncludePaths(configFile string, checked map[string]interface{}) ([]string, error) { if _, ok := checked[configFile]; ok { return []string{}, nil } configFile, err := filepath.Abs(configFile) if err != nil { return nil, err } filePaths := []string{configFile} checked[configFile] = nil parentDirectory := filepath.Dir(configFile) includeRegex := regexp.MustCompile(`(?m)^\s*include\s+(['"]?[^'";\n]*)`) content, err := os.ReadFile(configFile) if err != nil { return nil, err } includeMatches := includeRegex.FindAllStringSubmatch(string(content), -1) for _, match := range includeMatches { matchStr := match[1] if strings.HasPrefix(matchStr, "$") { continue } matchStr = strings.TrimPrefix(matchStr, "'") matchStr = strings.TrimPrefix(matchStr, "\"") // Include filepaths in NATS config are always relative fullyQualifiedPath := filepath.Join(parentDirectory, matchStr) fullyQualifiedPath = filepath.Clean(fullyQualifiedPath) if _, err := os.Stat(fullyQualifiedPath); os.IsNotExist(err) { return nil, fmt.Errorf("%s does not exist", fullyQualifiedPath) } // Recursive call to make sure we catch any nested includes // Using map[string]interface{} as a set to avoid loops includePaths, err := getIncludePaths(fullyQualifiedPath, checked) if err != nil { return nil, err } filePaths = append(filePaths, includePaths...) } return filePaths, nil } func getCertPaths(configPaths []string) ([]string, error) { certPaths := []string{} certRegex := regexp.MustCompile(`(?m)^\s*(cert_file|key_file|ca_file)\s*:\s*(['"]?[^'";\n]*)"?`) for _, configPath := range configPaths { content, err := os.ReadFile(configPath) if err != nil { return nil, err } certMatches := certRegex.FindAllStringSubmatch(string(content), -1) for _, match := range certMatches { matchStr := match[2] if strings.HasPrefix(matchStr, "$") { continue } matchStr = strings.TrimPrefix(matchStr, "'") matchStr = strings.TrimPrefix(matchStr, "\"") fullyQualifiedPath, err := filepath.Abs(matchStr) if err != nil { return nil, err } if _, err := os.Stat(fullyQualifiedPath); os.IsNotExist(err) { return nil, fmt.Errorf("%s does not exist", fullyQualifiedPath) } certPaths = append(certPaths, fullyQualifiedPath) } } return certPaths, nil } ================================================ FILE: pkg/natsreloader/natsreloader_test.go ================================================ // Copyright 2020-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package natsreloader import ( "context" "errors" "fmt" "io/fs" "os" "os/signal" "path/filepath" "sort" "strings" "sync" "syscall" "testing" "time" ) const ( testConfig_0 = ` jetstream { store_dir: data/jetstream max_mem: 10G max_file: 10G } operator: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3Sk1aNEQ0RE1WU1hGWDRYWExCTVVITjY1MjdaQlhaV0dZUUtBQVk0TVRQQTZOSEdHS1NBIiwiaWF0IjoxNjgyNTAzMzg4LCJpc3MiOiJPQ1hXU0tWNU5UTUxCUjI0NlZaQ0laSzVZRlBVTVNPVjZVWk5JNDRPTFVVUUlCNkE0VU1RWE1USiIsIm5hbWUiOiJuYXRzLXRlc3QtMDEiLCJzdWIiOiJPQ1hXU0tWNU5UTUxCUjI0NlZaQ0laSzVZRlBVTVNPVjZVWk5JNDRPTFVVUUlCNkE0VU1RWE1USiIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9DS1oyNkpJUDdCN1BHQk43QTdEVEVHVk9NUlNHNE5XVFZURjdPQ0pOSVdRS0xZT0YzWDJBTlFKIl0sImFjY291bnRfc2VydmVyX3VybCI6Im5hdHM6Ly9sb2NhbGhvc3Q6NDIyMiIsIm9wZXJhdG9yX3NlcnZpY2VfdXJscyI6WyJuYXRzOi8vbG9jYWxob3N0OjQyMjIiXSwic3lzdGVtX2FjY291bnQiOiJBQk5ITEY2NVlEWkxGWUlIUVVVU0pXWlZSUVc0UE8zVFFRT0VTNlA3WTRUQ1BQWVVTNkhIVzJFUyIsInR5cGUiOiJvcGVyYXRvciIsInZlcnNpb24iOjJ9fQ.LjVkEnA3Fg3F20cPZm5FShZQKWPiU4pLdhh2s0cj_zhxA88wXgNfUo_SPs59JE97qvpR7AOWksP5dzxMZJ2iBQ # System Account named SYS system_account: ABNHLF65YDZLFYIHQUUSJWZVRQW4PO3TQQOES6P7Y4TCPPYUS6HHW2ES resolver { type: full dir: './' } jetstream { store_dir: data/jetstream max_mem: 10G max_file: 10G } include './testConfig_1.conf'` testConfig_1 = `include ./testConfig_2.conf` testConfig_2 = ` tls: { cert_file: "./test.pem" key_file: "./testkey.pem" } ` includeTest_0 = ` include nats_0.conf include nats_1.conf; // semicolon terminated include "nats_2.conf" // double-quoted include "nats_3.conf"; // double-quoted and semicolon terminated include 'nats_4.conf' // single-quoted include 'nats_5.conf'; // single-quoted and semicolon terminated include $NATS; // ignore variable include "$NATS_6.conf" // filename starting with $ include includeTest_1.conf ` includeTest_1 = ` tls: { cert_file: ./nats_0.pem key_file: 'nats_0.key' } tls: { cert_file: "./nats_1.pem" key_file: $test } tls: { cert_file: "$nats_2.pem"; key_file: 'nats_1.key'; } ` ) var ( configContents = `port = 2222` newConfigContents = `port = 2222 someOtherThing = "bar" ` ) func TestReloader(t *testing.T) { // Setup a pidfile that points to us pid := os.Getpid() pidfile, err := os.CreateTemp(os.TempDir(), "nats-pid-") if err != nil { t.Fatal(err) } p := fmt.Sprintf("%d", pid) if _, err := pidfile.WriteString(p); err != nil { t.Fatal(err) } defer os.Remove(pidfile.Name()) // Create tempfile with contents, then update it nconfig := &Config{ PidFile: pidfile.Name(), WatchedFiles: []string{}, Signal: syscall.SIGHUP, } var configFiles []*os.File for i := 0; i < 2; i++ { configFile, err := os.CreateTemp(os.TempDir(), "nats-conf-") if err != nil { t.Fatal(err) } defer os.Remove(configFile.Name()) if _, err := configFile.WriteString(configContents); err != nil { t.Fatal(err) } configFiles = append(configFiles, configFile) nconfig.WatchedFiles = append(nconfig.WatchedFiles, configFile.Name()) } r, err := NewReloader(nconfig) if err != nil { t.Fatal(err) } signals := 0 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var sigsMu sync.Mutex // Signal handling. go func() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGHUP) // Success when receiving the first signal for range c { sigsMu.Lock() signals++ sigsMu.Unlock() } }() go func() { // This is terrible, but we need this thread to wait until r.Run(ctx) has finished starting up // before we start mucking with the file. // There isn't any other good way to synchronize on this happening. time.Sleep(200 * time.Millisecond) for _, configfile := range configFiles { for i := 0; i < 5; i++ { // Append some more stuff to the config if _, err := configfile.WriteAt([]byte(newConfigContents), 0); err != nil { return } time.Sleep(10 * time.Millisecond) } } // Create some random file in the same directory, shouldn't trigger an // additional server signal. configFile, err := os.CreateTemp(os.TempDir(), "foo") if err != nil { t.Log(err) return } defer os.Remove(configFile.Name()) time.Sleep(100 * time.Millisecond) cancel() }() err = r.Run(ctx) if err != nil && !errors.Is(err, context.Canceled) { t.Fatal(err) } // We should have gotten only one signal for each configuration file sigsMu.Lock() got := signals sigsMu.Unlock() expected := len(configFiles) if got != expected { t.Fatalf("Wrong number of signals received. Expected: %v, got: %v", expected, got) } } func TestInclude(t *testing.T) { directory, err := os.Getwd() if err != nil { t.Fatal(err) } dummyFiles := []string{ "nats_0.conf", "nats_1.conf", "nats_2.conf", "nats_3.conf", "nats_4.conf", "nats_5.conf", "$NATS_6.conf", "nats_0.pem", "nats_1.pem", "$nats_2.pem", "nats_0.key", "nats_1.key", } for _, f := range dummyFiles { p := filepath.Join(directory, f) err = writeFile("", p) defer os.Remove(p) if err != nil { t.Fatal(err) } } includeTestConf_0 := filepath.Join(directory, "includeTest_0.conf") err = writeFile(includeTest_0, includeTestConf_0) defer os.Remove(includeTestConf_0) if err != nil { t.Fatal(err) } includeTestConf_1 := filepath.Join(directory, "includeTest_1.conf") err = writeFile(includeTest_1, includeTestConf_1) defer os.Remove(includeTestConf_1) if err != nil { t.Fatal(err) } includes, err := getServerFiles("includeTest_0.conf") if err != nil { t.Fatal(err) } includePaths := make([]string, 0) for _, p := range includes { includePaths = append(includePaths, filepath.Base(p)) } dummyFiles = append(dummyFiles, "includeTest_0.conf") dummyFiles = append(dummyFiles, "includeTest_1.conf") sort.Strings(dummyFiles) sort.Strings(includePaths) for i, p := range dummyFiles { if p != includePaths[i] { t.Fatal("Expected include paths do not match") } } } func TestFileFinder(t *testing.T) { directory, err := os.Getwd() if err != nil { t.Fatal(err) } confFile := filepath.Join(directory, "testConfig_0.conf") err = writeFile(testConfig_0, confFile) defer os.Remove(confFile) if err != nil { t.Fatal(err) } confFile = filepath.Join(directory, "testConfig_1.conf") err = writeFile(testConfig_1, confFile) defer os.Remove(confFile) if err != nil { t.Fatal(err) } confFile = filepath.Join(directory, "testConfig_2.conf") err = writeFile(testConfig_2, confFile) defer os.Remove(confFile) if err != nil { t.Fatal(err) } confFile = filepath.Join(directory, "test.pem") err = writeFile("test", confFile) defer os.Remove(confFile) if err != nil { t.Fatal(err) } confFile = filepath.Join(directory, "testkey.pem") err = writeFile("test", confFile) defer os.Remove(confFile) if err != nil { t.Fatal(err) } pid := os.Getpid() pidFile := filepath.Join(directory, "nats.pid") err = writeFile(fmt.Sprintf("%d", pid), pidFile) if err != nil { t.Fatal(err) } defer os.Remove(pidFile) nconfig := &Config{ PidFile: pidFile, WatchedFiles: []string{filepath.Join(directory, "testConfig_0.conf")}, Signal: syscall.SIGHUP, } r, err := NewReloader(nconfig) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) go func() { err = r.Run(ctx) if err != nil { t.Error(err) } }() time.Sleep(time.Second) cancel() expectedWatchedFiles := []string{ "/testConfig_0.conf", "/testConfig_1.conf", "/testConfig_2.conf", "/test.pem", "/testkey.pem", } watchedFiles := r.WatchedFiles sort.Strings(expectedWatchedFiles) sort.Strings(watchedFiles) if len(watchedFiles) > len(expectedWatchedFiles) { t.Fatal("Unexpected number of watched files") } for i, e := range expectedWatchedFiles { f := strings.TrimPrefix(watchedFiles[i], directory) if f != e { t.Fatal("Expected watched file list does not match") } } } func writeFile(content, path string) error { parentDirectory := filepath.Dir(path) if _, err := os.Stat(parentDirectory); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(parentDirectory, 0o755) if err != nil { return err } } file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) if err != nil { return err } defer file.Close() _, err = file.WriteString(content) if err != nil { return err } return nil } func TestReloaderInotifyExhaustion(t *testing.T) { pid := os.Getpid() pidfile, err := os.CreateTemp(os.TempDir(), "nats-pid-") if err != nil { t.Fatal(err) } defer os.Remove(pidfile.Name()) p := fmt.Sprintf("%d", pid) if _, err := pidfile.WriteString(p); err != nil { t.Fatal(err) } // Create a config file configFile, err := os.CreateTemp(os.TempDir(), "nats-conf-") if err != nil { t.Fatal(err) } defer os.Remove(configFile.Name()) if _, err := configFile.WriteString(configContents); err != nil { t.Fatal(err) } // Create configuration that will fail during init nconfig := &Config{ PidFile: pidfile.Name(), WatchedFiles: []string{configFile.Name()}, Signal: syscall.SIGHUP, } r, err := NewReloader(nconfig) if err != nil { t.Fatal(err) } // Create many temporary files to potentially exhaust resources var manyFiles []string for i := 0; i < 10; i++ { tempFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("nats-test-%d-", i)) if err != nil { t.Fatal(err) } defer os.Remove(tempFile.Name()) if _, err := tempFile.WriteString(configContents); err != nil { t.Fatal(err) } manyFiles = append(manyFiles, tempFile.Name()) } // Update config to watch many files nconfig.WatchedFiles = append(nconfig.WatchedFiles, manyFiles...) // Try to run the reloader - this should work with current implementation // but demonstrates the scenario where it could fail ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() err = r.Run(ctx) if err != nil && !errors.Is(err, context.Canceled) { // This is where we would expect to see the "no space left on device" error // in a real inotify exhaustion scenario t.Logf("Expected error in inotify exhaustion scenario: %v", err) // Check if error message is misleading (current problem) if strings.Contains(err.Error(), "no space left on device") { t.Logf("ERROR: Misleading error message detected - this is the bug we're fixing") t.Logf("Error message should explain inotify exhaustion, not disk space") } } } func TestReloaderPollingMode(t *testing.T) { // Test that polling mode works correctly // Setup a pidfile that points to us pid := os.Getpid() pidfile, err := os.CreateTemp(os.TempDir(), "nats-pid-") if err != nil { t.Fatal(err) } defer os.Remove(pidfile.Name()) p := fmt.Sprintf("%d", pid) if _, err := pidfile.WriteString(p); err != nil { t.Fatal(err) } // Create a config file configFile, err := os.CreateTemp(os.TempDir(), "nats-conf-") if err != nil { t.Fatal(err) } defer os.Remove(configFile.Name()) if _, err := configFile.WriteString(configContents); err != nil { t.Fatal(err) } // Create configuration that forces polling mode nconfig := &Config{ PidFile: pidfile.Name(), WatchedFiles: []string{configFile.Name()}, Signal: syscall.SIGHUP, ForcePoll: true, PollInterval: 100 * time.Millisecond, // Fast polling for testing } r, err := NewReloader(nconfig) if err != nil { t.Fatal(err) } signals := 0 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var sigsMu sync.Mutex // Signal handling go func() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGHUP) for range c { sigsMu.Lock() signals++ sigsMu.Unlock() } }() go func() { // Wait for polling to start time.Sleep(200 * time.Millisecond) // Modify the config file if _, err := configFile.WriteAt([]byte(newConfigContents), 0); err != nil { t.Logf("Failed to write config: %v", err) return } // Wait for polling to detect the change time.Sleep(200 * time.Millisecond) cancel() }() err = r.Run(ctx) if err != nil && !errors.Is(err, context.Canceled) { t.Fatal(err) } // We should have gotten at least one signal sigsMu.Lock() got := signals sigsMu.Unlock() if got == 0 { t.Fatal("Expected at least one signal in polling mode, got 0") } t.Logf("Successfully received %d signals in polling mode", got) } func TestReloaderPollingModeFileDeletion(t *testing.T) { // Test that file deletion is handled correctly in polling mode // Setup a pidfile that points to us pid := os.Getpid() pidfile, err := os.CreateTemp(os.TempDir(), "nats-pid-") if err != nil { t.Fatal(err) } defer os.Remove(pidfile.Name()) p := fmt.Sprintf("%d", pid) if _, err := pidfile.WriteString(p); err != nil { t.Fatal(err) } // Create two config files configFile1, err := os.CreateTemp(os.TempDir(), "nats-conf-1-") if err != nil { t.Fatal(err) } configFile1Path := configFile1.Name() defer os.Remove(configFile1Path) if _, err := configFile1.WriteString(configContents); err != nil { t.Fatal(err) } configFile2, err := os.CreateTemp(os.TempDir(), "nats-conf-2-") if err != nil { t.Fatal(err) } configFile2Path := configFile2.Name() if _, err := configFile2.WriteString(configContents); err != nil { t.Fatal(err) } configFile2.Close() // Create configuration that forces polling mode nconfig := &Config{ PidFile: pidfile.Name(), WatchedFiles: []string{configFile1Path, configFile2Path}, Signal: syscall.SIGHUP, ForcePoll: true, PollInterval: 100 * time.Millisecond, // Fast polling for testing } r, err := NewReloader(nconfig) if err != nil { t.Fatal(err) } signals := 0 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var sigsMu sync.Mutex // Signal handling go func() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGHUP) for range c { sigsMu.Lock() signals++ sigsMu.Unlock() t.Logf("Received signal #%d", signals) } }() go func() { // Wait for polling to start time.Sleep(200 * time.Millisecond) // Delete the second config file t.Logf("Deleting config file: %s", configFile2Path) if err := os.Remove(configFile2Path); err != nil { t.Logf("Failed to delete config: %v", err) return } // Wait for polling to detect the deletion time.Sleep(300 * time.Millisecond) // Modify the first config file to ensure we can still detect changes t.Logf("Modifying remaining config file: %s", configFile1Path) if err := os.WriteFile(configFile1Path, []byte(newConfigContents), 0o644); err != nil { t.Logf("Failed to write config: %v", err) return } // Wait for polling to detect the change time.Sleep(300 * time.Millisecond) cancel() }() err = r.Run(ctx) if err != nil && !errors.Is(err, context.Canceled) { t.Fatal(err) } // We should have gotten at least 2 signals: // 1. For the file deletion // 2. For the file modification sigsMu.Lock() got := signals sigsMu.Unlock() if got < 2 { t.Fatalf("Expected at least 2 signals in polling mode (deletion + modification), got %d", got) } t.Logf("Successfully received %d signals in polling mode with file deletion", got) } ================================================ FILE: tests/Dockerfile ================================================ # This Dockerfile enables straight-forward building of the jetstream-controller # This is currently not used in the CI/CD pipeline but can be used for local builds of the controller image FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder ARG TARGETOS ARG TARGETARCH WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source COPY cmd/jetstream-controller/ cmd/jetstream-controller/ COPY pkg/ pkg/ COPY internal/ internal/ COPY controllers/ controllers/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o jetstream-controller cmd/jetstream-controller/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static-debian12:nonroot WORKDIR / COPY --from=builder /workspace/jetstream-controller . USER 65532:65532 ENTRYPOINT ["/jetstream-controller"] ================================================ FILE: tests/nack-control-loop.yaml ================================================ jetstream: enabled: true image: repository: nack tag: test # Enable controller-runtime mode additionalArgs: - --control-loop nats: url: nats://nats:4222 namespaced: true ================================================ FILE: tests/nack-legacy.yaml ================================================ jetstream: enabled: true image: repository: nack tag: test nats: url: nats://nats:4222 namespaced: true ================================================ FILE: tests/nats.yaml ================================================ --- global: labels: app: main-jetstream natsBox: enabled: false config: cluster: enabled: false gateway: enabled: false jetstream: enabled: true memoryStore: enabled: true maxSize: 256Mi fileStore: enabled: true pvc: enabled: true size: 256Mi ================================================ FILE: tests/stream-creation/00-nack.yaml ================================================ apiVersion: kuttl.dev/v1beta1 kind: TestStep unitTest: false commands: - command: helm uninstall --namespace $NAMESPACE nats ignoreFailure: true - command: helm uninstall --namespace $NAMESPACE nack ignoreFailure: true - command: helm repo add nats https://nats-io.github.io/k8s/helm/charts --force-update - command: helm upgrade --install --wait --namespace $NAMESPACE nats nats/nats -f ../nats.yaml - command: helm upgrade --install --wait --namespace $NAMESPACE nack nats/nack --skip-crds -f ../nack.yaml ================================================ FILE: tests/stream-creation/01-stream.yaml ================================================ apiVersion: kuttl.dev/v1beta1 kind: TestStep apply: - rides-stream.yaml assert: - asserted-rides-stream.yaml unitTest: false ================================================ FILE: tests/stream-creation/02-natscli-stream.yaml ================================================ apiVersion: kuttl.dev/v1beta1 kind: TestStep apply: - natscli.yaml assert: - asserted-natscli.yaml unitTest: false ================================================ FILE: tests/stream-creation/asserted-natscli.yaml ================================================ apiVersion: v1 kind: Pod metadata: labels: run: natscli name: natscli status: phase: Succeeded ================================================ FILE: tests/stream-creation/asserted-rides-stream.yaml ================================================ apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: rides spec: allowDirect: false allowMsgTtl: false allowRollup: false compression: "" creds: "" denyDelete: false discard: old discardPerSubject: false firstSequence: 0 maxAge: "" maxBytes: -1 maxConsumers: -1 maxMsgSize: -1 maxMsgs: -1 maxMsgsPerSubject: 0 name: rides nkey: "" noAck: false preventDelete: false preventUpdate: false replicas: 1 retention: limits servers: [] storage: memory subjectDeleteMarkerTtl: "" subjects: - rides.> ================================================ FILE: tests/stream-creation/natscli.yaml ================================================ apiVersion: v1 kind: Pod metadata: labels: run: natscli name: natscli spec: restartPolicy: Never containers: - image: natsio/nats-box name: natscli command: - nats args: - -s - nats://nats:4222 - stream - info - rides ================================================ FILE: tests/stream-creation/rides-stream.yaml ================================================ apiVersion: jetstream.nats.io/v1beta2 kind: Stream metadata: name: rides spec: name: rides subjects: - "rides.>" storage: memory replicas: 1