diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18fc5ec1ad..25b234846a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,11 @@ jobs: integration_tests: strategy: matrix: - java: [ 17, 21 ] - kubernetes: [ 'v1.29.12','1.30.8', '1.31.4', '1.32.0' ] + java: [ 17, 21, 25 ] + # Use the latest versions supported by minikube, otherwise GitHub it will + # end up in a throttling requests from minikube and workflow will fail. + # Minikube does such requests only if a version is not officially supported. + kubernetes: [ '1.30.12', '1.31.8', '1.32.4','1.33.1' ] uses: ./.github/workflows/integration-tests.yml with: java-version: ${{ matrix.java }} @@ -23,7 +26,7 @@ jobs: httpclient: [ 'vertx', 'jdk', 'jetty' ] uses: ./.github/workflows/integration-tests.yml with: - java-version: 21 + java-version: 25 kube-version: '1.32.0' http-client: ${{ matrix.httpclient }} experimental: true @@ -33,11 +36,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 17, 21 ] + java: [ 17, 21, 25 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ matrix.java }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 4ac58ab062..7aa92a409c 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,6 +1,6 @@ # Integration and end to end tests which runs locally and deploys the Operator to a Kubernetes # (Minikube) cluster and creates custom resources to verify the operator's functionality -name: Integration & End to End tests +name: End to End tests on: pull_request: paths-ignore: @@ -27,20 +27,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.13.1 + uses: manusa/actions-setup-minikube@v2.14.0 with: - minikube version: v1.34.0 - kubernetes version: v1.32.0 + minikube version: v1.36.0 + # Use the latest versions supported by minikube, otherwise GitHub it will + # end up in a throttling requests from minikube and workflow will fail. + # Minikube does such requests only if a version is not officially supported. + kubernetes version: v1.33.1 github token: ${{ secrets.GITHUB_TOKEN }} driver: docker - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: 17 + java-version: 25 distribution: temurin cache: 'maven' diff --git a/.github/workflows/fabric8-next-version-schedule.yml b/.github/workflows/fabric8-next-version-schedule.yml deleted file mode 100644 index 64d2042135..0000000000 --- a/.github/workflows/fabric8-next-version-schedule.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Fabric8 Client Snapshot Build - -env: - MAVEN_ARGS: -V -ntp -e - -concurrency: - group: ${{ github.ref }}-${{ github.workflow }} - cancel-in-progress: true -on: - schedule: - # Run on end of the day - - cron: '0 0 * * *' - workflow_dispatch: -jobs: - check_format_and_unit_tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: 'fabric8-next-version' - - name: Set up Java and Maven - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - name: Run unit tests - run: ./mvnw ${MAVEN_ARGS} clean install --file pom.xml - - build: - uses: ./.github/workflows/build.yml \ No newline at end of file diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 511f10a8e0..2c0a63d50d 100644 --- a/.github/workflows/hugo.yaml +++ b/.github/workflows/hugo.yaml @@ -41,7 +41,7 @@ jobs: - name: Install Dart Sass run: sudo snap install dart-sass - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive fetch-depth: 0 @@ -68,7 +68,7 @@ jobs: --minify \ --baseURL "${{ steps.pages.outputs.base_url }}/" - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: ./docs/public diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d5aca2ad54..fdb8897c07 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -29,22 +29,22 @@ jobs: continue-on-error: ${{ inputs.experimental }} timeout-minutes: 40 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ inputs.checkout-ref }} - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ inputs.java-version }} cache: 'maven' - name: Set up Minikube - uses: manusa/actions-setup-minikube@v2.13.1 + uses: manusa/actions-setup-minikube@v2.14.0 with: - minikube version: 'v1.34.0' + minikube version: 'v1.36.0' kubernetes version: '${{ inputs.kube-version }}' - driver: 'docker' - github token: ${{ secrets.GITHUB_TOKEN }} + github token: ${{ github.token }} + - name: "${{inputs.it-category}} integration tests (kube: ${{ inputs.kube-version }} / java: ${{ inputs.java-version }} / client: ${{ inputs.http-client }})" run: | if [ -z "${{inputs.it-category}}" ]; then diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dea18217f9..79660cfb1b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,12 +17,12 @@ jobs: check_format_and_unit_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 25 cache: 'maven' - name: Check code format run: | diff --git a/.github/workflows/release-project-in-dir.yml b/.github/workflows/release-project-in-dir.yml index dc79b6f6c2..0313aebe4d 100644 --- a/.github/workflows/release-project-in-dir.yml +++ b/.github/workflows/release-project-in-dir.yml @@ -19,16 +19,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout "${{inputs.version_branch}}" branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: "${{inputs.version_branch}}" - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: temurin cache: 'maven' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Change version to release version # Assume that RELEASE_VERSION will have form like: "v1.0.1". So we cut the "v" @@ -37,15 +42,12 @@ jobs: env: RELEASE_VERSION: ${{ github.event.release.tag_name }} - - name: Release Maven package - uses: samuelmeuli/action-maven-publish@v1 - with: - maven_profiles: "release" - maven_args: ${{ env.MAVEN_ARGS }} - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - nexus_username: ${{ secrets.OSSRH_USERNAME }} - nexus_password: ${{ secrets.OSSRH_TOKEN }} + - name: Publish to Apache Maven Central + run: mvn package deploy -Prelease + env: + MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} # This is separate job because there were issues with git after release step, was not able to commit changes. update-working-version: @@ -54,12 +56,12 @@ jobs: if: "!contains(github.event.release.tag_name, 'RC')" # not sure we should keep this the RC part steps: - name: Checkout "${{inputs.version_branch}}" branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: "${{inputs.version_branch}}" - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: temurin @@ -78,4 +80,4 @@ jobs: uses: ad-m/github-push-action@master with: branch: "${{inputs.version_branch}}" - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index 66fe9d25a3..0f560dd2cb 100644 --- a/.github/workflows/snapshot-releases.yml +++ b/.github/workflows/snapshot-releases.yml @@ -16,12 +16,12 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: 'maven' - name: Build and test project run: ./mvnw ${MAVEN_ARGS} clean install --file pom.xml @@ -29,18 +29,22 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: + java-version: 21 distribution: temurin - java-version: 17 cache: 'maven' - - name: Release Maven package - uses: samuelmeuli/action-maven-publish@v1 - with: - maven_profiles: "release" - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - nexus_username: ${{ secrets.OSSRH_USERNAME }} - nexus_password: ${{ secrets.OSSRH_TOKEN }} + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Publish to Apache Maven Central + run: mvn package deploy -Prelease + env: + MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index b7a96edef6..132575edaa 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -23,12 +23,12 @@ jobs: runs-on: ubuntu-latest if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 25 cache: 'maven' - name: Cache SonarCloud packages uses: actions/cache@v4 diff --git a/.gitignore b/.gitignore index 6f4bbe6a01..638e4a93f2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ target/ .mvn/wrapper/maven-wrapper.jar -.java-version \ No newline at end of file +.java-version +.aider* diff --git a/README.md b/README.md index 86edf232b0..5bb2758ae5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ conversion hooks and dynamic admission controllers are supported as a separate p Under the hood it uses the excellent [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client), which provides additional facilities, like generating CRD from source code (and vice versa). +Icon + +Java Operator SDK is a CNCF project as part of [Operator Framework](https://github.com/operator-framework). + ## Documentation Documentation can be found on the **[JOSDK WebSite](https://javaoperatorsdk.io/)**. @@ -43,7 +47,7 @@ It makes it easy to implement best practices and patterns for an Operator. Featu * Easy to use Error Handling * ... and everything that a batteries included framework needs -For all features and their usage see the [related section on the website](https://javaoperatorsdk.io/docs/features). +For all features and their usage see the [related sections on the website](https://javaoperatorsdk.io/docs/documentation/). ## Related Projects @@ -63,6 +67,7 @@ projects want to advertise that fact here. For this reason, we ask that if you'd to be featured in this section, please open a PR, adding a link to and short description of your project, as shown below: +- [kroxylicious](https://github.com/kroxylicious/kroxylicious/tree/main/kroxylicious-operator) Kafka proxy operator - [ExposedApp operator](https://github.com/halkyonio/exposedapp-rhdblog): a sample operator written to illustrate JOSDK concepts and its Quarkus extension in the ["Write Kubernetes Operators in Java with the Java Operator SDK" blog series](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk#). diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 535945760f..5952379112 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT bootstrapper @@ -14,10 +14,10 @@ Operator SDK - Bootstrapper Maven Plugin - 3.15.1 - 3.9.9 + 3.15.2 + 3.9.11 3.0.0 - 3.15.1 + 3.15.2 @@ -58,7 +58,7 @@ commons-io commons-io - 2.18.0 + 2.20.0 com.github.spullara.mustache.java diff --git a/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java b/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java index a8d43c60db..59eae8b01c 100644 --- a/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java +++ b/bootstrapper-maven-plugin/src/main/resources/templates/ConfigMapDependentResource.java @@ -17,10 +17,6 @@ public class ConfigMapDependentResource public static final String KEY = "key"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired({{artifactClassId}}CustomResource primary, Context<{{artifactClassId}}CustomResource> context) { diff --git a/bootstrapper-maven-plugin/src/main/resources/templates/pom.xml b/bootstrapper-maven-plugin/src/main/resources/templates/pom.xml index 11d4288421..09e8ed0ef8 100644 --- a/bootstrapper-maven-plugin/src/main/resources/templates/pom.xml +++ b/bootstrapper-maven-plugin/src/main/resources/templates/pom.xml @@ -15,7 +15,7 @@ ${java.version} ${java.version} {{josdkVersion}} - 1.7.36 + 2.0.17 5.9.2 2.20.0 {{fabric8Version}} diff --git a/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java b/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java similarity index 96% rename from bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java rename to bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java index f7840c1585..ec1399fcf8 100644 --- a/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java +++ b/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java @@ -13,9 +13,9 @@ import static org.assertj.core.api.Assertions.assertThat; -class BootstrapperTest { +class BootstrapperIT { - private static final Logger log = LoggerFactory.getLogger(BootstrapperTest.class); + private static final Logger log = LoggerFactory.getLogger(BootstrapperIT.class); Bootstrapper bootstrapper = new Bootstrapper(); diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index bd51e0af11..76f3db9abc 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT caffeine-bounded-cache-support diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java index 05d31a7479..532e5237f8 100644 --- a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java +++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java @@ -44,7 +44,7 @@ void reconciliationWorksWithLimitedCache() { private void assertConfigMapsDeleted() { await() - .atMost(Duration.ofSeconds(30)) + .atMost(Duration.ofSeconds(120)) .untilAsserted( () -> IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index db177d4ac7..5ea571c69d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,28 +1,57 @@ -# How to Contribute +# Contributing to Java Operator SDK Documentation -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. +Thank you for your interest in improving the Java Operator SDK documentation! We welcome contributions from the community and appreciate your help in making our documentation better. -## Contributor License Agreement +## How to Contribute -Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; -this simply gives us permission to use and redistribute your contributions as -part of the project. Head over to to see -your current agreements on file or to sign a new one. +### Getting Started -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. +1. **Fork the repository** and clone your fork locally +2. **Create a new branch** for your changes +3. **Make your improvements** to the documentation +4. **Test your changes** locally using `hugo server` +5. **Submit a pull request** with a clear description of your changes -## Code reviews +### Types of Contributions -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. +We welcome various types of contributions: + +- **Content improvements**: Fix typos, clarify explanations, add examples +- **New documentation**: Add missing sections or entirely new guides +- **Structural improvements**: Better organization, navigation, or formatting +- **Translation**: Help translate documentation to other languages + +## Guidelines + +### Writing Style + +- Use clear, concise language +- Write in active voice when possible +- Define technical terms when first used +- Include practical examples where helpful +- Keep sentences and paragraphs reasonably short + +### Technical Requirements + +- Test all code examples to ensure they work +- Use proper markdown formatting +- Follow existing documentation structure and conventions +- Ensure links work and point to current resources + +## Legal Requirements + +### Contributor License Agreement + +All contributions must be accompanied by a Contributor License Agreement (CLA). You (or your employer) retain the copyright to your contribution; the CLA simply gives us permission to use and redistribute your contributions as part of the project. + +Visit to see your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one (even for a different project), you probably don't need to do it again. + +### Code Review Process + +All submissions, including those by project members, require review. We use GitHub pull requests for this purpose. Please consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines -This project follows -[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). +This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). diff --git a/docs/README.md b/docs/README.md index f9d6ce7183..14f675b53b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,190 +1,82 @@ -# JOSDK comments: +# Java Operator SDK Documentation -see: sample github action: https://gohugo.io/hosting-and-deployment/hosting-on-github/ +This repository contains the documentation website for the Java Operator SDK (JOSDK), built using Hugo and the Docsy theme. -currently use hugo version v0.125.7 +## About Java Operator SDK -# Docsy Example +Java Operator SDK is a framework that makes it easy to build Kubernetes operators in Java. It provides APIs designed to feel natural to Java developers and handles common operator challenges automatically, allowing you to focus on your business logic. -[Docsy][] is a [Hugo theme module][] for technical documentation sites, providing easy -site navigation, structure, and more. This **Docsy Example Project** uses the Docsy -theme component as a hugo module and provides a skeleton documentation structure for you to use. -You can clone/copy this project and edit it with your own content, or use it as an example. +## Development Setup -In this project, the Docsy theme is pulled in as a Hugo module, together with -its dependencies: +This documentation site uses Hugo v0.125.7 with the Docsy theme. -```console -$ hugo mod graph -... -``` - -For Docsy documentation, see [Docsy user guide][]. - -This Docsy Example Project is hosted on [Netlify][] at [example.docsy.dev][]. -You can view deploy logs from the [deploy section of the project's Netlify -dashboard][deploys], or this [alternate dashboard][]. - -This is not an officially supported Google product. This project is currently maintained. - -## Using the Docsy Example Project as a template - -A simple way to get started is to use this project as a template, which gives you a site project that is set up and ready to use. To do this: - -1. Use the dropdown for switching branches/tags to change to the **latest** released tag. - -2. Click **Use this template**. - -3. Select a name for your new project and click **Create repository from template**. - -4. Make your own local working copy of your new repo using git clone, replacing https://github.com/me/example.git with your repo’s web URL: - -```bash -git clone --depth 1 https://github.com/me/example.git -``` +## Prerequisites -You can now edit your own versions of the site’s source files. +- Hugo v0.125.7 or later (extended version required) +- Node.js and npm (for PostCSS processing) +- Git -If you want to do SCSS edits and want to publish these, you need to install `PostCSS` +## Local Development -```bash -npm install -``` - -## Running the website locally - -Building and running the site locally requires a recent `extended` version of [Hugo](https://gohugo.io). -You can find out more about how to install Hugo for your environment in our -[Getting started](https://www.docsy.dev/docs/getting-started/#prerequisites-and-installation) guide. - -Once you've made your working copy of the site repo, from the repo root folder, run: - -```bash -hugo server -``` +### Quick Start -## Running a container locally +1. Clone this repository +2. Install dependencies: + ```bash + npm install + ``` +3. Start the development server: + ```bash + hugo server + ``` +4. Open your browser to `http://localhost:1313` -You can run docsy-example inside a [Docker](https://docs.docker.com/) -container, the container runs with a volume bound to the `docsy-example` -folder. This approach doesn't require you to install any dependencies other -than [Docker Desktop](https://www.docker.com/products/docker-desktop) on -Windows and Mac, and [Docker Compose](https://docs.docker.com/compose/install/) -on Linux. +### Using Docker -1. Build the docker image +You can also run the documentation site using Docker: +1. Build the container: ```bash docker-compose build ``` - -1. Run the built image - +2. Run the container: ```bash docker-compose up ``` + > **Note**: You can combine both commands with `docker-compose up --build` - > NOTE: You can run both commands at once with `docker-compose up --build`. +3. Access the site at `http://localhost:1313` -1. Verify that the service is working. - - Open your web browser and type `http://localhost:1313` in your navigation bar, - This opens a local instance of the docsy-example homepage. You can now make - changes to the docsy example and those changes will immediately show up in your - browser after you save. - -### Cleanup - -To stop Docker Compose, on your terminal window, press **Ctrl + C**. - -To remove the produced images run: +To stop the container, press **Ctrl + C** in your terminal. +To clean up Docker resources: ```bash docker-compose rm ``` -For more information see the [Docker Compose documentation][]. - -## Using a local Docsy clone - -Make sure your installed go version is `1.18` or higher. - -Clone the latest version of the docsy theme into the parent folder of your project. The newly created repo should now reside in a sibling folder of your site's root folder. - -```shell -cd root-of-your-site -git clone --branch v0.7.2 https://github.com/google/docsy.git ../docsy -``` - -Now run: - -```shell -HUGO_MODULE_WORKSPACE=docsy.work hugo server --ignoreVendorPaths "**" -``` - -or, when using npm, prepend `local` to the script you want to invoke, e.g.: - -```shell -npm run local serve -``` - -By using the `HUGO_MODULE_WORKSPACE` directive (either directly or via prefix `local` when using npm), the server now watches all files and directories inside the sibling directory `../docsy` , too. Any changes inside the local `docsy` theme clone are now immediately picked up (hot reload), you can instantly see the effect of your local edits. -In the command above, we used the environment variable `HUGO_MODULE_WORKSPACE` to tell hugo about the local workspace file `docsy.work`. Alternatively, you can declare the workspace file inside your settings file `hugo.toml`: - -```toml -[module] - workspace = "docsy.work" -``` +## Contributing -Your project's `hugo.toml` file already contains these lines, the directive for workspace assignment is commented out, however. Remove the two trailing comment characters '//' so that this line takes effect. +We welcome contributions to improve the documentation! Please see our [contribution guidelines](CONTRIBUTING.md) for details on how to get started. ## Troubleshooting -As you run the website locally, you may run into the following error: - -```console -$ hugo server -WARN 2023/06/27 16:59:06 Module "project" is not compatible with this Hugo version; run "hugo mod graph" for more information. -Start building sites … -hugo v0.101.0-466fa43c16709b4483689930a4f9ac8add5c9f66+extended windows/amd64 BuildDate=2022-06-16T07:09:16Z VendorInfo=gohugoio -Error: Error building site: "C:\Users\foo\path\to\docsy-example\content\en\_index.md:5:1": failed to extract shortcode: template for shortcode "blocks/cover" not found -Built in 27 ms -``` - -This error occurs if you are running an outdated version of Hugo. As of docsy theme version `v0.7.0`, hugo version `0.110.0` or higher is required. -See this [section](https://www.docsy.dev/docs/get-started/docsy-as-module/installation-prerequisites/#install-hugo) of the user guide for instructions on how to install Hugo. - -Or you may be confronted with the following error: - +### Module Compatibility Error +If you see an error about module compatibility, ensure you're using Hugo v0.110.0 or higher: ```console -$ hugo server - -INFO 2021/01/21 21:07:55 Using config file: -Building sites … INFO 2021/01/21 21:07:55 syncing static files to / -Built in 288 ms -Error: Error building site: TOCSS: failed to transform "scss/main.scss" (text/x-scss): resource "scss/scss/main.scss_9fadf33d895a46083cdd64396b57ef68" not found in file cache +Error: Error building site: failed to extract shortcode: template for shortcode "blocks/cover" not found ``` -This error occurs if you have not installed the extended version of Hugo. -See this [section](https://www.docsy.dev/docs/get-started/docsy-as-module/installation-prerequisites/#install-hugo) of the user guide for instructions on how to install Hugo. - -Or you may encounter the following error: - +### SCSS Processing Error +If you encounter SCSS-related errors, make sure you have the extended version of Hugo installed: ```console -$ hugo server - -Error: failed to download modules: binary with name "go" not found +Error: TOCSS: failed to transform "scss/main.scss" ``` -This error occurs if you have not installed the `go` programming language on your system. -See this [section](https://www.docsy.dev/docs/get-started/docsy-as-module/installation-prerequisites/#install-go-language) of the user guide for instructions on how to install `go`. +### Go Binary Not Found +If you see "binary with name 'go' not found", install the Go programming language from [golang.org](https://golang.org). +## Links -[alternate dashboard]: https://app.netlify.com/sites/goldydocs/deploys -[deploys]: https://app.netlify.com/sites/docsy-example/deploys -[Docsy user guide]: https://docsy.dev/docs -[Docsy]: https://github.com/google/docsy -[example.docsy.dev]: https://example.docsy.dev -[Hugo theme module]: https://gohugo.io/hugo-modules/use-modules/#use-a-module-for-a-theme -[Netlify]: https://netlify.com -[Docker Compose documentation]: https://docs.docker.com/compose/gettingstarted/ +- [Hugo Documentation](https://gohugo.io/documentation/) +- [Docsy Theme Documentation](https://www.docsy.dev/docs/) +- [Java Operator SDK GitHub Repository](https://github.com/operator-framework/java-operator-sdk) diff --git a/docs/content/en/_index.md b/docs/content/en/_index.md index f2124a21a2..f375ebfb97 100644 --- a/docs/content/en/_index.md +++ b/docs/content/en/_index.md @@ -33,7 +33,7 @@ We do a [Pull Request](https://github.com/operator-framework/java-operator-sdk/p {{% /blocks/feature %}} -{{% blocks/feature icon="fab fa-twitter" title="Follow us on Twitter!" url="https://twitter.com/javaoperatorsdk" %}} +{{% blocks/feature icon="fa-brands fa-bluesky" title="Follow us on BlueSky!" url="https://bsky.app/profile/javaoperatorsdk.bsky.social" %}} For announcement of latest features etc. {{% /blocks/feature %}} diff --git a/docs/content/en/blog/_index.md b/docs/content/en/blog/_index.md index c8219f7994..e792e415fe 100644 --- a/docs/content/en/blog/_index.md +++ b/docs/content/en/blog/_index.md @@ -1,6 +1,6 @@ --- title: Blog -menu: {main: {weight: 30}} +menu: {main: {weight: 2}} --- This is the **blog** section. It has two categories: News and Releases. diff --git a/docs/content/en/blog/news/_index.md b/docs/content/en/blog/news/_index.md index 646c97f954..aaf1c2adcd 100644 --- a/docs/content/en/blog/news/_index.md +++ b/docs/content/en/blog/news/_index.md @@ -1,4 +1,4 @@ --- title: Posts -weight: 20 +weight: 220 --- diff --git a/docs/content/en/blog/news/primary-cache-for-next-recon.md b/docs/content/en/blog/news/primary-cache-for-next-recon.md new file mode 100644 index 0000000000..67326a6f17 --- /dev/null +++ b/docs/content/en/blog/news/primary-cache-for-next-recon.md @@ -0,0 +1,92 @@ +--- +title: How to guarantee allocated values for next reconciliation +date: 2025-05-22 +author: >- + [Attila Mészáros](https://github.com/csviri) and [Chris Laprun](https://github.com/metacosm) +--- + +We recently released v5.1 of Java Operator SDK (JOSDK). One of the highlights of this release is related to a topic of +so-called +[allocated values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values +). + +To describe the problem, let's say that our controller needs to create a resource that has a generated identifier, i.e. +a resource which identifier cannot be directly derived from the custom resource's desired state as specified in its +`spec` field. To record the fact that the resource was successfully created, and to avoid attempting to +recreate the resource again in subsequent reconciliations, it is typical for this type of controller to store the +generated identifier in the custom resource's `status` field. + +The Java Operator SDK relies on the informers' cache to retrieve resources. These caches, however, are only guaranteed +to be eventually consistent. It could happen that, if some other event occurs, that would result in a new +reconciliation, **before** the update that's been made to our resource status has the chance to be propagated first to +the cluster and then back to the informer cache, that the resource in the informer cache does **not** contain the latest +version as modified by the reconciler. This would result in a new reconciliation where the generated identifier would be +missing from the resource status and, therefore, another attempt to create the resource by the reconciler, which is not +what we'd like. + +Java Operator SDK now provides a utility class [ +`PrimaryUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java) +to handle this particular use case. Using that overlay cache, your reconciler is guaranteed to see the most up-to-date +version of the resource on the next reconciliation: + +```java + +@Override +public UpdateControl reconcile( + StatusPatchCacheCustomResource resource, + Context context) { + + // omitted code + + var freshCopy = createFreshCopy(resource); // need fresh copy just because we use the SSA version of update + freshCopy + .getStatus() + .setValue(statusWithAllocatedValue()); + + // using the utility instead of update control to patch the resource status + var updated = + PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context); + return UpdateControl.noUpdate(); +} +``` + +How does `PrimaryUpdateAndCacheUtils` work? +There are multiple ways to solve this problem, but ultimately, we only provide the solution described below. If you +want to dig deep in alternatives, see +this [PR](https://github.com/operator-framework/java-operator-sdk/pull/2800/files). + +The trick is to intercept the resource that the reconciler updated and cache that version in an additional cache on top +of the informer's cache. Subsequently, if the reconciler needs to read the resource, the SDK will first check if it is +in the overlay cache and read it from there if present, otherwise read it from the informer's cache. If the informer +receives an event with a fresh resource, we always remove the resource from the overlay cache, since that is a more +recent resource. But this **works only** if the reconciler updates the resource using **optimistic locking**. +If the update fails on conflict, because the resource has already been updated on the cluster before we got +the chance to get our update in, we simply wait and poll the informer cache until the new resource version from the +server appears in the informer's cache, +and then try to apply our updates to the resource again using the updated version from the server, again with optimistic +locking. + +So why is optimistic locking required? We hinted at it above, but the gist of it, is that if another party updates the +resource before we get a chance to, we wouldn't be able to properly handle the resulting situation correctly in all +cases. The informer would receive that new event before our own update would get a chance to propagate. Without +optimistic locking, there wouldn't be a fail-proof way to determine which update should prevail (i.e. which occurred +first), in particular in the event of the informer losing the connection to the cluster or other edge cases (the joys of +distributed computing!). + +Optimistic locking simplifies the situation and provides us with stronger guarantees: if the update succeeds, then we +can be sure we have the proper resource version in our caches. The next event will contain our update in all cases. +Because we know that, we can also be sure that we can evict the cached resource in the overlay cache whenever we receive +a new event. The overlay cache is only used if the SDK detects that the original resource (i.e. the one before we +applied our status update in the example above) is still in the informer's cache. + +The following diagram sums up the process: + +```mermaid +flowchart TD + A["Update Resource with Lock"] --> B{"Is Successful"} + B -- Fails on conflict --> D["Poll the Informer cache until resource updated"] + D --> A + B -- Yes --> n2{"Original resource still in informer cache?"} + n2 -- Yes --> C["Cache the resource in overlay cache"] + n2 -- No --> n3["Informer cache already contains up-to-date version, do not use overlay cache"] +``` diff --git a/docs/content/en/blog/releases/_index.md b/docs/content/en/blog/releases/_index.md index 9143a23148..dbf2ee1729 100644 --- a/docs/content/en/blog/releases/_index.md +++ b/docs/content/en/blog/releases/_index.md @@ -1,4 +1,4 @@ --- title: Releases -weight: 20 +weight: 230 --- diff --git a/docs/content/en/community/_index.md b/docs/content/en/community/_index.md index 3f237b8a79..fa42c2d974 100644 --- a/docs/content/en/community/_index.md +++ b/docs/content/en/community/_index.md @@ -1,6 +1,6 @@ --- title: Community -menu: {main: {weight: 40}} +menu: {main: {weight: 3}} --- diff --git a/docs/content/en/docs/_index.md b/docs/content/en/docs/_index.md index 76486e22f7..5c7b74ab4b 100755 --- a/docs/content/en/docs/_index.md +++ b/docs/content/en/docs/_index.md @@ -1,8 +1,6 @@ --- title: Documentation linkTitle: Docs -menu: {main: {weight: 20}} -weight: 20 +menu: {main: {weight: 1}} +weight: 1 --- - - diff --git a/docs/content/en/docs/architecture/_index.md b/docs/content/en/docs/architecture/_index.md deleted file mode 100644 index a29f70c4f6..0000000000 --- a/docs/content/en/docs/architecture/_index.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Architecture and Internals -weight: 90 ---- - - -This document gives an overview of the internal structure and components of Java Operator SDK core, -in order to make it easier for developers to understand and contribute to it. This document is -not intended to be a comprehensive reference, rather an introduction to the core concepts and we -hope that the other parts should be fairly easy to understand. We will evolve this document -based on the community's feedback. - -## The Big Picture and Core Components - -![JOSDK architecture](/images/architecture.svg) - -An [Operator](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java) -is a set of -independent [controllers](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java) -. -The `Controller` class, however, is an internal class managed by the framework itself and -usually shouldn't interacted with directly by end users. It -manages all the processing units involved with reconciling a single type of Kubernetes resource. - -Other components include: - -- [Reconciler](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) - is the primary entry-point for the developers of the framework to implement the reconciliation - logic. -- [EventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java) - represents a source of events that might eventually trigger a reconciliation. -- [EventSourceManager](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java) - aggregates all the event sources associated with a controller. Manages the event sources' - lifecycle. -- [ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java) - is a central event source that watches the resources associated with the controller (also - called primary resources) for changes, propagates events and caches the related state. -- [EventProcessor](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java) - processes the incoming events and makes sure they are executed in a sequential manner, that is - making sure that the events are processed in the order they are received for a given resource, - despite requests being processed concurrently overall. The `EventProcessor` also takes care of - re-scheduling or retrying requests as needed. -- [ReconcilerDispatcher](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java) - is responsible for dispatching requests to the appropriate `Reconciler` method and handling - the reconciliation results, making the instructed Kubernetes API calls. - -## Typical Workflow - -A typical workflows looks like following: - -1. An `EventSource` produces an event, that is propagated to the `EventProcessor`. -2. The resource associated with the event is read from the internal cache. -3. If the resource is not already being processed, a reconciliation request is - submitted to the executor service to be executed in a different thread, encapsulated in a - `ControllerExecution` instance. -4. This, in turns, calls the `ReconcilerDispatcher` which dispatches the call to the appropriate - `Reconciler` method, passing along all the required information. -5. Once the `Reconciler` is done, what happens depends on the result returned by the - `Reconciler`. If needed, the `ReconcilerDispatcher` will make the appropriate calls to the - Kubernetes API server. -6. Once the `Reconciler` is done, the `EventProcessor` is called back to finalize the - execution and update the controller's state. -7. The `EventProcessor` checks if the request needs to be rescheduled or retried and if there are no - subsequent events received for the same resource. -8. When none of this happens, the processing of the event is finished. diff --git a/docs/content/en/docs/configuration/_index.md b/docs/content/en/docs/configuration/_index.md deleted file mode 100644 index 11929e3358..0000000000 --- a/docs/content/en/docs/configuration/_index.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Configuring JOSDK -layout: docs -permalink: /docs/configuration ---- - -# Configuration options - -The Java Operator SDK (JOSDK) provides several abstractions that work great out of the -box. However, while we strive to cover the most common cases with the default behavior, we also -recognize that that default behavior is not always what any given user might want for their -operator. Numerous configuration options are therefore provided to help people tailor the -framework to their needs. - -Configuration options act at several levels, depending on which behavior you wish to act upon: -- `Operator`-level using `ConfigurationService` -- `Reconciler`-level using `ControllerConfiguration` -- `DependentResouce`-level using the `DependentResourceConfigurator` interface -- `EventSource`-level: some event sources, such as `InformerEventSource`, might need to be - fine-tuned to properly identify which events will trigger the associated reconciler. - -## Operator-level configuration - -Configuration that impacts the whole operator is performed via the `ConfigurationService` class. -`ConfigurationService` is an abstract class, and the implementation can be different based -on which flavor of the framework is used. For example Quarkus Operator SDK replaces the -default implementation. Configurations are initialized with sensible defaults, but can -be changed during initialization. - -For instance, if you wish to not validate that the CRDs are present on your cluster when the -operator starts and configure leader election, you would do something similar to: - -```java -Operator operator = new Operator( override -> override - .checkingCRDAndValidateLocalModel(false) - .withLeaderElectionConfiguration(new LeaderElectionConfiguration("bar", "barNS"))); -``` - -## Reconciler-level configuration - -While reconcilers are typically configured using the `@ControllerConfiguration` annotation, it -is also possible to override the configuration at runtime, when the reconciler is registered -with the operator instance, either by passing it a completely new `ControllerConfiguration` -instance or by preferably overriding some aspects of the current configuration using a -`ControllerConfigurationOverrider` `Consumer`: - -```java -Operator operator; -Reconciler reconciler; -... -operator.register(reconciler, configOverrider -> - configOverrider.withFinalizer("my-nifty-operator/finalizer").withLabelSelector("foo=bar")); -``` - -## DependentResource-level configuration - -`DependentResource` implementations can implement the `DependentResourceConfigurator` interface -to pass information to the implementation. For example, the SDK -provides specific support for the `KubernetesDependentResource`, which can be configured via the -`@KubernetesDependent` annotation. This annotation is, in turn, converted into a -`KubernetesDependentResourceConfig` instance, which is then passed to the `configureWith` method -implementation. - -TODO: still subject to change / uniformization - -## EventSource-level configuration - -TODO diff --git a/docs/content/en/docs/contributing/_index.md b/docs/content/en/docs/contributing/_index.md index dfe6dec99c..0ab40d55b1 100644 --- a/docs/content/en/docs/contributing/_index.md +++ b/docs/content/en/docs/contributing/_index.md @@ -1,84 +1,68 @@ --- -title: Contributing To Java Operator SDK -weight: 100 +title: Contributing +weight: 110 --- -First of all, we'd like to thank you for considering contributing to the project! We really -hope to create a vibrant community around this project but this won't happen without help from -people like you! +Thank you for considering contributing to the Java Operator SDK project! We're building a vibrant community and need help from people like you to make it happen. ## Code of Conduct -We are serious about making this a welcoming, happy project. We will not tolerate discrimination, -aggressive or insulting behaviour. +We're committed to making this a welcoming, inclusive project. We do not tolerate discrimination, aggressive or insulting behavior. -To this end, the project and everyone participating in it is bound by the [Code of -Conduct]({{baseurl}}/coc). By participating, you are expected to uphold this code. Please report -unacceptable behaviour to any of the project admins. +This project and all participants are bound by our [Code of Conduct]({{baseurl}}/coc). By participating, you're expected to uphold this code. Please report unacceptable behavior to any project admin. -## Bugs +## Reporting Bugs -If you find a bug, -please [open an issue](https://github.com/java-operator-sdk/java-operator-sdk/issues)! Do try -to include all the details needed to recreate your problem. This is likely to include: +Found a bug? Please [open an issue](https://github.com/java-operator-sdk/java-operator-sdk/issues)! Include all details needed to recreate the problem: -- The version of the Operator SDK being used -- The exact platform and version of the platform that you're running on -- The steps taken to cause the bug -- Reproducer code is also very welcome to help us diagnose the issue and fix it quickly +- Operator SDK version being used +- Exact platform and version you're running on +- Steps to reproduce the bug +- Reproducer code (very helpful for quick diagnosis and fixes) -## Building Features and Documentation +## Contributing Features and Documentation -If you're looking for something to work on, take look at the issue tracker, in particular any items -labelled [good first issue](https://github.com/java-operator-sdk/java-operator-sdk/labels/good%20first%20issue) -. -Please leave a comment on the issue to mention that you have started work, in order to avoid -multiple people working on the same issue. +Looking for something to work on? Check the issue tracker, especially items labeled [good first issue](https://github.com/java-operator-sdk/java-operator-sdk/labels/good%20first%20issue). Please comment on the issue when you start work to avoid duplicated effort. -If you have an idea for a feature - whether or not you have time to work on it - please also open an -issue describing your feature and label it "enhancement". We can then discuss it as a community and -see what can be done. Please be aware that some features may not align with the project goals and -might therefore be closed. In particular, please don't start work on a new feature without -discussing it first to avoid wasting effort. We do commit to listening to all proposals and will do -our best to work something out! +### Feature Ideas -Once you've got the go ahead to work on a feature, you can start work. Feel free to communicate with -team via updates on the issue tracker or the [Discord channel](https://discord.gg/DacEhAy) and ask -for feedback, pointers etc. Once you're happy with your code, go ahead and open a Pull Request. +Have a feature idea? Open an issue labeled "enhancement" even if you can't work on it immediately. We'll discuss it as a community and see what's possible. -## Pull Request Process +**Important**: Some features may not align with project goals. Please discuss new features before starting work to avoid wasted effort. We commit to listening to all proposals and working something out when possible. + +### Development Process -First, please format your commit messages so that they follow -the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format. +Once you have approval to work on a feature: +1. Communicate progress via issue updates or our [Discord channel](https://discord.gg/DacEhAy) +2. Ask for feedback and pointers as needed +3. Open a Pull Request when ready + +## Pull Request Process -On opening a PR, a GitHub action will execute the test suite against the new code. All code is -required to pass the tests, and new code must be accompanied by new tests. +### Commit Messages +Format commit messages following [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format. -All PRs have to be reviewed and signed off by another developer before being merged. This review -will likely ask for some changes to the code - please don't be alarmed or upset -at this; it is expected that all PRs will need tweaks and a normal part of the process. +### Testing and Review +- GitHub Actions will run the test suite on your PR +- All code must pass tests +- New code must include new tests +- All PRs require review and sign-off from another developer +- Expect requests for changes - this is normal and part of the process +- PRs must comply with Java Google code style -The PRs are checked to be compliant with the Java Google code style. +### Licensing +All Operator SDK code is released under the [Apache 2.0 licence](LICENSE). -Be aware that all Operator SDK code is released under the [Apache 2.0 licence](LICENSE). +## Development Environment Setup -## Development environment setup +### Code Style -### Code style +SDK modules and samples follow Java Google code style. Code gets formatted automatically on every `compile`, but to avoid PR rejections due to style issues, set up your IDE: -The SDK modules and samples are formatted to follow the Java Google code style. -On every `compile` the code gets formatted automatically, however, to make things simpler (i.e. -avoid getting a PR rejected simply because of code style issues), you can import one of the -following code style schemes based on the IDE you use: +**IntelliJ IDEA**: Install the [google-java-format](https://plugins.jetbrains.com/plugin/8527-google-java-format) plugin -- for *Intellij IDEA* - install [google-java-format](https://plugins.jetbrains.com/plugin/8527-google-java-format) plugin -- for *Eclipse* - follow [these intructions](https://github.com/google/google-java-format?tab=readme-ov-file#eclipse) +**Eclipse**: Follow [these instructions](https://github.com/google/google-java-format?tab=readme-ov-file#eclipse) -## Thanks +## Acknowledgments -These guidelines were based on several sources, including -[Atom](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [PurpleBooth's -advice](https://gist.github.com/PurpleBooth/b24679402957c63ec426) and the [Contributor -Covenant](https://www.contributor-covenant.org/). +These guidelines were inspired by [Atom](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [PurpleBooth's advice](https://gist.github.com/PurpleBooth/b24679402957c63ec426), and the [Contributor Covenant](https://www.contributor-covenant.org/). diff --git a/docs/content/en/docs/documentation/_index.md b/docs/content/en/docs/documentation/_index.md new file mode 100644 index 0000000000..59373c6974 --- /dev/null +++ b/docs/content/en/docs/documentation/_index.md @@ -0,0 +1,25 @@ +--- +title: Documentation +weight: 40 +--- + +# JOSDK Documentation + +This section contains detailed documentation for all Java Operator SDK features and concepts. Whether you're building your first operator or need advanced configuration options, you'll find comprehensive guides here. + +## Core Concepts + +- **[Implementing a Reconciler](reconciler/)** - The heart of any operator +- **[Architecture](architecture/)** - How JOSDK works under the hood +- **[Dependent Resources & Workflows](dependent-resource-and-workflows/)** - Managing resource relationships +- **[Configuration](configuration/)** - Customizing operator behavior +- **[Error Handling & Retries](error-handling-retries/)** - Managing failures gracefully + +## Advanced Features + +- **[Eventing](eventing/)** - Understanding the event-driven model +- **[Accessing Resources in Caches](working-with-es-caches/) - How to access resources in caches +- **[Observability](observability/)** - Monitoring and debugging your operators +- **[Other Features](features/)** - Additional capabilities and integrations + +Each guide includes practical examples and best practices to help you build robust, production-ready operators. diff --git a/docs/content/en/docs/documentation/architecture.md b/docs/content/en/docs/documentation/architecture.md new file mode 100644 index 0000000000..4108849c04 --- /dev/null +++ b/docs/content/en/docs/documentation/architecture.md @@ -0,0 +1,36 @@ +--- +title: Architecture and Internals +weight: 85 +--- + +This document provides an overview of the Java Operator SDK's internal structure and components to help developers understand and contribute to the project. While not a comprehensive reference, it introduces core concepts that should make other components easier to understand. + +## The Big Picture and Core Components + +![JOSDK architecture](/images/architecture.svg) + +An [Operator](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java) is a set of independent [controllers](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java). + +The `Controller` class is an internal class managed by the framework and typically shouldn't be interacted with directly. It manages all processing units involved with reconciling a single type of Kubernetes resource. + +### Core Components + +- **[Reconciler](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java)** - The primary entry point for developers to implement reconciliation logic +- **[EventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java)** - Represents a source of events that might trigger reconciliation +- **[EventSourceManager](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java)** - Aggregates all event sources for a controller and manages their lifecycle +- **[ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java)** - Central event source that watches primary resources associated with a given controller for changes, propagates events and caches state +- **[EventProcessor](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java)** - Processes incoming events sequentially per resource while allowing concurrent overall processing. Handles rescheduling and retrying +- **[ReconcilerDispatcher](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java)** - Dispatches requests to appropriate `Reconciler` methods and handles reconciliation results, making necessary Kubernetes API calls + +## Typical Workflow + +A typical workflow follows these steps: + +1. **Event Generation**: An `EventSource` produces an event and propagates it to the `EventProcessor` +2. **Resource Reading**: The resource associated with the event is read from the internal cache +3. **Reconciliation Submission**: If the resource isn't already being processed, a reconciliation request is submitted to the executor service in a different thread (encapsulated in a `ControllerExecution` instance) +4. **Dispatching**: The `ReconcilerDispatcher` is called, which dispatches the call to the appropriate `Reconciler` method with all required information +5. **Reconciler Execution**: Once the `Reconciler` completes, the `ReconcilerDispatcher` makes appropriate Kubernetes API server calls based on the returned result +6. **Finalization**: The `EventProcessor` is called back to finalize execution and update the controller's state +7. **Rescheduling Check**: The `EventProcessor` checks if the request needs rescheduling or retrying, and whether subsequent events were received for the same resource +8. **Completion**: When no further action is needed, event processing is finished diff --git a/docs/content/en/docs/documentation/configuration.md b/docs/content/en/docs/documentation/configuration.md new file mode 100644 index 0000000000..888804628f --- /dev/null +++ b/docs/content/en/docs/documentation/configuration.md @@ -0,0 +1,154 @@ +--- +title: Configurations +weight: 55 +--- + +The Java Operator SDK (JOSDK) provides abstractions that work great out of the box. However, we recognize that default behavior isn't always suitable for every use case. Numerous configuration options help you tailor the framework to your specific needs. + +Configuration options operate at several levels: +- **Operator-level** using `ConfigurationService` +- **Reconciler-level** using `ControllerConfiguration` +- **DependentResource-level** using the `DependentResourceConfigurator` interface +- **EventSource-level** where some event sources (like `InformerEventSource`) need fine-tuning to identify which events trigger the associated reconciler + +## Operator-Level Configuration + +Configuration that impacts the entire operator is performed via the `ConfigurationService` class. `ConfigurationService` is an abstract class with different implementations based on which framework flavor you use (e.g., Quarkus Operator SDK replaces the default implementation). Configurations initialize with sensible defaults but can be changed during initialization. + +For example, to disable CRD validation on startup and configure leader election: + +```java +Operator operator = new Operator( override -> override + .checkingCRDAndValidateLocalModel(false) + .withLeaderElectionConfiguration(new LeaderElectionConfiguration("bar", "barNS"))); +``` + +## Reconciler-Level Configuration + +While reconcilers are typically configured using the `@ControllerConfiguration` annotation, you can also override configuration at runtime when registering the reconciler with the operator. You can either: +- Pass a completely new `ControllerConfiguration` instance +- Override specific aspects using a `ControllerConfigurationOverrider` `Consumer` (preferred) + +```java +Operator operator; +Reconciler reconciler; +... +operator.register(reconciler, configOverrider -> + configOverrider.withFinalizer("my-nifty-operator/finalizer").withLabelSelector("foo=bar")); +``` + +## Dynamically Changing Target Namespaces + +A controller can be configured to watch a specific set of namespaces in addition of the +namespace in which it is currently deployed or the whole cluster. The framework supports +dynamically changing the list of these namespaces while the operator is running. +When a reconciler is registered, an instance of +[`RegisteredController`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java#L5) +is returned, providing access to the methods allowing users to change watched namespaces as the +operator is running. + +A typical scenario would probably involve extracting the list of target namespaces from a +`ConfigMap` or some other input but this part is out of the scope of the framework since this is +use-case specific. For example, reacting to changes to a `ConfigMap` would probably involve +registering an associated `Informer` and then calling the `changeNamespaces` method on +`RegisteredController`. + +```java + +public static void main(String[] args) { + KubernetesClient client = new DefaultKubernetesClient(); + Operator operator = new Operator(client); + RegisteredController registeredController = operator.register(new WebPageReconciler(client)); + operator.installShutdownHook(); + operator.start(); + + // call registeredController further while operator is running +} + +``` + +If watched namespaces change for a controller, it might be desirable to propagate these changes to +`InformerEventSources` associated with the controller. In order to express this, +`InformerEventSource` implementations interested in following such changes need to be +configured appropriately so that the `followControllerNamespaceChanges` method returns `true`: + +```java + +@ControllerConfiguration +public class MyReconciler implements Reconciler { + + @Override + public Map prepareEventSources( + EventSourceContext context) { + + InformerEventSource configMapES = + new InformerEventSource<>(InformerEventSourceConfiguration.from(ConfigMap.class, TestCustomResource.class) + .withNamespacesInheritedFromController(context) + .build(), context); + + return EventSourceUtils.nameEventSources(configMapES); + } + +} +``` + +As seen in the above code snippet, the informer will have the initial namespaces inherited from +controller, but also will adjust the target namespaces if it changes for the controller. + +See also +the [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace) +for this feature. + +## DependentResource-level configuration + +It is possible to define custom annotations to configure custom `DependentResource` implementations. In order to provide +such a configuration mechanism for your own `DependentResource` implementations, they must be annotated with the +`@Configured` annotation. This annotation defines 3 fields that tie everything together: + +- `by`, which specifies which annotation class will be used to configure your dependents, +- `with`, which specifies the class holding the configuration object for your dependents and +- `converter`, which specifies the `ConfigurationConverter` implementation in charge of converting the annotation + specified by the `by` field into objects of the class specified by the `with` field. + +`ConfigurationConverter` instances implement a single `configFrom` method, which will receive, as expected, the +annotation instance annotating the dependent resource instance to be configured, but it can also extract information +from the `DependentResourceSpec` instance associated with the `DependentResource` class so that metadata from it can be +used in the configuration, as well as the parent `ControllerConfiguration`, if needed. The role of +`ConfigurationConverter` implementations is to extract the annotation information, augment it with metadata from the +`DependentResourceSpec` and the configuration from the parent controller on which the dependent is defined, to finally +create the configuration object that the `DependentResource` instances will use. + +However, one last element is required to finish the configuration process: the target `DependentResource` class must +implement the `ConfiguredDependentResource` interface, parameterized with the annotation class defined by the +`@Configured` annotation `by` field. This interface is called by the framework to inject the configuration at the +appropriate time and retrieve the configuration, if it's available. + +For example, `KubernetesDependentResource`, a core implementation that the framework provides, can be configured via the +`@KubernetesDependent` annotation. This set up is configured as follows: + +```java + +@Configured( + by = KubernetesDependent.class, + with = KubernetesDependentResourceConfig.class, + converter = KubernetesDependentConverter.class) +public abstract class KubernetesDependentResource + extends AbstractEventSourceHolderDependentResource> + implements ConfiguredDependentResource> { + // code omitted +} +``` + +The `@Configured` annotation specifies that `KubernetesDependentResource` instances can be configured by using the +`@KubernetesDependent` annotation, which gets converted into a `KubernetesDependentResourceConfig` object by a +`KubernetesDependentConverter`. That configuration object is then injected by the framework in the +`KubernetesDependentResource` instance, after it's been created, because the class implements the +`ConfiguredDependentResource` interface, properly parameterized. + +For more information on how to use this feature, we recommend looking at how this mechanism is implemented for +`KubernetesDependentResource` in the core framework, `SchemaDependentResource` in the samples or `CustomAnnotationDep` +in the `BaseConfigurationServiceTest` test class. + +## EventSource-level configuration + +TODO diff --git a/docs/content/en/docs/documentation/dependent-resource-and-workflows/_index.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/_index.md new file mode 100644 index 0000000000..9446f7ceca --- /dev/null +++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/_index.md @@ -0,0 +1,9 @@ +--- +title: Dependent resources and workflows +weight: 70 +--- + +Dependent resources and workflows are features sometimes referenced as higher +level abstractions. These two related concepts provides an abstraction +over reconciliation of a single resource (Dependent resource) and the +orchestration of such resources (Workflows). \ No newline at end of file diff --git a/docs/content/en/docs/dependent-resources/_index.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md similarity index 97% rename from docs/content/en/docs/dependent-resources/_index.md rename to docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md index f79443de74..7416949869 100644 --- a/docs/content/en/docs/dependent-resources/_index.md +++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md @@ -1,6 +1,6 @@ --- -title: Dependent Resources -weight: 60 +title: Dependent resources +weight: 75 --- ## Motivations and Goals @@ -133,13 +133,9 @@ Deleted (or set to be garbage collected). The following example shows how to cre ```java -@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +@KubernetesDependent(informer = @Informer(labelSelector = SELECTOR)) class DeploymentDependentResource extends CRUDKubernetesDependentResource { - public DeploymentDependentResource() { - super(Deployment.class); - } - @Override protected Deployment desired(WebPage webPage, Context context) { var deploymentName = deploymentName(webPage); @@ -178,9 +174,10 @@ JOSDK will take the appropriate steps to wire everything together and call your `DependentResource` implementations `reconcile` method before your primary resource is reconciled. This makes sense in most use cases where the logic associated with the primary resource is usually limited to status handling based on the state of the secondary resources and the -resources are not dependent on each other. +resources are not dependent on each other. As an alternative, you can also invoke reconciliation explicitly, +event for managed workflows. -See [Workflows](https://javaoperatorsdk.io/docs/workflows) for more details on how the dependent +See [Workflows](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) for more details on how the dependent resources are reconciled. This behavior and automated handling is referred to as "managed" because the `DependentResource` @@ -188,12 +185,14 @@ instances are managed by JOSDK, an example of which can be seen below: ```java -@ControllerConfiguration( - labelSelector = SELECTOR, +@Workflow( dependents = { @Dependent(type = ConfigMapDependentResource.class), @Dependent(type = DeploymentDependentResource.class), - @Dependent(type = ServiceDependentResource.class) + @Dependent(type = ServiceDependentResource.class), + @Dependent( + type = IngressDependentResource.class, + reconcilePrecondition = ExposedIngressCondition.class) }) public class WebPageManagedDependentsReconciler implements Reconciler, ErrorStatusHandler { @@ -208,7 +207,6 @@ public class WebPageManagedDependentsReconciler webPage.setStatus(createStatus(name)); return UpdateControl.patchStatus(webPage); } - } ``` @@ -222,7 +220,7 @@ It is also possible to wire dependent resources programmatically. In practice th developer is responsible for initializing and managing the dependent resources as well as calling their `reconcile` method. However, this makes it possible for developers to fully customize the reconciliation process. Standalone dependent resources should be used in cases when the managed use -case does not fit. You can, of course, also use [Workflows](https://javaoperatorsdk.io/docs/workflows) when managing +case does not fit. You can, of course, also use [Workflows](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) when managing resources programmatically. You can see a commented example of how to do diff --git a/docs/content/en/docs/workflows/_index.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md similarity index 97% rename from docs/content/en/docs/workflows/_index.md rename to docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md index 620f8c5436..c5ee83a446 100644 --- a/docs/content/en/docs/workflows/_index.md +++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/workflows.md @@ -1,6 +1,6 @@ --- title: Workflows -weight: 70 +weight: 80 --- ## Overview @@ -12,12 +12,12 @@ depends on the state of other resources or cannot be processed until these other a given state or some condition holds true for them. Dealing with such scenarios are therefore rather common for operators and the purpose of the workflow feature of the Java Operator SDK (JOSDK) is to simplify supporting such cases in a declarative way. Workflows build on top of the -[dependent resources](https://javaoperatorsdk.io/docs/dependent-resources) feature. +[dependent resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) feature. While dependent resources focus on how a given secondary resource should be reconciled, workflows focus on orchestrating how these dependent resources should be reconciled. Workflows describe how as a set of -[dependent resources](https://javaoperatorsdk.io/docs/dependent-resources) (DR) depend on one +[dependent resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) (DR) depend on one another, along with the conditions that need to hold true at certain stages of the reconciliation process. @@ -135,7 +135,7 @@ public class SampleWorkflowReconciler implements Reconciler { + // reconciler implementation +} +``` + +**Custom retry implementation:** + +Specify a custom retry class in the `@ControllerConfiguration` annotation: + +```java +@ControllerConfiguration(retry = MyCustomRetry.class) +public class MyReconciler implements Reconciler { + // reconciler implementation +} +``` + +Your custom retry class must: +- Provide a no-argument constructor for automatic instantiation +- Optionally implement `AnnotationConfigurable` for configuration from annotations. See [`GenericRetry`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java#) + implementation for more details. + +### Accessing Retry Information + +The [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) object provides retry state information: + +```java +@Override +public UpdateControl reconcile(MyResource resource, Context context) { + if (context.isLastAttempt()) { + // Handle final retry attempt differently + resource.getStatus().setErrorMessage("Failed after all retry attempts"); + return UpdateControl.patchStatus(resource); + } + + // Normal reconciliation logic + // ... +} +``` + +### Important Retry Behavior Notes + +- **Retry limits don't block new events**: When retry limits are reached, new reconciliations still occur for new events +- **No retry on limit reached**: If an error occurs after reaching the retry limit, no additional retries are scheduled until new events arrive +- **Event-driven recovery**: Fresh events can restart the retry cycle, allowing recovery from previously failed states + +A successful execution resets the retry state. + +### Reconciler Error Handler + +In order to facilitate error reporting you can override [`updateErrorStatus`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java#L52) +method in `Reconciler`: + +```java +public class MyReconciler implements Reconciler { + + @Override + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context context, Exception e) { + return handleError(resource, e); + } + +} +``` + +The `updateErrorStatus` method is called in case an exception is thrown from the `Reconciler`. It is +also called even if no retry policy is configured, just after the reconciler execution. +`RetryInfo.getAttemptCount()` is zero after the first reconciliation attempt, since it is not a +result of a retry (regardless of whether a retry policy is configured). + +`ErrorStatusUpdateControl` tells the SDK what to do and how to perform the status +update on the primary resource, which is always performed as a status sub-resource request. Note that +this update request will also produce an event and result in a reconciliation if the +controller is not generation-aware. + +This feature is only available for the `reconcile` method of the `Reconciler` interface, since +there should not be updates to resources that have been marked for deletion. + +Retry can be skipped in cases of unrecoverable errors: + +```java + ErrorStatusUpdateControl.patchStatus(customResource).withNoRetry(); +``` + +### Correctness and Automatic Retries + +While it is possible to deactivate automatic retries, this is not desirable unless there is a particular reason. +Errors naturally occur, whether it be transient network errors or conflicts +when a given resource is handled by a `Reconciler` but modified simultaneously by a user in +a different process. Automatic retries handle these cases nicely and will eventually result in a +successful reconciliation. + +## Retry, Rescheduling and Event Handling Common Behavior + +Retry, reschedule, and standard event processing form a relatively complex system, each of these +functionalities interacting with the others. In the following, we describe the interplay of +these features: + +1. A successful execution resets a retry and the rescheduled executions that were present before + the reconciliation. However, the reconciliation outcome can instruct a new rescheduling (`UpdateControl` or `DeleteControl`). + + For example, if a reconciliation had previously been rescheduled for after some amount of time, but an event triggered + the reconciliation (or cleanup) in the meantime, the scheduled execution would be automatically cancelled, i.e. + rescheduling a reconciliation does not guarantee that one will occur precisely at that time; it simply guarantees that a reconciliation will occur at the latest. + Of course, it's always possible to reschedule a new reconciliation at the end of that "automatic" reconciliation. + + Similarly, if a retry was scheduled, any event from the cluster triggering a successful execution in the meantime + would cancel the scheduled retry (because there's now no point in retrying something that already succeeded) + +2. In case an exception is thrown, a retry is initiated. However, if an event is received + meanwhile, it will be reconciled instantly, and this execution won't count as a retry attempt. +3. If the retry limit is reached (so no more automatic retry would happen), but a new event + received, the reconciliation will still happen, but won't reset the retry, and will still be + marked as the last attempt in the retry info. The point (1) still holds - thus successful reconciliation will reset the retry - but no retry will happen in case of an error. + +The thing to remember when it comes to retrying or rescheduling is that JOSDK tries to avoid unnecessary work. When +you reschedule an operation, you instruct JOSDK to perform that operation by the end of the rescheduling +delay at the latest. If something occurred on the cluster that triggers that particular operation (reconciliation or cleanup), then +JOSDK considers that there's no point in attempting that operation again at the end of the specified delay since there +is no point in doing so anymore. The same idea also applies to retries. diff --git a/docs/content/en/docs/documentation/eventing.md b/docs/content/en/docs/documentation/eventing.md new file mode 100644 index 0000000000..77daeb6fa3 --- /dev/null +++ b/docs/content/en/docs/documentation/eventing.md @@ -0,0 +1,327 @@ +--- +title: Event sources and related topics +weight: 47 +--- + +## Handling Related Events with Event Sources + +See also +this [blog post](https://csviri.medium.com/java-operator-sdk-introduction-to-event-sources-a1aab5af4b7b) +. + +Event sources are a relatively simple yet powerful and extensible concept to trigger controller +executions, usually based on changes to dependent resources. You typically need an event source +when you want your `Reconciler` to be triggered when something occurs to secondary resources +that might affect the state of your primary resource. This is needed because a given +`Reconciler` will only listen by default to events affecting the primary resource type it is +configured for. Event sources act as listen to events affecting these secondary resources so +that a reconciliation of the associated primary resource can be triggered when needed. Note that +these secondary resources need not be Kubernetes resources. Typically, when dealing with +non-Kubernetes objects or services, we can extend our operator to handle webhooks or websockets +or to react to any event coming from a service we interact with. This allows for very efficient +controller implementations because reconciliations are then only triggered when something occurs +on resources affecting our primary resources thus doing away with the need to periodically +reschedule reconciliations. + +![Event Sources architecture diagram](/images/event-sources.png) + +There are few interesting points here: + +The `CustomResourceEventSource` event source is a special one, responsible for handling events +pertaining to changes affecting our primary resources. This `EventSource` is always registered +for every controller automatically by the SDK. It is important to note that events always relate +to a given primary resource. Concurrency is still handled for you, even in the presence of +`EventSource` implementations, and the SDK still guarantees that there is no concurrent execution of +the controller for any given primary resource (though, of course, concurrent/parallel executions +of events pertaining to other primary resources still occur as expected). + +### Caching and Event Sources + +Kubernetes resources are handled in a declarative manner. The same also holds true for event +sources. For example, if we define an event source to watch for changes of a Kubernetes Deployment +object using an `InformerEventSource`, we always receive the whole associated object from the +Kubernetes API. This object might be needed at any point during our reconciliation process and +it's best to retrieve it from the event source directly when possible instead of fetching it +from the Kubernetes API since the event source guarantees that it will provide the latest +version. Not only that, but many event source implementations also cache resources they handle +so that it's possible to retrieve the latest version of resources without needing to make any +calls to the Kubernetes API, thus allowing for very efficient controller implementations. + +Note after an operator starts, caches are already populated by the time the first reconciliation +is processed for the `InformerEventSource` implementation. However, this does not necessarily +hold true for all event source implementations (`PerResourceEventSource` for example). The SDK +provides methods to handle this situation elegantly, allowing you to check if an object is +cached, retrieving it from a provided supplier if not. See +related [method](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java#L146) +. + +### Registering Event Sources + +To register event sources, your `Reconciler` has to override the `prepareEventSources` and return +list of event sources to register. One way to see this in action is +to look at the +[WebPage example](https://github.com/operator-framework/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java) +(irrelevant details omitted): + +```java + +import java.util.List; + +@ControllerConfiguration +public class WebappReconciler + implements Reconciler, Cleaner, EventSourceInitializer { + // ommitted code + + @Override + public List> prepareEventSources(EventSourceContext context) { + InformerEventSourceConfiguration configuration = + InformerEventSourceConfiguration.from(Deployment.class, Webapp.class) + .withLabelSelector(SELECTOR) + .build(); + return List.of(new InformerEventSource<>(configuration, context)); + } +} +``` + +In the example above an `InformerEventSource` is configured and registered. +`InformerEventSource` is one of the bundled `EventSource` implementations that JOSDK provides to +cover common use cases. + +### Managing Relation between Primary and Secondary Resources + +Event sources let your operator know when a secondary resource has changed and that your +operator might need to reconcile this new information. However, in order to do so, the SDK needs +to somehow retrieve the primary resource associated with which ever secondary resource triggered +the event. In the `Webapp` example above, when an event occurs on a tracked `Deployment`, the +SDK needs to be able to identify which `Webapp` resource is impacted by that change. + +Seasoned Kubernetes users already know one way to track this parent-child kind of relationship: +using owner references. Indeed, that's how the SDK deals with this situation by default as well, +that is, if your controller properly set owner references on your secondary resources, the SDK +will be able to follow that reference back to your primary resource automatically without you +having to worry about it. + +However, owner references cannot always be used as they are restricted to operating within a +single namespace (i.e. you cannot have an owner reference to a resource in a different namespace) +and are, by essence, limited to Kubernetes resources so you're out of luck if your secondary +resources live outside of a cluster. + +This is why JOSDK provides the `SecondaryToPrimaryMapper` interface so that you can provide +alternative ways for the SDK to identify which primary resource needs to be reconciled when +something occurs to your secondary resources. We even provide some of these alternatives in the +[Mappers](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java) +class. + +Note that, while a set of `ResourceID` is returned, this set usually consists only of one +element. It is however possible to return multiple values or even no value at all to cover some +rare corner cases. Returning an empty set means that the mapper considered the secondary +resource event as irrelevant and the SDK will thus not trigger a reconciliation of the primary +resource in that situation. + +Adding a `SecondaryToPrimaryMapper` is typically sufficient when there is a one-to-many relationship +between primary and secondary resources. The secondary resources can be mapped to its primary +owner, and this is enough information to also get these secondary resources from the `Context` +object that's passed to your `Reconciler`. + +There are however cases when this isn't sufficient and you need to provide an explicit mapping +between a primary resource and its associated secondary resources using an implementation of the +`PrimaryToSecondaryMapper` interface. This is typically needed when there are many-to-one or +many-to-many relationships between primary and secondary resources, e.g. when the primary resource +is referencing secondary resources. +See [PrimaryToSecondaryIT](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java) +integration test for a sample. + +### Built-in EventSources + +There are multiple event-sources provided out of the box, the following are some more central ones: + +#### `InformerEventSource` + +[InformerEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java) +is probably the most important `EventSource` implementation to know about. When you create an +`InformerEventSource`, JOSDK will automatically create and register a `SharedIndexInformer`, a +fabric8 Kubernetes client class, that will listen for events associated with the resource type +you configured your `InformerEventSource` with. If you want to listen to Kubernetes resource +events, `InformerEventSource` is probably the only thing you need to use. It's highly +configurable so you can tune it to your needs. Take a look at +[InformerEventSourceConfiguration](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java) +and associated classes for more details but some interesting features we can mention here is the +ability to filter events so that you can only get notified for events you care about. A +particularly interesting feature of the `InformerEventSource`, as opposed to using your own +informer-based listening mechanism is that caches are particularly well optimized preventing +reconciliations from being triggered when not needed and allowing efficient operators to be written. + +#### `PerResourcePollingEventSource` + +[PerResourcePollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java) +is used to poll external APIs, which don't support webhooks or other event notifications. It +extends the abstract +[ExternalResourceCachingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java) +to support caching. +See [MySQL Schema sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java) +for usage. + +#### `PollingEventSource` + +[PollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java) +is similar to `PerResourceCachingEventSource` except that, contrary to that event source, it +doesn't poll a specific API separately per resource, but periodically and independently of +actually observed primary resources. + +#### Inbound event sources + +[SimpleInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java) +and +[CachingInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java) +are used to handle incoming events from webhooks and messaging systems. + +#### `ControllerResourceEventSource` + +[ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java) +is a special `EventSource` implementation that you will never have to deal with directly. It is, +however, at the core of the SDK is automatically added for you: this is the main event source +that listens for changes to your primary resources and triggers your `Reconciler` when needed. +It features smart caching and is really optimized to minimize Kubernetes API accesses and avoid +triggering unduly your `Reconciler`. + +More on the philosophy of the non Kubernetes API related event source see in +issue [#729](https://github.com/java-operator-sdk/java-operator-sdk/issues/729). + + +## InformerEventSource Multi-Cluster Support + +It is possible to handle resources for remote cluster with `InformerEventSource`. To do so, +simply set a client that connects to a remote cluster: + +```java + +InformerEventSourceConfiguration configuration = + InformerEventSourceConfiguration.from(SecondaryResource.class, PrimaryResource.class) + .withKubernetesClient(remoteClusterClient) + .withSecondaryToPrimaryMapper(Mappers.fromDefaultAnnotations()); + +``` + +You will also need to specify a `SecondaryToPrimaryMapper`, since the default one +is based on owner references and won't work across cluster instances. You could, for example, use the provided implementation that relies on annotations added to the secondary resources to identify the associated primary resource. + +See related [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster). + + +## Generation Awareness and Event Filtering + +A best practice when an operator starts up is to reconcile all the associated resources because +changes might have occurred to the resources while the operator was not running. + +When this first reconciliation is done successfully, the next reconciliation is triggered if either +dependent resources are changed or the primary resource `.spec` field is changed. If other fields +like `.metadata` are changed on the primary resource, the reconciliation could be skipped. This +behavior is supported out of the box and reconciliation is by default not triggered if +changes to the primary resource do not increase the `.metadata.generation` field. +Note that changes to `.metada.generation` are automatically handled by Kubernetes. + +To turn off this feature, set `generationAwareEventProcessing` to `false` for the `Reconciler`. + + +## Max Interval Between Reconciliations + +When informers / event sources are properly set up, and the `Reconciler` implementation is +correct, no additional reconciliation triggers should be needed. However, it's +a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966) +to have a failsafe periodic trigger in place, just to make sure resources are nevertheless +reconciled after a certain amount of time. This functionality is in place by default, with a +rather high time interval (currently 10 hours) after which a reconciliation will be +automatically triggered even in the absence of other events. See how to override this using the +standard annotation: + +```java +@ControllerConfiguration(maxReconciliationInterval = @MaxReconciliationInterval( + interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) +public class MyReconciler implements Reconciler {} +``` + +The event is not propagated at a fixed rate, rather it's scheduled after each reconciliation. So the +next reconciliation will occur at most within the specified interval after the last reconciliation. + +This feature can be turned off by setting `maxReconciliationInterval` +to [`Constants.NO_MAX_RECONCILIATION_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L20-L20) +or any non-positive number. + +The automatic retries are not affected by this feature so a reconciliation will be re-triggered +on error, according to the specified retry policy, regardless of this maximum interval setting. + +## Rate Limiting + +It is possible to rate limit reconciliation on a per-resource basis. The rate limit also takes +precedence over retry/re-schedule configurations: for example, even if a retry was scheduled for +the next second but this request would make the resource go over its rate limit, the next +reconciliation will be postponed according to the rate limiting rules. Note that the +reconciliation is never cancelled, it will just be executed as early as possible based on rate +limitations. + +Rate limiting is by default turned **off**, since correct configuration depends on the reconciler +implementation, in particular, on how long a typical reconciliation takes. +(The parallelism of reconciliation itself can be +limited [`ConfigurationService`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L120-L120) +by configuring the `ExecutorService` appropriately.) + +A default rate limiter implementation is provided, see: +[`PeriodRateLimiter`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiter.java#L14-L14) +. +Users can override it by implementing their own +[`RateLimiter`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimiter.java) +and specifying this custom implementation using the `rateLimiter` field of the +`@ControllerConfiguration` annotation. Similarly to the `Retry` implementations, +`RateLimiter` implementations must provide an accessible, no-arg constructor for instantiation +purposes and can further be automatically configured from your own, provided annotation provided +your `RateLimiter` implementation also implements the `AnnotationConfigurable` interface, +parameterized by your custom annotation type. + +To configure the default rate limiter use the `@RateLimited` annotation on your +`Reconciler` class. The following configuration limits each resource to reconcile at most twice +within a 3 second interval: + +```java + +@RateLimited(maxReconciliations = 2, within = 3, unit = TimeUnit.SECONDS) +@ControllerConfiguration +public class MyReconciler implements Reconciler { + +} +``` + +Thus, if a given resource was reconciled twice in one second, no further reconciliation for this +resource will happen before two seconds have elapsed. Note that, since rate is limited on a +per-resource basis, other resources can still be reconciled at the same time, as long, of course, +that they stay within their own rate limits. + +## Optimizing Caches + +One of the ideas around the operator pattern is that all the relevant resources are cached, thus reconciliation is +usually very fast (especially if no resources are updated in the process) since the operator is then mostly working with +in-memory state. However for large clusters, caching huge amount of primary and secondary resources might consume lots +of memory. JOSDK provides ways to mitigate this issue and optimize the memory usage of controllers. While these features +are working and tested, we need feedback from real production usage. + +### Bounded Caches for Informers + +Limiting caches for informers - thus for Kubernetes resources - is supported by ensuring that resources are in the cache +for a limited time, via a cache eviction of least recently used resources. This means that when resources are created +and frequently reconciled, they stay "hot" in the cache. However, if, over time, a given resource "cools" down, i.e. it +becomes less and less used to the point that it might not be reconciled anymore, it will eventually get evicted from the +cache to free up memory. If such an evicted resource were to become reconciled again, the bounded cache implementation +would then fetch it from the API server and the "hot/cold" cycle would start anew. + +Since all resources need to be reconciled when a controller start, it is not practical to set a maximal cache size as +it's desirable that all resources be cached as soon as possible to make the initial reconciliation process on start as +fast and efficient as possible, avoiding undue load on the API server. It's therefore more interesting to gradually +evict cold resources than try to limit cache sizes. + +See usage of the related implementation using [Caffeine](https://github.com/ben-manes/caffeine) cache in integration +tests +for [primary resources](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/AbstractTestReconciler.java). + +See +also [CaffeineBoundedItemStores](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java) +for more details. \ No newline at end of file diff --git a/docs/content/en/docs/documentation/features.md b/docs/content/en/docs/documentation/features.md new file mode 100644 index 0000000000..8c8909c8b2 --- /dev/null +++ b/docs/content/en/docs/documentation/features.md @@ -0,0 +1,55 @@ +--- +title: Other Features +weight: 57 +--- + +The Java Operator SDK (JOSDK) is a high-level framework and tooling suite for implementing Kubernetes operators. By default, features follow best practices in an opinionated way. However, configuration options and feature flags are available to fine-tune or disable these features. + +## Support for Well-Known Kubernetes Resources + +Controllers can be registered for standard Kubernetes resources (not just custom resources), such as `Ingress`, `Deployment`, and others. + +See the [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment) for an example of reconciling deployments. + +```java +public class DeploymentReconciler + implements Reconciler, TestExecutionInfoProvider { + + @Override + public UpdateControl reconcile( + Deployment resource, Context context) { + // omitted code + } +} +``` + +## Leader Election + +Operators are typically deployed with a single active instance. However, you can deploy multiple instances where only one (the "leader") processes events. This is achieved through "leader election." + +While all instances run and start their event sources to populate caches, only the leader processes events. If the leader crashes, other instances are already warmed up and ready to take over when a new leader is elected. + +See sample configuration in the [E2E test](https://github.com/java-operator-sdk/java-operator-sdk/blob/8865302ac0346ee31f2d7b348997ec2913d5922b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java#L21-L23). + +## Automatic CRD Generation + +**Note:** This feature is provided by the [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client), not JOSDK itself. + +To automatically generate CRD manifests from your annotated Custom Resource classes, add this dependency to your project: + +```xml + + + io.fabric8 + crd-generator-apt + provided + +``` + +The CRD will be generated in `target/classes/META-INF/fabric8` (or `target/test-classes/META-INF/fabric8` for test scope) with the CRD name suffixed by the generated spec version. + +For example, a CR using the `java-operator-sdk.io` group with a `mycrs` plural form will result in these files: +- `mycrs.java-operator-sdk.io-v1.yml` +- `mycrs.java-operator-sdk.io-v1beta1.yml` + +**Note for Quarkus users:** If you're using the `quarkus-operator-sdk` extension, you don't need to add any extra dependency for CRD generation - the extension handles this automatically. diff --git a/docs/content/en/docs/documentation/observability.md b/docs/content/en/docs/documentation/observability.md new file mode 100644 index 0000000000..27a68086d5 --- /dev/null +++ b/docs/content/en/docs/documentation/observability.md @@ -0,0 +1,112 @@ +--- +title: Observability +weight: 55 +--- + +## Runtime Info + +[RuntimeInfo](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java#L16-L16) +is used mainly to check the actual health of event sources. Based on this information it is easy to implement custom +liveness probes. + +[stopOnInformerErrorDuringStartup](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) +setting, where this flag usually needs to be set to false, in order to control the exact liveness properties. + +See also an example implementation in the +[WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43) + +## Contextual Info for Logging with MDC + +Logging is enhanced with additional contextual information using +[MDC](http://www.slf4j.org/manual.html#mdc). The following attributes are available in most +parts of reconciliation logic and during the execution of the controller: + +| MDC Key | Value added from primary resource | +|:---------------------------|:----------------------------------| +| `resource.apiVersion` | `.apiVersion` | +| `resource.kind` | `.kind` | +| `resource.name` | `.metadata.name` | +| `resource.namespace` | `.metadata.namespace` | +| `resource.resourceVersion` | `.metadata.resourceVersion` | +| `resource.generation` | `.metadata.generation` | +| `resource.uid` | `.metadata.uid` | + +For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback). + +## Metrics + +JOSDK provides built-in support for metrics reporting on what is happening with your reconcilers in the form of +the `Metrics` interface which can be implemented to connect to your metrics provider of choice, JOSDK calling the +methods as it goes about reconciling resources. By default, a no-operation implementation is provided thus providing a +no-cost sane default. A [micrometer](https://micrometer.io)-based implementation is also provided. + +You can use a different implementation by overriding the default one provided by the default `ConfigurationService`, as +follows: + +```java +Metrics metrics; // initialize your metrics implementation +Operator operator = new Operator(client, o -> o.withMetrics(metrics)); +``` + +### Micrometer implementation + +The micrometer implementation is typically created using one of the provided factory methods which, depending on which +is used, will return either a ready to use instance or a builder allowing users to customized how the implementation +behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect +metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but +this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which +could lead to performance issues. + +To create a `MicrometerMetrics` implementation that behaves how it has historically behaved, you can just create an +instance via: + +```java +MeterRegistry registry; // initialize your registry implementation +Metrics metrics = new MicrometerMetrics(registry); +``` + +Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either +return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance +will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource +basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed. +See the relevant classes documentation for more details. + +For example, the following will create a `MicrometerMetrics` instance configured to collect metrics on a per-resource +basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so. + +```java +MicrometerMetrics.newPerResourceCollectingMicrometerMetricsBuilder(registry) + .withCleanUpDelayInSeconds(5) + .withCleaningThreadNumber(2) + .build(); +``` + +### Operator SDK metrics + +The micrometer implementation records the following metrics: + +| Meter name | Type | Tag names | Description | +|-------------------------------------------------------------|----------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| +| operator.sdk.reconciliations.executions.`` | gauge | group, version, kind | Number of executions of the named reconciler | +| operator.sdk.reconciliations.queue.size.`` | gauge | group, version, kind | How many resources are queued to get reconciled by named reconciler | +| operator.sdk.``.size | gauge map size | | Gauge tracking the size of a specified map (currently unused but could be used to monitor caches size) | +| operator.sdk.events.received | counter | ``, event, action | Number of received Kubernetes events | +| operator.sdk.events.delete | counter | `` | Number of received Kubernetes delete events | +| operator.sdk.reconciliations.started | counter | ``, reconciliations.retries.last, reconciliations.retries.number | Number of started reconciliations per resource type | +| operator.sdk.reconciliations.failed | counter | ``, exception | Number of failed reconciliations per resource type | +| operator.sdk.reconciliations.success | counter | `` | Number of successful reconciliations per resource type | +| operator.sdk.controllers.execution.reconcile | timer | ``, controller | Time taken for reconciliations per controller | +| operator.sdk.controllers.execution.cleanup | timer | ``, controller | Time taken for cleanups per controller | +| operator.sdk.controllers.execution.reconcile.success | counter | controller, type | Number of successful reconciliations per controller | +| operator.sdk.controllers.execution.reconcile.failure | counter | controller, exception | Number of failed reconciliations per controller | +| operator.sdk.controllers.execution.cleanup.success | counter | controller, type | Number of successful cleanups per controller | +| operator.sdk.controllers.execution.cleanup.failure | counter | controller, exception | Number of failed cleanups per controller | + +As you can see all the recorded metrics start with the `operator.sdk` prefix. ``, in the table above, +refers to resource-specific metadata and depends on the considered metric and how the implementation is configured and +could be summed up as follows: `group?, version, kind, [name, namespace?], scope` where the tags in square +brackets (`[]`) won't be present when per-resource collection is disabled and tags followed by a question mark are +omitted if the associated value is empty. Of note, when in the context of controllers' execution metrics, these tag +names are prefixed with `resource.`. This prefix might be removed in a future version for greater consistency. + + diff --git a/docs/content/en/docs/documentation/reconciler.md b/docs/content/en/docs/documentation/reconciler.md new file mode 100644 index 0000000000..3ea09cf167 --- /dev/null +++ b/docs/content/en/docs/documentation/reconciler.md @@ -0,0 +1,212 @@ +--- +title: Implementing a reconciler +weight: 45 +--- + +## How Reconciliation Works + +The reconciliation process is event-driven and follows this flow: + +1. **Event Reception**: Events trigger reconciliation from: + - **Primary resources** (usually custom resources) when created, updated, or deleted + - **Secondary resources** through registered event sources + +2. **Reconciliation Execution**: Each reconciler handles a specific resource type and listens for events from the Kubernetes API server. When an event arrives, it triggers reconciliation unless one is already running for that resource. The framework ensures no concurrent reconciliation occurs for the same resource. + +3. **Post-Reconciliation Processing**: After reconciliation completes, the framework: + - Schedules a retry if an exception was thrown + - Schedules new reconciliation if events were received during execution + - Schedules a timer event if rescheduling was requested (`UpdateControl.rescheduleAfter(..)`) + - Finishes reconciliation if none of the above apply + +The SDK core implements an event-driven system where events trigger reconciliation requests. + +## Implementing Reconciler and Cleaner Interfaces + +To implement a reconciler, you must implement the [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) interface. + +A Kubernetes resource lifecycle has two phases depending on whether the resource is marked for deletion: + +**Normal Phase**: The framework calls the `reconcile` method for regular resource operations. + +**Deletion Phase**: If the resource is marked for deletion and your `Reconciler` implements the [`Cleaner`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) interface, only the `cleanup` method is called. The framework automatically handles finalizers for you. + +If you need explicit cleanup logic, always use finalizers. See [Finalizer support](#finalizer-support) for details. + +### Using `UpdateControl` and `DeleteControl` + +These classes control the behavior after reconciliation completes. + +[`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) can instruct the framework to: +- Update the status sub-resource +- Reschedule reconciliation with a time delay + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + // omitted code + + return UpdateControl.patchStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +without an update: + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + // omitted code + + return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +Note, though, that using `EventSources` is the preferred way of scheduling since the +reconciliation is triggered only when a resource is changed, not on a timely basis. + +At the end of the reconciliation, you typically update the status sub-resources. +It is also possible to update both the status and the resource with the `patchResourceAndStatus` method. In this case, +the resource is updated first followed by the status, using two separate requests to the Kubernetes API. + +From v5 `UpdateControl` only supports patching the resources, by default +using [Server Side Apply (SSA)](https://kubernetes.io/docs/reference/using-api/server-side-apply/). +It is important to understand how SSA works in Kubernetes. Mainly, resources applied using SSA +should contain only the fields identifying the resource and those the user is interested in (a 'fully specified intent' +in Kubernetes parlance), thus usually using a resource created from scratch, see +[sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa). +To contrast, see the same sample, this time [without SSA](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java). + +Non-SSA based patch is still supported. +You can control whether or not to use SSA +using [`ConfigurationServcice.useSSAToPatchPrimaryResource()`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L385-L385) +and the related `ConfigurationServiceOverrider.withUseSSAToPatchPrimaryResource` method. +Related integration test can be +found [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa). + +Handling resources directly using the client, instead of delegating these updates operations to JOSDK by returning +an `UpdateControl` at the end of your reconciliation, should work appropriately. However, we do recommend to +use `UpdateControl` instead since JOSDK makes sure that the operations are handled properly, since there are subtleties +to be aware of. For example, if you are using a finalizer, JOSDK makes sure to include it in your fully specified intent +so that it is not unintentionally removed from the resource (which would happen if you omit it, since your controller is +the designated manager for that field and Kubernetes interprets the finalizer being gone from the specified intent as a +request for removal). + +[`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) +typically instructs the framework to remove the finalizer after the dependent +resource are cleaned up in `cleanup` implementation. + +```java + +public DeleteControl cleanup(MyCustomResource customResource,Context context){ + // omitted code + + return DeleteControl.defaultDelete(); + } + +``` + +However, it is possible to instruct the SDK to not remove the finalizer, this allows to clean up +the resources in a more asynchronous way, mostly for cases when there is a long waiting period +after a delete operation is initiated. Note that in this case you might want to either schedule +a timed event to make sure `cleanup` is executed again or use event sources to get notified +about the state changes of the deleted resource. + +### Finalizer Support + +[Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) +make sure that your `Reconciler` gets a chance to act before a resource is actually deleted +after it's been marked for deletion. Without finalizers, the resource would be deleted directly +by the Kubernetes server. + +Depending on your use case, you might or might not need to use finalizers. In particular, if +your operator doesn't need to clean any state that would not be automatically managed by the +Kubernetes cluster (e.g. external resources), you might not need to use finalizers. You should +use the +Kubernetes [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#owners-dependents) +mechanism as much as possible by setting owner references for your secondary resources so that +the cluster can automatically delete them for you whenever the associated primary resource is +deleted. Note that setting owner references is the responsibility of the `Reconciler` +implementation, though [dependent resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) +make that process easier. + +If you do need to clean such a state, you need to use finalizers so that their +presence will prevent the Kubernetes server from deleting the resource before your operator is +ready to allow it. This allows for clean-up even if your operator was down when the resource was marked for deletion. + +JOSDK makes cleaning resources in this fashion easier by taking care of managing finalizers +automatically for you when needed. The only thing you need to do is let the SDK know that your +operator is interested in cleaning the state associated with your primary resources by having it +implement +the [`Cleaner

`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) +interface. If your `Reconciler` doesn't implement the `Cleaner` interface, the SDK will consider +that you don't need to perform any clean-up when resources are deleted and will, therefore, not activate finalizer support. +In other words, finalizer support is added only if your `Reconciler` implements the `Cleaner` interface. + +The framework automatically adds finalizers as the first step, thus after a resource +is created but before the first reconciliation. The finalizer is added via a separate +Kubernetes API call. As a result of this update, the finalizer will then be present on the +resource. The reconciliation can then proceed as normal. + +The automatically added finalizer will also be removed after the `cleanup` is executed on +the reconciler. This behavior is customizable as explained +[above](#using-updatecontrol-and-deletecontrol) when we addressed the use of +`DeleteControl`. + +You can specify the name of the finalizer to use for your `Reconciler` using the +[`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) +annotation. If you do not specify a finalizer name, one will be automatically generated for you. + +From v5, by default, the finalizer is added using Server Side Apply. See also `UpdateControl` in docs. + +### Making sure the primary resource is up to date for the next reconciliation + +It is typical to want to update the status subresource with the information that is available during the reconciliation. +This is sometimes referred to as the last observed state. When the primary resource is updated, though, the framework +does not cache the resource directly, relying instead on the propagation of the update to the underlying informer's +cache. It can, therefore, happen that, if other events trigger other reconciliations, before the informer cache gets +updated, your reconciler does not see the latest version of the primary resource. While this might not typically be a +problem in most cases, as caches eventually become consistent, depending on your reconciliation logic, you might still +require the latest status version possible, for example, if the status subresource is used to store allocated values. +See [Representing Allocated Values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values) +from the Kubernetes docs for more details. + +The framework provides the +[`PrimaryUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java) utility class +to help with these use cases. + +This class' methods use internal caches in combination with update methods that leveraging +optimistic locking. If the update method fails on optimistic locking, it will retry +using a fresh resource from the server as base for modification. + +```java +@Override +public UpdateControl reconcile( + StatusPatchCacheCustomResource resource, Context context) { + + // omitted logic + + // update with SSA requires a fresh copy + var freshCopy = createFreshCopy(primary); + freshCopy.getStatus().setValue(statusWithState()); + + var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context); + + // the resource was updated transparently via the utils, no further action is required via UpdateControl in this case + return UpdateControl.noUpdate(); + } +``` + +After the update `PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource` puts the result of the update into an internal +cache and the framework will make sure that the next reconciliation contains the most recent version of the resource. +Note that it is not necessarily the same version returned as response from the update, it can be a newer version since other parties +can do additional updates meanwhile. However, unless it has been explicitly modified, that +resource will contain the up-to-date status. + +Note that you can also perform additional updates after the `PrimaryUpdateAndCacheUtils.*PatchStatusAndCacheResource` is +called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or via `UpdateControl`. Using +`PrimaryUpdateAndCacheUtils` guarantees that the next reconciliation will see a resource state no older than the version +updated via `PrimaryUpdateAndCacheUtils`. + +See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache). diff --git a/docs/content/en/docs/documentation/working-with-es-caches.md b/docs/content/en/docs/documentation/working-with-es-caches.md new file mode 100644 index 0000000000..bb1e140303 --- /dev/null +++ b/docs/content/en/docs/documentation/working-with-es-caches.md @@ -0,0 +1,218 @@ +--- +title: Working with EventSource caches +weight: 48 +--- + +As described in [Event sources and related topics](eventing.md), event sources serve as the backbone +for caching resources and triggering reconciliation for primary resources that are related +to these secondary resources. + +In the Kubernetes ecosystem, the component responsible for this is called an Informer. Without delving into +the details (there are plenty of excellent resources online about informers), informers +watch resources, cache them, and emit events when resources change. + +`EventSource` is a generalized concept that extends the Informer pattern to non-Kubernetes resources, +allowing you to cache external resources and trigger reconciliation when those resources change. + +## The InformerEventSource + +The underlying informer implementation comes from the Fabric8 client, called [DefaultSharedIndexInformer](https://github.com/fabric8io/kubernetes-client/blob/main/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/informers/impl/DefaultSharedIndexInformer.java). +[InformerEventSource](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java) +in Java Operator SDK wraps the Fabric8 client informers. +While this wrapper adds additional capabilities specifically required for controllers, this is the event +source that most likely will be used to deal with Kubernetes resources. + +These additional capabilities include: +- Maintaining an index that maps secondary resources in the informer cache to their related primary resources +- Setting up multiple informers for the same resource type when needed (for example, you need one informer per namespace if the informer is not watching the entire cluster) +- Dynamically adding and removing watched namespaces +- Other capabilities that are beyond the scope of this document + +### Associating Secondary Resources to Primary Resource + +Event sources need to trigger the appropriate reconciler, providing the correct primary resource, whenever one of their +handled secondary resources changes. It is thus core to an event source's role to identify which primary resource +(usually, your custom resource) is potentially impacted by that change. +The framework uses [`SecondaryToPrimaryMapper`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java) +for this purpose. For `InformerEventSources`, which target Kubernetes resources, this mapping is typically done using +either the owner reference or an annotation on the secondary resource. For external resources, other mechanisms need to +be used and there are also cases where the default mechanisms provided by the SDK do not work, even for Kubernetes +resources. + +However, once the event source has triggered a primary resource reconciliation, the associated reconciler needs to +access the secondary resources which changes caused the reconciliation. Indeed, the information from the secondary +resources might be needed during the reconciliation. For that purpose, +`InformerEventSource` maintains a reverse +index [PrimaryToSecondaryIndex](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/DefaultPrimaryToSecondaryIndex.java), +based on the result of the `SecondaryToPrimaryMapper`result. + +## Unified API for Related Resources + +To access all related resources for a primary resource, the framework provides an API to access the related +secondary resources using the `Set getSecondaryResources(Class expectedType)` method of the `Context` object +provided as part of the `reconcile` method. + +For `InformerEventSource`, this will leverage the associated `PrimaryToSecondaryIndex`. Resources are then retrieved +from the informer's cache. Note that since all those steps work on top of indexes, those operations are very fast, +usually O(1). + +While we've focused mostly on `InformerEventSource`, this concept can be extended to all `EventSources`, since +[`EventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java#L93) +actually implements the `Set getSecondaryResources(P primary)` method that can be called from the `Context`. + +As there can be multiple event sources for the same resource types, things are a little more complex: the union of each +event source results is returned. + +## Getting Resources Directly from Event Sources + +Note that nothing prevents you from directly accessing resources in the cache without going through +`getSecondaryResources(...)`: + +```java +public class WebPageReconciler implements Reconciler { + + InformerEventSource configMapEventSource; + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + // accessing resource directly from an event source + var mySecondaryResource = configMapEventSource.get(new ResourceID("name","namespace")); + // details omitted + } + + @Override + public List> prepareEventSources(EventSourceContext context) { + configMapEventSource = new InformerEventSource<>( + InformerEventSourceConfiguration.from(ConfigMap.class, WebPage.class) + .withLabelSelector(SELECTOR) + .build(), + context); + + return List.of(configMapEventSource); + } +} +``` + +## The Use Case for PrimaryToSecondaryMapper + +**TL;DR**: `PrimaryToSecondaryMapper` allows `InformerEventSource` to access secondary resources directly +instead of using the `PrimaryToSecondaryIndex`. When this mapper is configured, `InformerEventSource.getSecondaryResources(..)` +will call the mapper to retrieve the target secondary resources. This is typically required when the `SecondaryToPrimaryMapper` +uses informer caches to list the target resources. + +As discussed, we provide a unified API to access related resources using `Context.getSecondaryResources(...)`. +The term "Secondary" refers to resources that a reconciler needs to consider when properly reconciling a primary +resource. These resources encompass more than just "child" resources (resources created by a reconciler that +typically have an owner reference pointing to the primary custom resource). They also include +"related" resources (which may or may not be managed by Kubernetes) that serve as input for reconciliations. + +In some cases, the SDK needs additional information beyond what's readily available, particularly when +secondary resources lack owner references or any direct link to their associated primary resource. + +Consider this example: a `Job` primary resource can be assigned to run on a cluster, represented by a +`Cluster` resource. +Multiple jobs can run on the same cluster, so multiple `Job` resources can reference the same `Cluster` resource. However, +a `Cluster` resource shouldn't know about `Job` resources, as this information isn't part of what defines a cluster. +When a cluster changes, though, we might want to redirect associated jobs to other clusters. Our reconciler +therefore needs to determine which `Job` (primary) resources are associated with the changed `Cluster` (secondary) +resource. +See full +sample [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary). + +```java +InformerEventSourceConfiguration + .from(Cluster.class, Job.class) + .withSecondaryToPrimaryMapper(cluster -> + context.getPrimaryCache() + .list() + .filter(job -> job.getSpec().getClusterName().equals(cluster.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet())) +``` + +This configuration will trigger all related `Jobs` when the associated cluster changes and maintains the `PrimaryToSecondaryIndex`, +allowing us to use `getSecondaryResources` in the `Job` reconciler to access the cluster. +However, there's a potential issue: when a new `Job` is created, it doesn't automatically propagate +to the `PrimaryToSecondaryIndex` in the `Cluster`'s `InformerEventSource`. Re-indexing only occurs +when a `Cluster` event is received, which triggers all related `Jobs` again. +Until this re-indexing happens, you cannot use `getSecondaryResources` for the new `Job`, since it +won't be present in the reverse index. + +You can work around this by accessing the Cluster directly from the cache in the reconciler: + +```java + +@Override +public UpdateControl reconcile(Job resource, Context context) { + + clusterInformer.get(new ResourceID(job.getSpec().getClusterName(), job.getMetadata().getNamespace())); + + // omitted details +} +``` + +However, if you prefer to use the unified API (`context.getSecondaryResources()`), you need to add +a `PrimaryToSecondaryMapper`: + +```java +clusterInformer.withPrimaryToSecondaryMapper( job -> + Set.of(new ResourceID(job.getSpec().getClusterName(), job.getMetadata().getNamespace()))); +``` + +When using `PrimaryToSecondaryMapper`, the InformerEventSource bypasses the `PrimaryToSecondaryIndex` +and instead calls the mapper to retrieve resources based on its results. +In fact, when this mapper is configured, the `PrimaryToSecondaryIndex` isn't even initialized. + +### Using Informer Indexes to Improve Performance + +In the `SecondaryToPrimaryMapper` example above, we iterate through all resources in the cache: + +```java +context.getPrimaryCache().list().filter(job -> job.getSpec().getClusterName().equals(cluster.getMetadata().getName())) +``` + +This approach can be inefficient when dealing with a large number of primary (`Job`) resources. To improve performance, +you can create an index in the underlying Informer that indexes the target jobs for each cluster: + +```java + +@Override +public List> prepareEventSources(EventSourceContext context) { + + context.getPrimaryCache() + .addIndexer(JOB_CLUSTER_INDEX, + (job -> List.of(indexKey(job.getSpec().getClusterName(), job.getMetadata().getNamespace())))); + + // omitted details +} +``` + +where `indexKey` is a String that uniquely identifies a Cluster: + +```java +private String indexKey(String clusterName, String namespace) { + return clusterName + "#" + namespace; +} +``` + +With this index in place, you can retrieve the target resources very efficiently: + +```java + + InformerEventSource clusterInformer = + new InformerEventSource( + InformerEventSourceConfiguration.from(Cluster.class, Job.class) + .withSecondaryToPrimaryMapper( + cluster -> + context + .getPrimaryCache() + .byIndex( + JOB_CLUSTER_INDEX, + indexKey( + cluster.getMetadata().getName(), + cluster.getMetadata().getNamespace())) + .stream() + .map(ResourceID::fromResource) + .collect(Collectors.toSet())) + .withNamespacesInheritedFromController().build(), context); +``` diff --git a/docs/content/en/docs/faq/_index.md b/docs/content/en/docs/faq/_index.md index 5e4975a385..977a725b0d 100644 --- a/docs/content/en/docs/faq/_index.md +++ b/docs/content/en/docs/faq/_index.md @@ -1,100 +1,147 @@ --- title: FAQ -weight: 80 +weight: 90 --- -### Q: How can I access the events which triggered the Reconciliation? +## Events and Reconciliation -In the v1.* version events were exposed to `Reconciler` (which was called `ResourceController` -then). This included events (Create, Update) of the custom resource, but also events produced by -Event Sources. After long discussions also with developers of golang version (controller-runtime), -we decided to remove access to these events. We already advocated to not use events in the -reconciliation logic, since events can be lost. Instead, reconcile all the resources on every -execution of reconciliation. On first this might sound a little opinionated, but there is a -sound agreement between the developers that this is the way to go. +### How can I access the events that triggered reconciliation? -Note that this is also consistent with Kubernetes -[level based](https://cloud.redhat.com/blog/kubernetes-operators-best-practices) reconciliation approach. +In v1.* versions, events were exposed to `Reconciler` (then called `ResourceController`). This included custom resource events (Create, Update) and events from Event Sources. After extensive discussions with golang controller-runtime developers, we decided to remove event access. -### Q: Can I re-schedule a reconciliation, possibly with a specific delay? +**Why this change was made:** +- Events can be lost in distributed systems +- Best practice is to reconcile all resources on every execution +- Aligns with Kubernetes [level-based](https://cloud.redhat.com/blog/kubernetes-operators-best-practices) reconciliation approach -Yes, this can be done -using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) -and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) -, see: +**Recommendation**: Always reconcile all resources instead of relying on specific events. +### Can I reschedule a reconciliation with a specific delay? + +Yes, you can reschedule reconciliation using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java). + +**With status update:** ```java - @Override - public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { - ... - return UpdateControl.patchStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); - } +@Override +public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + // ... reconciliation logic + return UpdateControl.patchStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); +} ``` -without an update: - +**Without an update:** ```java - @Override - public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { - ... - return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); - } +@Override +public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + // ... reconciliation logic + return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); +} ``` -Although you might consider using `EventSources`, to handle reconciliation triggering in a smarter -way. +**Note**: Consider using `EventSources` for smarter reconciliation triggering instead of time-based scheduling. + +### How can I make status updates trigger reconciliation? -### Q: How can I run an operator without cluster scope rights? +By default, the framework filters out events that don't increase the `generation` field of the primary resource's metadata. Since `generation` typically only increases when the `.spec` field changes, status-only changes won't trigger reconciliation. + +To change this behavior, set [`generationAwareEventProcessing`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L43) to `false`: + +```java +@ControllerConfiguration(generationAwareEventProcessing = false) +static class TestCustomReconciler implements Reconciler { + @Override + public UpdateControl reconcile( + TestCustomResource resource, Context context) { + // reconciliation logic + } +} +``` -By default, JOSDK requires access to CRs at cluster scope. You may not be granted such -rights and you will see some error at startup that looks like: +For secondary resources, every change should trigger reconciliation by default, except when you add explicit filters or use dependent resource implementations that filter out self-triggered changes. See [related docs](../documentation/dependent-resource-and-workflows/dependent-resources.md#caching-and-event-handling-in-kubernetesdependentresource). + +## Permissions and Access Control + +### How can I run an operator without cluster-scope rights? + +By default, JOSDK requires cluster-scope access to custom resources. Without these rights, you'll see startup errors like: ```plain io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: GET at: https://kubernetes.local.svc/apis/mygroup/v1alpha1/mycr. Message: Forbidden! Configured service account doesn't have access. Service account may have been revoked. mycrs.mygroup is forbidden: User "system:serviceaccount:ns:sa" cannot list resource "mycrs" in API group "mygroup" at the cluster scope. ``` -To restrict the operator to a set of namespaces, you may override which namespaces are watched by a reconciler -at [Reconciler-level configuration](../configuration.md#reconciler-level-configuration): +**Solution 1: Restrict to specific namespaces** + +Override watched namespaces using [Reconciler-level configuration](../configuration.md#reconciler-level-configuration): ```java Operator operator; Reconciler reconciler; -... +// ... operator.register(reconciler, configOverrider -> configOverrider.settingNamespace("mynamespace")); ``` -Note that configuring the watched namespaces can also be done using the `@ControllerConfiguration` annotation. -Furthermore, you may not be able to list CRDs at startup which is required when `checkingCRDAndValidateLocalModel` -is `true` (`false` by default). To disable, set it to `false` at [Operator-level configuration](../configuration#operator-level-configuration): +**Note**: You can also configure watched namespaces using the `@ControllerConfiguration` annotation. + +**Solution 2: Disable CRD validation** + +If you can't list CRDs at startup (required when `checkingCRDAndValidateLocalModel` is `true`), disable it using [Operator-level configuration](../configuration#operator-level-configuration): ```java -Operator operator = new Operator( override -> override.checkingCRDAndValidateLocalModel(false)); +Operator operator = new Operator(override -> override.checkingCRDAndValidateLocalModel(false)); ``` -### Q: I'm managing an external resource that has a generated ID, where should I store that? +## State Management + +### Where should I store generated IDs for external resources? -It is common that a non-Kubernetes or external resource is managed from a controller. Those external resources might -have a generated ID, so are not simply addressable based on the spec of a custom resources. Therefore, the -generated ID needs to be stored somewhere in order to address the resource during the subsequent reconciliations. +When managing external (non-Kubernetes) resources, they often have generated IDs that aren't simply addressable based on your custom resource spec. You need to store these IDs for subsequent reconciliations. -Usually there are two options you can consider to store the ID: +**Storage Options:** +1. **Separate resource** (usually ConfigMap, Secret, or dedicated CustomResource) +2. **Custom resource status field** -1. Create a separate resource (usually ConfigMap, Secret or dedicated CustomResource) where you store the ID. -2. Store the ID in the status of the custom resource. +**Important considerations:** -Note that both approaches are a bit tricky, since you have to guarantee the resources are cached for the next -reconciliation. For example if you patch the status at the end of the reconciliation (`UpdateControl.patchStatus(...)`) -it is not guaranteed that during the next reconciliation you will see the fresh resource. Therefore, controllers -which do this, usually cache the updated status in memory to make sure it is present for next reconciliation. +Both approaches require guaranteeing resources are cached for the next reconciliation. If you patch status at the end of reconciliation (`UpdateControl.patchStatus(...)`), the fresh resource isn't guaranteed to be available during the next reconciliation. Controllers typically cache updated status in memory to ensure availability. -Dependent Resources feature supports the [first approach](../dependent-resources/_index.md#external-state-tracking-dependent-resources). +**Modern solution**: From version 5.1, use [this utility](../documentation/reconciler.md#making-sure-the-primary-resource-is-up-to-date-for-the-next-reconciliation) to ensure updated status is available for the next reconciliation. -### Q: How to fix `sun.security.provider.certpath.SunCertPathBuilderException` on Rancher Desktop and k3d/k3s Kubernetes +**Dependent Resources**: This feature supports [the first approach](../documentation/dependent-resource-and-workflows/dependent-resources.md#external-state-tracking-dependent-resources) natively. -It's a common issue when using k3d and the fabric8 client tries to connect to the cluster an exception is thrown: +## Advanced Use Cases + +### How can I skip the reconciliation of a dependent resource? + +Skipping workflow reconciliation altogether is possible with the explicit invocation feature since v5. You can read more about this in [v5 release notes](https://javaoperatorsdk.io/blog/2025/01/06/version-5-released/#explicit-workflow-invocation). + +However, what if you want to avoid reconciling a single dependent resource based on some state? First, remember that the dependent resource won't be modified if the desired state and actual state match. Moreover, it's generally good practice to reconcile all your resources, with JOSDK taking care of only processing resources whose state doesn't match the desired one. + +However, in some corner cases (for example, if it's expensive to compute the desired state or compare it to the actual state), it's sometimes useful to skip the reconciliation of some resources but not all, if it's known that they don't need processing based on the status of the custom resource. + +A common mistake is to use `ReconcilePrecondition`. If the condition doesn't hold, it will delete the resources. This is by design (although the name might be misleading), but not what we want in this case. + +The correct approach is to override the matcher in the dependent resource: + +```java +public Result match(R actualResource, R desired, P primary, Context

context) { + if (alreadyIsCertainState(primary.getStatus())) { + return true; + } else { + return super.match(actual, desired, primary, context); + } +} +``` + +This ensures the dependent resource isn't updated if the primary resource is in a certain state. + +## Troubleshooting + +### How to fix SSL certificate issues with Rancher Desktop and k3d/k3s + +This is a common issue when using k3d and the fabric8 client tries to connect to the cluster: ``` Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target @@ -103,12 +150,13 @@ Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.s at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:295) ``` -The cause is that fabric8 kubernetes client does not handle elliptical curve encryption by default. To fix this, add -the following dependency on the classpath: +**Cause**: The fabric8 kubernetes client doesn't handle elliptical curve encryption by default. + +**Solution**: Add the following dependency to your classpath: ```xml org.bouncycastle bcpkix-jdk15on -``` \ No newline at end of file +``` diff --git a/docs/content/en/docs/features/_index.md b/docs/content/en/docs/features/_index.md deleted file mode 100644 index de49abe2b5..0000000000 --- a/docs/content/en/docs/features/_index.md +++ /dev/null @@ -1,853 +0,0 @@ ---- -title: Features -weight: 50 ---- - -# Features - -The Java Operator SDK (JOSDK) is a high level framework and related tooling aimed at -facilitating the implementation of Kubernetes operators. The features are by default following -the best practices in an opinionated way. However, feature flags and other configuration options -are provided to fine tune or turn off these features. - -## Reconciliation Execution in a Nutshell - -Reconciliation execution is always triggered by an event. Events typically come from a -primary resource, most of the time a custom resource, triggered by changes made to that resource -on the server (e.g. a resource is created, updated or deleted). Reconciler implementations are -associated with a given resource type and listens for such events from the Kubernetes API server -so that they can appropriately react to them. It is, however, possible for secondary sources to -trigger the reconciliation process. This usually occurs via -the [event source](#handling-related-events-with-event-sources) mechanism. - -When an event is received reconciliation is executed, unless a reconciliation is already -underway for this particular resource. In other words, the framework guarantees that no -concurrent reconciliation happens for any given resource. - -Once the reconciliation is done, the framework checks if: - -- an exception was thrown during execution and if yes schedules a retry. -- new events were received during the controller execution, if yes schedule a new reconciliation. -- the reconcilier instructed the SDK to re-schedule a reconciliation at a later date, if yes - schedules a timer event with the specified delay. -- none of the above, the reconciliation is finished. - -In summary, the core of the SDK is implemented as an eventing system, where events trigger -reconciliation requests. - -## Implementing a [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) and/or [`Cleaner`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) - -The lifecycle of a Kubernetes resource can be clearly separated into two phases from the -perspective of an operator depending on whether a resource is created or updated, or on the -other hand if it is marked for deletion. - -This separation-related logic is automatically handled by the framework. The framework will always -call the `reconcile` method, unless the custom resource is -[marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) -. On the other, if the resource is marked from deletion and if the `Reconciler` implements the -`Cleaner` interface, only the `cleanup` method will be called. Implementing the `Cleaner` -interface allows developers to let the SDK know that they are interested in cleaning related -state (e.g. out-of-cluster resources). The SDK will therefore automatically add a finalizer -associated with your `Reconciler` so that the Kubernetes server doesn't delete your resources -before your `Reconciler` gets a chance to clean things up. -See [Finalizer support](#finalizer-support) for more details. - -### Using `UpdateControl` and `DeleteControl` - -These two classes are used to control the outcome or the desired behaviour after the reconciliation. - -The [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) -can instruct the framework to update the status sub-resource of the resource -and/or re-schedule a reconciliation with a desired time delay: - -```java - @Override - public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { - // omitted code - - return UpdateControl.patchStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); - } -``` - -without an update: - -```java - @Override - public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { - // omitted code - - return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); - } -``` - -Note, though, that using `EventSources` should be preferred to rescheduling since the -reconciliation will then be triggered only when needed instead than on a timely basis. - -Those are the typical use cases of resource updates, however in some cases there it can happen that -the controller wants to update the resource itself (for example to add annotations) or not perform -any updates, which is also supported. - -It is also possible to update both the status and the resource with the `patchResourceAndStatus` method. In this case, -the resource is updated first followed by the status, using two separate requests to the Kubernetes API. - -From v5 `UpdateControl` only supports patching the resources, by default -using [Server Side Apply (SSA)](https://kubernetes.io/docs/reference/using-api/server-side-apply/). -It is important to understand how SSA works in Kubernetes. Mainly, resources applied using SSA -should contain only the fields identifying the resource and those the user is interested in (a 'fully specified intent' -in Kubernetes parlance), thus usually using a resource created from scratch, see -[sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa). -To contrast, see the same sample, this time [without SSA](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java). - -Non-SSA based patch is still supported. -You can control whether or not to use SSA -using [`ConfigurationServcice.useSSAToPatchPrimaryResource()`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L385-L385) -and the related `ConfigurationServiceOverrider.withUseSSAToPatchPrimaryResource` method. -Related integration test can be -found [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa). - -Handling resources directly using the client, instead of delegating these updates operations to JOSDK by returning -an `UpdateControl` at the end of your reconciliation, should work appropriately. However, we do recommend to -use `UpdateControl` instead since JOSDK makes sure that the operations are handled properly, since there are subtleties -to be aware of. For example, if you are using a finalizer, JOSDK makes sure to include it in your fully specified intent -so that it is not unintentionally removed from the resource (which would happen if you omit it, since your controller is -the designated manager for that field and Kubernetes interprets the finalizer being gone from the specified intent as a -request for removal). - -[`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) -typically instructs the framework to remove the finalizer after the dependent -resource are cleaned up in `cleanup` implementation. - -```java - -public DeleteControl cleanup(MyCustomResource customResource,Context context){ - // omitted code - - return DeleteControl.defaultDelete(); - } - -``` - -However, it is possible to instruct the SDK to not remove the finalizer, this allows to clean up -the resources in a more asynchronous way, mostly for cases when there is a long waiting period -after a delete operation is initiated. Note that in this case you might want to either schedule -a timed event to make sure `cleanup` is executed again or use event sources to get notified -about the state changes of the deleted resource. - -### Finalizer Support - -[Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) -make sure that your `Reconciler` gets a chance to act before a resource is actually deleted -after it's been marked for deletion. Without finalizers, the resource would be deleted directly -by the Kubernetes server. - -Depending on your use case, you might or might not need to use finalizers. In particular, if -your operator doesn't need to clean any state that would not be automatically managed by the -Kubernetes cluster (e.g. external resources), you might not need to use finalizers. You should -use the -Kubernetes [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#owners-dependents) -mechanism as much as possible by setting owner references for your secondary resources so that -the cluster can automatically deleted them for you whenever the associated primary resource is -deleted. Note that setting owner references is the responsibility of the `Reconciler` -implementation, though [dependent resources](https://javaoperatorsdk.io/docs/dependent-resources) -make that process easier. - -If you do need to clean such state, you need to use finalizers so that their -presence will prevent the Kubernetes server from deleting the resource before your operator is -ready to allow it. This allows for clean up to still occur even if your operator was down when -the resources was "deleted" by a user. - -JOSDK makes cleaning resources in this fashion easier by taking care of managing finalizers -automatically for you when needed. The only thing you need to do is let the SDK know that your -operator is interested in cleaning state associated with your primary resources by having it -implement -the [`Cleaner

`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) -interface. If your `Reconciler` doesn't implement the `Cleaner` interface, the SDK will consider -that you don't need to perform any clean-up when resources are deleted and will therefore not -activate finalizer support. In other words, finalizer support is added only if your `Reconciler` -implements the `Cleaner` interface. - -Finalizers are automatically added by the framework as the first step, thus after a resource -is created, but before the first reconciliation. The finalizer is added via a separate -Kubernetes API call. As a result of this update, the finalizer will then be present on the -resource. The reconciliation can then proceed as normal. - -The finalizer that is automatically added will be also removed after the `cleanup` is executed on -the reconciler. This behavior is customizable as explained -[above](#using-updatecontrol-and-deletecontrol) when we addressed the use of -`DeleteControl`. - -You can specify the name of the finalizer to use for your `Reconciler` using the -[`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) -annotation. If you do not specify a finalizer name, one will be automatically generated for you. - -From v5 by default finalizer is added using Served Side Apply. See also UpdateControl in docs. - -## Generation Awareness and Event Filtering - -A best practice when an operator starts up is to reconcile all the associated resources because -changes might have occurred to the resources while the operator was not running. - -When this first reconciliation is done successfully, the next reconciliation is triggered if either -dependent resources are changed or the primary resource `.spec` field is changed. If other fields -like `.metadata` are changed on the primary resource, the reconciliation could be skipped. This -behavior is supported out of the box and reconciliation is by default not triggered if -changes to the primary resource do not increase the `.metadata.generation` field. -Note that changes to `.metada.generation` are automatically handled by Kubernetes. - -To turn off this feature, set `generationAwareEventProcessing` to `false` for the `Reconciler`. - -## Support for Well Known (non-custom) Kubernetes Resources - -A Controller can be registered for a non-custom resource, so well known Kubernetes resources like ( -`Ingress`, `Deployment`,...). - -See -the [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment) -for reconciling deployments. - -```java -public class DeploymentReconciler - implements Reconciler, TestExecutionInfoProvider { - - @Override - public UpdateControl reconcile( - Deployment resource, Context context) { - // omitted code - } -} -``` - -## Max Interval Between Reconciliations - -When informers / event sources are properly set up, and the `Reconciler` implementation is -correct, no additional reconciliation triggers should be needed. However, it's -a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966) -to have a failsafe periodic trigger in place, just to make sure resources are nevertheless -reconciled after a certain amount of time. This functionality is in place by default, with a -rather high time interval (currently 10 hours) after which a reconciliation will be -automatically triggered even in the absence of other events. See how to override this using the -standard annotation: - -```java -@ControllerConfiguration(maxReconciliationInterval = @MaxReconciliationInterval( - interval = 50, - timeUnit = TimeUnit.MILLISECONDS)) -public class MyReconciler implements Reconciler {} -``` - -The event is not propagated at a fixed rate, rather it's scheduled after each reconciliation. So the -next reconciliation will occur at most within the specified interval after the last reconciliation. - -This feature can be turned off by setting `maxReconciliationInterval` -to [`Constants.NO_MAX_RECONCILIATION_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L20-L20) -or any non-positive number. - -The automatic retries are not affected by this feature so a reconciliation will be re-triggered -on error, according to the specified retry policy, regardless of this maximum interval setting. - -## Automatic Retries on Error - -JOSDK will schedule an automatic retry of the reconciliation whenever an exception is thrown by -your `Reconciler`. The retry is behavior is configurable but a default implementation is provided -covering most of the typical use-cases, see -[GenericRetry](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java) -. - -```java - GenericRetry.defaultLimitedExponentialRetry() - .setInitialInterval(5000) - .setIntervalMultiplier(1.5D) - .setMaxAttempts(5); -``` - -You can also configure the default retry behavior using the `@GradualRetry` annotation. - -It is possible to provide a custom implementation using the `retry` field of the -`@ControllerConfiguration` annotation and specifying the class of your custom implementation. -Note that this class will need to provide an accessible no-arg constructor for automated -instantiation. Additionally, your implementation can be automatically configured from an -annotation that you can provide by having your `Retry` implementation implement the -`AnnotationConfigurable` interface, parameterized with your annotation type. See the -`GenericRetry` implementation for more details. - -Information about the current retry state is accessible from -the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) -object. Of note, particularly interesting is the `isLastAttempt` method, which could allow your -`Reconciler` to implement a different behavior based on this status, by setting an error message -in your resource' status, for example, when attempting a last retry. - -Note, though, that reaching the retry limit won't prevent new events to be processed. New -reconciliations will happen for new events as usual. However, if an error also occurs that -would normally trigger a retry, the SDK won't schedule one at this point since the retry limit -is already reached. - -A successful execution resets the retry state. - -### Setting Error Status After Last Retry Attempt - -In order to facilitate error reporting, `Reconciler` can implement the -[ErrorStatusHandler](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java) -interface: - -```java -public interface ErrorStatusHandler

{ - - ErrorStatusUpdateControl

updateErrorStatus(P resource, Context

context, Exception e); - -} -``` - -The `updateErrorStatus` method is called in case an exception is thrown from the `Reconciler`. It is -also called even if no retry policy is configured, just after the reconciler execution. -`RetryInfo.getAttemptCount()` is zero after the first reconciliation attempt, since it is not a -result of a retry (regardless of whether a retry policy is configured or not). - -`ErrorStatusUpdateControl` is used to tell the SDK what to do and how to perform the status -update on the primary resource, always performed as a status sub-resource request. Note that -this update request will also produce an event, and will result in a reconciliation if the -controller is not generation aware. - -This feature is only available for the `reconcile` method of the `Reconciler` interface, since -there should not be updates to resource that have been marked for deletion. - -Retry can be skipped in cases of unrecoverable errors: - -```java - ErrorStatusUpdateControl.patchStatus(customResource).withNoRetry(); -``` - -### Correctness and Automatic Retries - -While it is possible to deactivate automatic retries, this is not desirable, unless for very -specific reasons. Errors naturally occur, whether it be transient network errors or conflicts -when a given resource is handled by a `Reconciler` but is modified at the same time by a user in -a different process. Automatic retries handle these cases nicely and will usually result in a -successful reconciliation. - -## Retry and Rescheduling and Event Handling Common Behavior - -Retry, reschedule and standard event processing form a relatively complex system, each of these -functionalities interacting with the others. In the following, we describe the interplay of -these features: - -1. A successful execution resets a retry and the rescheduled executions which were present before - the reconciliation. However, a new rescheduling can be instructed from the reconciliation - outcome (`UpdateControl` or `DeleteControl`). - - For example, if a reconciliation had previously been re-scheduled after some amount of time, but an event triggered - the reconciliation (or cleanup) in the mean time, the scheduled execution would be automatically cancelled, i.e. - re-scheduling a reconciliation does not guarantee that one will occur exactly at that time, it simply guarantees that - one reconciliation will occur at that time at the latest, triggering one if no event from the cluster triggered one. - Of course, it's always possible to re-schedule a new reconciliation at the end of that "automatic" reconciliation. - - Similarly, if a retry was scheduled, any event from the cluster triggering a successful execution in the mean time - would cancel the scheduled retry (because there's now no point in retrying something that already succeeded) - -2. In case an exception happened, a retry is initiated. However, if an event is received - meanwhile, it will be reconciled instantly, and this execution won't count as a retry attempt. -3. If the retry limit is reached (so no more automatic retry would happen), but a new event - received, the reconciliation will still happen, but won't reset the retry, and will still be - marked as the last attempt in the retry info. The point (1) still holds, but in case of an - error, no retry will happen. - -The thing to keep in mind when it comes to retrying or rescheduling is that JOSDK tries to avoid unnecessary work. When -you reschedule an operation, you instruct JOSDK to perform that operation at the latest by the end of the rescheduling -delay. If something occurred on the cluster that triggers that particular operation (reconciliation or cleanup), then -JOSDK considers that there's no point in attempting that operation again at the end of the specified delay since there -is now no point to do so anymore. The same idea also applies to retries. - -## Rate Limiting - -It is possible to rate limit reconciliation on a per-resource basis. The rate limit also takes -precedence over retry/re-schedule configurations: for example, even if a retry was scheduled for -the next second but this request would make the resource go over its rate limit, the next -reconciliation will be postponed according to the rate limiting rules. Note that the -reconciliation is never cancelled, it will just be executed as early as possible based on rate -limitations. - -Rate limiting is by default turned **off**, since correct configuration depends on the reconciler -implementation, in particular, on how long a typical reconciliation takes. -(The parallelism of reconciliation itself can be -limited [`ConfigurationService`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L120-L120) -by configuring the `ExecutorService` appropriately.) - -A default rate limiter implementation is provided, see: -[`PeriodRateLimiter`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/PeriodRateLimiter.java#L14-L14) -. -Users can override it by implementing their own -[`RateLimiter`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ce4d996ee073ebef5715737995fc3d33f4751275/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/rate/RateLimiter.java) -and specifying this custom implementation using the `rateLimiter` field of the -`@ControllerConfiguration` annotation. Similarly to the `Retry` implementations, -`RateLimiter` implementations must provide an accessible, no-arg constructor for instantiation -purposes and can further be automatically configured from your own, provided annotation provided -your `RateLimiter` implementation also implements the `AnnotationConfigurable` interface, -parameterized by your custom annotation type. - -To configure the default rate limiter use the `@RateLimited` annotation on your -`Reconciler` class. The following configuration limits each resource to reconcile at most twice -within a 3 second interval: - -```java - -@RateLimited(maxReconciliations = 2, within = 3, unit = TimeUnit.SECONDS) -@ControllerConfiguration -public class MyReconciler implements Reconciler { - -} -``` - -Thus, if a given resource was reconciled twice in one second, no further reconciliation for this -resource will happen before two seconds have elapsed. Note that, since rate is limited on a -per-resource basis, other resources can still be reconciled at the same time, as long, of course, -that they stay within their own rate limits. - -## Handling Related Events with Event Sources - -See also -this [blog post](https://csviri.medium.com/java-operator-sdk-introduction-to-event-sources-a1aab5af4b7b) -. - -Event sources are a relatively simple yet powerful and extensible concept to trigger controller -executions, usually based on changes to dependent resources. You typically need an event source -when you want your `Reconciler` to be triggered when something occurs to secondary resources -that might affect the state of your primary resource. This is needed because a given -`Reconciler` will only listen by default to events affecting the primary resource type it is -configured for. Event sources act as listen to events affecting these secondary resources so -that a reconciliation of the associated primary resource can be triggered when needed. Note that -these secondary resources need not be Kubernetes resources. Typically, when dealing with -non-Kubernetes objects or services, we can extend our operator to handle webhooks or websockets -or to react to any event coming from a service we interact with. This allows for very efficient -controller implementations because reconciliations are then only triggered when something occurs -on resources affecting our primary resources thus doing away with the need to periodically -reschedule reconciliations. - -![Event Sources architecture diagram](../assets/images/event-sources.png) - -There are few interesting points here: - -The `CustomResourceEventSource` event source is a special one, responsible for handling events -pertaining to changes affecting our primary resources. This `EventSource` is always registered -for every controller automatically by the SDK. It is important to note that events always relate -to a given primary resource. Concurrency is still handled for you, even in the presence of -`EventSource` implementations, and the SDK still guarantees that there is no concurrent execution of -the controller for any given primary resource (though, of course, concurrent/parallel executions -of events pertaining to other primary resources still occur as expected). - -### Caching and Event Sources - -Kubernetes resources are handled in a declarative manner. The same also holds true for event -sources. For example, if we define an event source to watch for changes of a Kubernetes Deployment -object using an `InformerEventSource`, we always receive the whole associated object from the -Kubernetes API. This object might be needed at any point during our reconciliation process and -it's best to retrieve it from the event source directly when possible instead of fetching it -from the Kubernetes API since the event source guarantees that it will provide the latest -version. Not only that, but many event source implementations also cache resources they handle -so that it's possible to retrieve the latest version of resources without needing to make any -calls to the Kubernetes API, thus allowing for very efficient controller implementations. - -Note after an operator starts, caches are already populated by the time the first reconciliation -is processed for the `InformerEventSource` implementation. However, this does not necessarily -hold true for all event source implementations (`PerResourceEventSource` for example). The SDK -provides methods to handle this situation elegantly, allowing you to check if an object is -cached, retrieving it from a provided supplier if not. See -related [method](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java#L146) -. - -### Registering Event Sources - -To register event sources, your `Reconciler` has to override the `prepareEventSources` and return -list of event sources to register. One way to see this in action is -to look at the -[tomcat example](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java) -(irrelevant details omitted): - -```java - -@ControllerConfiguration -public class WebappReconciler - implements Reconciler, Cleaner, EventSourceInitializer { - // ommitted code - - @Override - public Map prepareEventSources(EventSourceContext context) { - InformerEventSourceConfiguration configuration = - InformerEventSourceConfiguration.from(Tomcat.class, Tomcat.class) - .withSecondaryToPrimaryMapper(webappsMatchingTomcatName) - .withPrimaryToSecondaryMapper( - (Webapp primary) -> Set.of(new ResourceID(primary.getSpec().getTomcat(), - primary.getMetadata().getNamespace()))) - .build(); - return EventSourceInitializer - .nameEventSources(new InformerEventSource<>(configuration, context)); - } - -} -``` - -In the example above an `InformerEventSource` is configured and registered. -`InformerEventSource` is one of the bundled `EventSource` implementations that JOSDK provides to -cover common use cases. - -### Managing Relation between Primary and Secondary Resources - -Event sources let your operator know when a secondary resource has changed and that your -operator might need to reconcile this new information. However, in order to do so, the SDK needs -to somehow retrieve the primary resource associated with which ever secondary resource triggered -the event. In the `Tomcat` example above, when an event occurs on a tracked `Deployment`, the -SDK needs to be able to identify which `Tomcat` resource is impacted by that change. - -Seasoned Kubernetes users already know one way to track this parent-child kind of relationship: -using owner references. Indeed, that's how the SDK deals with this situation by default as well, -that is, if your controller properly set owner references on your secondary resources, the SDK -will be able to follow that reference back to your primary resource automatically without you -having to worry about it. - -However, owner references cannot always be used as they are restricted to operating within a -single namespace (i.e. you cannot have an owner reference to a resource in a different namespace) -and are, by essence, limited to Kubernetes resources so you're out of luck if your secondary -resources live outside of a cluster. - -This is why JOSDK provides the `SecondayToPrimaryMapper` interface so that you can provide -alternative ways for the SDK to identify which primary resource needs to be reconciled when -something occurs to your secondary resources. We even provide some of these alternatives in the -[Mappers](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java) -class. - -Note that, while a set of `ResourceID` is returned, this set usually consists only of one -element. It is however possible to return multiple values or even no value at all to cover some -rare corner cases. Returning an empty set means that the mapper considered the secondary -resource event as irrelevant and the SDK will thus not trigger a reconciliation of the primary -resource in that situation. - -Adding a `SecondaryToPrimaryMapper` is typically sufficient when there is a one-to-many relationship -between primary and secondary resources. The secondary resources can be mapped to its primary -owner, and this is enough information to also get these secondary resources from the `Context` -object that's passed to your `Reconciler`. - -There are however cases when this isn't sufficient and you need to provide an explicit mapping -between a primary resource and its associated secondary resources using an implementation of the -`PrimaryToSecondaryMapper` interface. This is typically needed when there are many-to-one or -many-to-many relationships between primary and secondary resources, e.g. when the primary resource -is referencing secondary resources. -See [PrimaryToSecondaryIT](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java) -integration test for a sample. - -### Built-in EventSources - -There are multiple event-sources provided out of the box, the following are some more central ones: - -#### `InformerEventSource` - -[InformerEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java) -is probably the most important `EventSource` implementation to know about. When you create an -`InformerEventSource`, JOSDK will automatically create and register a `SharedIndexInformer`, a -fabric8 Kubernetes client class, that will listen for events associated with the resource type -you configured your `InformerEventSource` with. If you want to listen to Kubernetes resource -events, `InformerEventSource` is probably the only thing you need to use. It's highly -configurable so you can tune it to your needs. Take a look at -[InformerEventSourceConfiguration](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java) -and associated classes for more details but some interesting features we can mention here is the -ability to filter events so that you can only get notified for events you care about. A -particularly interesting feature of the `InformerEventSource`, as opposed to using your own -informer-based listening mechanism is that caches are particularly well optimized preventing -reconciliations from being triggered when not needed and allowing efficient operators to be written. - -#### `PerResourcePollingEventSource` - -[PerResourcePollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java) -is used to poll external APIs, which don't support webhooks or other event notifications. It -extends the abstract -[ExternalResourceCachingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java) -to support caching. -See [MySQL Schema sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java) -for usage. - -#### `PollingEventSource` - -[PollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java) -is similar to `PerResourceCachingEventSource` except that, contrary to that event source, it -doesn't poll a specific API separately per resource, but periodically and independently of -actually observed primary resources. - -#### Inbound event sources - -[SimpleInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java) -and -[CachingInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java) -are used to handle incoming events from webhooks and messaging systems. - -#### `ControllerResourceEventSource` - -[ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java) -is a special `EventSource` implementation that you will never have to deal with directly. It is, -however, at the core of the SDK is automatically added for you: this is the main event source -that listens for changes to your primary resources and triggers your `Reconciler` when needed. -It features smart caching and is really optimized to minimize Kubernetes API accesses and avoid -triggering unduly your `Reconciler`. - -More on the philosophy of the non Kubernetes API related event source see in -issue [#729](https://github.com/java-operator-sdk/java-operator-sdk/issues/729). - -## Contextual Info for Logging with MDC - -Logging is enhanced with additional contextual information using -[MDC](http://www.slf4j.org/manual.html#mdc). The following attributes are available in most -parts of reconciliation logic and during the execution of the controller: - -| MDC Key | Value added from primary resource | -|:---------------------------|:----------------------------------| -| `resource.apiVersion` | `.apiVersion` | -| `resource.kind` | `.kind` | -| `resource.name` | `.metadata.name` | -| `resource.namespace` | `.metadata.namespace` | -| `resource.resourceVersion` | `.metadata.resourceVersion` | -| `resource.generation` | `.metadata.generation` | -| `resource.uid` | `.metadata.uid` | - -For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback). - -## InformerEventSource Multi-Cluster Support - -It is possible to handle resources for remote cluster with `InformerEventSource`. To do so, -simply set a client that connects to a remote cluster: - -```java - -InformerEventSourceConfiguration configuration = - InformerEventSourceConfiguration.from(SecondaryResource.class, PrimaryResource.class) - .withKubernetesClient(remoteClusterClient) - .withSecondaryToPrimaryMapper(Mappers.fromDefaultAnnotations()); - -``` - -You will also need to specify a `SecondaryToPrimaryMapper`, since the default one -is based on owner references and won't work across cluster instances. You could, for example, use the provided implementation that relies on annotations added to the secondary resources to identify the associated primary resource. - -See related [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster). - -## Dynamically Changing Target Namespaces - -A controller can be configured to watch a specific set of namespaces in addition of the -namespace in which it is currently deployed or the whole cluster. The framework supports -dynamically changing the list of these namespaces while the operator is running. -When a reconciler is registered, an instance of -[`RegisteredController`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java#L5) -is returned, providing access to the methods allowing users to change watched namespaces as the -operator is running. - -A typical scenario would probably involve extracting the list of target namespaces from a -`ConfigMap` or some other input but this part is out of the scope of the framework since this is -use-case specific. For example, reacting to changes to a `ConfigMap` would probably involve -registering an associated `Informer` and then calling the `changeNamespaces` method on -`RegisteredController`. - -```java - -public static void main(String[] args) { - KubernetesClient client = new DefaultKubernetesClient(); - Operator operator = new Operator(client); - RegisteredController registeredController = operator.register(new WebPageReconciler(client)); - operator.installShutdownHook(); - operator.start(); - - // call registeredController further while operator is running -} - -``` - -If watched namespaces change for a controller, it might be desirable to propagate these changes to -`InformerEventSources` associated with the controller. In order to express this, -`InformerEventSource` implementations interested in following such changes need to be -configured appropriately so that the `followControllerNamespaceChanges` method returns `true`: - -```java - -@ControllerConfiguration -public class MyReconciler implements Reconciler { - - @Override - public Map prepareEventSources( - EventSourceContext context) { - - InformerEventSource configMapES = - new InformerEventSource<>(InformerEventSourceConfiguration.from(ConfigMap.class, TestCustomResource.class) - .withNamespacesInheritedFromController(context) - .build(), context); - - return EventSourceUtils.nameEventSources(configMapES); - } - -} -``` - -As seen in the above code snippet, the informer will have the initial namespaces inherited from -controller, but also will adjust the target namespaces if it changes for the controller. - -See also -the [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace) -for this feature. - -## Leader Election - -Operators are generally deployed with a single running or active instance. However, it is -possible to deploy multiple instances in such a way that only one, called the "leader", processes the -events. This is achieved via a mechanism called "leader election". While all the instances are -running, and even start their event sources to populate the caches, only the leader will process -the events. This means that should the leader change for any reason, for example because it -crashed, the other instances are already warmed up and ready to pick up where the previous -leader left off should one of them become elected leader. - -See sample configuration in -the [E2E test](https://github.com/java-operator-sdk/java-operator-sdk/blob/8865302ac0346ee31f2d7b348997ec2913d5922b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java#L21-L23) -. - -## Runtime Info - -[RuntimeInfo](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java#L16-L16) -is used mainly to check the actual health of event sources. Based on this information it is easy to implement custom -liveness probes. - -[stopOnInformerErrorDuringStartup](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) -setting, where this flag usually needs to be set to false, in order to control the exact liveness properties. - -See also an example implementation in the -[WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43) - -## Automatic Generation of CRDs - -Note that this feature is provided by the -[Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client), not JOSDK itself. - -To automatically generate CRD manifests from your annotated Custom Resource classes, you only need -to add the following dependencies to your project: - -```xml - - - io.fabric8 - crd-generator-apt - provided - -``` - -The CRD will be generated in `target/classes/META-INF/fabric8` (or -in `target/test-classes/META-INF/fabric8`, if you use the `test` scope) with the CRD name -suffixed by the generated spec version. For example, a CR using the `java-operator-sdk.io` group -with a `mycrs` plural form will result in 2 files: - -- `mycrs.java-operator-sdk.io-v1.yml` -- `mycrs.java-operator-sdk.io-v1beta1.yml` - -**NOTE:** -> Quarkus users using the `quarkus-operator-sdk` extension do not need to add any extra dependency -> to get their CRD generated as this is handled by the extension itself. - -## Metrics - -JOSDK provides built-in support for metrics reporting on what is happening with your reconcilers in the form of -the `Metrics` interface which can be implemented to connect to your metrics provider of choice, JOSDK calling the -methods as it goes about reconciling resources. By default, a no-operation implementation is provided thus providing a -no-cost sane default. A [micrometer](https://micrometer.io)-based implementation is also provided. - -You can use a different implementation by overriding the default one provided by the default `ConfigurationService`, as -follows: - -```java -Metrics metrics; // initialize your metrics implementation -Operator operator = new Operator(client, o -> o.withMetrics(metrics)); -``` - -### Micrometer implementation - -The micrometer implementation is typically created using one of the provided factory methods which, depending on which -is used, will return either a ready to use instance or a builder allowing users to customized how the implementation -behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect -metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but -this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which -could lead to performance issues. - -To create a `MicrometerMetrics` implementation that behaves how it has historically behaved, you can just create an -instance via: - -```java -MeterRegistry registry; // initialize your registry implementation -Metrics metrics = new MicrometerMetrics(registry); -``` - -Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either -return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance -will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource -basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed. -See the relevant classes documentation for more details. - -For example, the following will create a `MicrometerMetrics` instance configured to collect metrics on a per-resource -basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so. - -```java -MicrometerMetrics.newPerResourceCollectingMicrometerMetricsBuilder(registry) - .withCleanUpDelayInSeconds(5) - .withCleaningThreadNumber(2) - .build(); -``` - -### Operator SDK metrics - -The micrometer implementation records the following metrics: - -| Meter name | Type | Tag names | Description | -|-------------------------------------------------------------|----------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| -| operator.sdk.reconciliations.executions.`` | gauge | group, version, kind | Number of executions of the named reconciler | -| operator.sdk.reconciliations.queue.size.`` | gauge | group, version, kind | How many resources are queued to get reconciled by named reconciler | -| operator.sdk.``.size | gauge map size | | Gauge tracking the size of a specified map (currently unused but could be used to monitor caches size) | -| operator.sdk.events.received | counter | ``, event, action | Number of received Kubernetes events | -| operator.sdk.events.delete | counter | `` | Number of received Kubernetes delete events | -| operator.sdk.reconciliations.started | counter | ``, reconciliations.retries.last, reconciliations.retries.number | Number of started reconciliations per resource type | -| operator.sdk.reconciliations.failed | counter | ``, exception | Number of failed reconciliations per resource type | -| operator.sdk.reconciliations.success | counter | `` | Number of successful reconciliations per resource type | -| operator.sdk.controllers.execution.reconcile | timer | ``, controller | Time taken for reconciliations per controller | -| operator.sdk.controllers.execution.cleanup | timer | ``, controller | Time taken for cleanups per controller | -| operator.sdk.controllers.execution.reconcile.success | counter | controller, type | Number of successful reconciliations per controller | -| operator.sdk.controllers.execution.reconcile.failure | counter | controller, exception | Number of failed reconciliations per controller | -| operator.sdk.controllers.execution.cleanup.success | counter | controller, type | Number of successful cleanups per controller | -| operator.sdk.controllers.execution.cleanup.failure | counter | controller, exception | Number of failed cleanups per controller | - -As you can see all the recorded metrics start with the `operator.sdk` prefix. ``, in the table above, -refers to resource-specific metadata and depends on the considered metric and how the implementation is configured and -could be summed up as follows: `group?, version, kind, [name, namespace?], scope` where the tags in square -brackets (`[]`) won't be present when per-resource collection is disabled and tags followed by a question mark are -omitted if the associated value is empty. Of note, when in the context of controllers' execution metrics, these tag -names are prefixed with `resource.`. This prefix might be removed in a future version for greater consistency. - -## Optimizing Caches - -One of the ideas around the operator pattern is that all the relevant resources are cached, thus reconciliation is -usually very fast (especially if no resources are updated in the process) since the operator is then mostly working with -in-memory state. However for large clusters, caching huge amount of primary and secondary resources might consume lots -of memory. JOSDK provides ways to mitigate this issue and optimize the memory usage of controllers. While these features -are working and tested, we need feedback from real production usage. - -### Bounded Caches for Informers - -Limiting caches for informers - thus for Kubernetes resources - is supported by ensuring that resources are in the cache -for a limited time, via a cache eviction of least recently used resources. This means that when resources are created -and frequently reconciled, they stay "hot" in the cache. However, if, over time, a given resource "cools" down, i.e. it -becomes less and less used to the point that it might not be reconciled anymore, it will eventually get evicted from the -cache to free up memory. If such an evicted resource were to become reconciled again, the bounded cache implementation -would then fetch it from the API server and the "hot/cold" cycle would start anew. - -Since all resources need to be reconciled when a controller start, it is not practical to set a maximal cache size as -it's desirable that all resources be cached as soon as possible to make the initial reconciliation process on start as -fast and efficient as possible, avoiding undue load on the API server. It's therefore more interesting to gradually -evict cold resources than try to limit cache sizes. - -See usage of the related implementation using [Caffeine](https://github.com/ben-manes/caffeine) cache in integration -tests -for [primary resources](https://github.com/java-operator-sdk/java-operator-sdk/blob/902c8a562dfd7f8993a52e03473a7ad4b00f378b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/AbstractTestReconciler.java#L29-L29). - -See -also [CaffeineBoundedItemStores](https://github.com/java-operator-sdk/java-operator-sdk/blob/902c8a562dfd7f8993a52e03473a7ad4b00f378b/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java) -for more details. - - diff --git a/docs/content/en/docs/getting-started/_index.md b/docs/content/en/docs/getting-started/_index.md index e3a3f95788..df8a4b77fe 100644 --- a/docs/content/en/docs/getting-started/_index.md +++ b/docs/content/en/docs/getting-started/_index.md @@ -1,58 +1,4 @@ --- -title: Getting Started - -weight: 20 ---- - -## Introduction & Resources on Operators - -Operators manage both cluster and non-cluster resources on behalf of Kubernetes. This Java -Operator SDK (JOSDK) aims at making it as easy as possible to write Kubernetes operators in Java -using an API that should feel natural to Java developers and without having to worry about many -low-level details that the SDK handles automatically. - -For an introduction on operators, please see this -[blog post](https://blog.container-solutions.com/kubernetes-operators-explained). -or [this talk](https://www.youtube.com/watch?v=CvftaV-xrB4) - -You can read about the common problems JOSDK is solving for you -[here](https://blog.container-solutions.com/a-deep-dive-into-the-java-operator-sdk). - -You can also refer to the -[Writing Kubernetes operators using JOSDK blog series](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk) -. - -## Generating Project Skeleton - -Project includes a maven plugin to generate a skeleton project: - -```shell -mvn io.javaoperatorsdk:bootstrapper:[version]:create -DprojectGroupId=org.acme -DprojectArtifactId=getting-started -``` - -## Getting Started - -The easiest way to get started with SDK is to start -[minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) and -execute one of our [examples](https://github.com/java-operator-sdk/java-operator-sdk/tree/main/sample-operators). -There is a dedicated page to describe how to [use the samples](/docs/using-samples). - -Here are the main steps to develop the code and deploy the operator to a Kubernetes cluster. -A more detailed and specific version can be found under `samples/mysql-schema/README.md`. - -1. Setup `kubectl` to work with your Kubernetes cluster of choice. -1. Apply Custom Resource Definition -1. Compile the whole project (framework + samples) using `mvn install` in the root directory -1. Run the main class of the sample you picked and check out the sample's README to see what it - does. When run locally the framework will use your Kubernetes client configuration (in `~/. - kube/config`) to establish a connection to the cluster. This is why it was important to set - up `kubectl` up front. -1. You can work in this local development mode to play with the code. -1. Build the Docker image and push it to the registry -1. Apply RBAC configuration -1. Apply deployment configuration -1. Verify if the operator is up and running. Don't run it locally anymore to avoid conflicts in - processing events from the cluster's API server. - - - +title: Getting started +weight: 10 +--- \ No newline at end of file diff --git a/docs/content/en/docs/getting-started/bootstrap-and-samples.md b/docs/content/en/docs/getting-started/bootstrap-and-samples.md new file mode 100644 index 0000000000..d0d94f860d --- /dev/null +++ b/docs/content/en/docs/getting-started/bootstrap-and-samples.md @@ -0,0 +1,95 @@ +--- +title: Bootstrapping and samples +weight: 20 +--- + +## Creating a New Operator Project + +### Using the Maven Plugin + +The simplest way to start a new operator project is using the provided Maven plugin, which generates a complete project skeleton: + +```shell +mvn io.javaoperatorsdk:bootstrapper:[version]:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=getting-started +``` + +This command creates a new Maven project with: +- A basic operator implementation +- Maven configuration with required dependencies +- Generated [CustomResourceDefinition](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) (CRD) + +### Building Your Project + +Build the generated project with Maven: +```shell +mvn clean install +``` + +The build process automatically generates the CustomResourceDefinition YAML file that you'll need to apply to your Kubernetes cluster. + +## Exploring Sample Operators + +The [sample-operators](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators) directory contains real-world examples demonstrating different JOSDK features and patterns: + +### Available Samples + +**[webpage](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators/webpage)** +- **Purpose**: Creates NGINX webservers from Custom Resources containing HTML code +- **Key Features**: Multiple implementation approaches using both low-level APIs and higher-level abstractions +- **Good for**: Understanding basic operator concepts and API usage patterns + +**[mysql-schema](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators/mysql-schema)** +- **Purpose**: Manages database schemas in MySQL instances +- **Key Features**: Demonstrates managing non-Kubernetes resources (external systems) +- **Good for**: Learning how to integrate with external services and manage state outside Kubernetes + +**[tomcat](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators/tomcat)** +- **Purpose**: Manages Tomcat instances and web applications +- **Key Features**: Multiple controllers managing related custom resources +- **Good for**: Understanding complex operators with multiple resource types and relationships + +## Running the Samples + +### Prerequisites + +The easiest way to try samples is using a local Kubernetes cluster: +- [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) +- [kind](https://kind.sigs.k8s.io/) +- [Docker Desktop with Kubernetes](https://docs.docker.com/desktop/kubernetes/) + +### Step-by-Step Instructions + +1. **Apply the CustomResourceDefinition**: + ```shell + kubectl apply -f target/classes/META-INF/fabric8/[resource-name]-v1.yml + ``` + +2. **Run the operator**: + ```shell + mvn exec:java -Dexec.mainClass="your.main.ClassName" + ``` + Or run your main class directly from your IDE. + +3. **Create custom resources**: + The operator will automatically detect and reconcile custom resources when you create them: + ```shell + kubectl apply -f examples/sample-resource.yaml + ``` + +### Detailed Examples + +For comprehensive setup instructions and examples, see: +- [MySQL Schema sample README](https://github.com/operator-framework/java-operator-sdk/blob/main/sample-operators/mysql-schema/README.md) +- Individual sample directories for specific setup requirements + +## Next Steps + +After exploring the samples: +1. Review the [patterns and best practices](../patterns-best-practices) guide +2. Learn about [implementing reconcilers](../../documentation/reconciler) +3. Explore [dependent resources and workflows](../../documentation/dependent-resource-and-workflows) for advanced use cases + + + diff --git a/docs/content/en/docs/getting-started/intro-to-operators.md b/docs/content/en/docs/getting-started/intro-to-operators.md new file mode 100644 index 0000000000..2879f00db9 --- /dev/null +++ b/docs/content/en/docs/getting-started/intro-to-operators.md @@ -0,0 +1,33 @@ +--- +title: Introduction to Kubernetes operators +weight: 15 +--- + +## What are Kubernetes Operators? + +Kubernetes operators are software extensions that manage both cluster and non-cluster resources on behalf of Kubernetes. The Java Operator SDK (JOSDK) makes it easy to implement Kubernetes operators in Java, with APIs designed to feel natural to Java developers and framework handling of common problems so you can focus on your business logic. + +## Why Use Java Operator SDK? + +JOSDK provides several key advantages: + +- **Java-native APIs** that feel familiar to Java developers +- **Automatic handling** of common operator challenges (caching, event handling, retries) +- **Production-ready features** like observability, metrics, and error handling +- **Simplified development** so you can focus on business logic instead of Kubernetes complexities + +## Learning Resources + +### Getting Started +- [Introduction to Kubernetes operators](https://blog.container-solutions.com/kubernetes-operators-explained) - Core concepts explained +- [Implementing Kubernetes Operators in Java](https://www.youtube.com/watch?v=CvftaV-xrB4) - Introduction talk +- [Kubernetes operator pattern documentation](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) - Official Kubernetes docs + +### Deep Dives +- [Problems JOSDK solves](https://blog.container-solutions.com/a-deep-dive-into-the-java-operator-sdk) - Technical deep dive +- [Why Java operators make sense](https://blog.container-solutions.com/cloud-native-java-infrastructure-automation-with-kubernetes-operators) - Java in cloud-native infrastructure +- [Building a Kubernetes operator SDK for Java](https://csviri.medium.com/deep-dive-building-a-kubernetes-operator-sdk-for-java-developers-5008218822cb) - Framework design principles + +### Tutorials +- [Writing Kubernetes operators using JOSDK](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk) - Step-by-step blog series + diff --git a/docs/content/en/docs/getting-started/patterns-best-practices.md b/docs/content/en/docs/getting-started/patterns-best-practices.md new file mode 100644 index 0000000000..f092d48971 --- /dev/null +++ b/docs/content/en/docs/getting-started/patterns-best-practices.md @@ -0,0 +1,118 @@ +--- +title: Patterns and best practices +weight: 25 +--- + +This document describes patterns and best practices for building and running operators, and how to implement them using the Java Operator SDK (JOSDK). + +See also best practices in the [Operator SDK](https://sdk.operatorframework.io/docs/best-practices/best-practices/). + +## Implementing a Reconciler + +### Always Reconcile All Resources + +Reconciliation can be triggered by events from multiple sources. It might be tempting to check the events and only reconcile the related resource or subset of resources that the controller manages. However, this is **considered an anti-pattern** for operators. + +**Why this is problematic:** +- Kubernetes' distributed nature makes it difficult to ensure all events are received +- If your operator misses some events and doesn't reconcile the complete state, it might operate with incorrect assumptions about the cluster state +- Always reconcile all resources, regardless of the triggering event + +JOSDK makes this efficient by providing smart caches to avoid unnecessary Kubernetes API server access and ensuring your reconciler is triggered only when needed. + +Since there's industry consensus on this topic, JOSDK no longer provides event access from `Reconciler` implementations starting with version 2. + +### Event Sources and Caching + +During reconciliation, best practice is to reconcile all dependent resources managed by the controller. This means comparing the desired state with the actual cluster state. + +**The Challenge**: Reading the actual state directly from the Kubernetes API Server every time would create significant load. + +**The Solution**: Create a watch for dependent resources and cache their latest state using the Informer pattern. In JOSDK, informers are wrapped into `EventSource` to integrate with the framework's eventing system via the `InformerEventSource` class. + +**How it works**: +- New events trigger reconciliation only when the resource is already cached +- Reconciler implementations compare desired state with cached observed state +- If a resource isn't in cache, it needs to be created +- If actual state doesn't match desired state, the resource needs updating + +### Idempotency + +Since all resources should be reconciled when your `Reconciler` is triggered, and reconciliations can be triggered multiple times for any given resource (especially with retry policies), it's crucial that `Reconciler` implementations be **idempotent**. + +**Idempotency means**: The same observed state should always result in exactly the same outcome. + +**Key implications**: +- Operators should generally operate in a stateless fashion +- Since operators usually manage declarative resources, ensuring idempotency is typically straightforward + +### Synchronous vs Asynchronous Resource Handling + +Sometimes your reconciliation logic needs to wait for resources to reach their desired state (e.g., waiting for a `Pod` to become ready). You can approach this either synchronously or asynchronously. + +#### Asynchronous Approach (Recommended) + +Exit the reconciliation logic as soon as the `Reconciler` determines it cannot complete at this point. This frees resources to process other events. + +**Requirements**: Set up adequate event sources to monitor state changes of all resources the operator waits for. When state changes occur, the `Reconciler` is triggered again and can finish processing. + +#### Synchronous Approach + +Periodically poll resources' state until they reach the desired state. If done within the `reconcile` method, this blocks the current thread for potentially long periods. + +**Recommendation**: Use the asynchronous approach for better resource utilization. + +## Why Use Automatic Retries? + +Automatic retries are enabled by default and configurable. While you can deactivate this feature, we advise against it. + +**Why retries are important**: +- **Transient network errors**: Common in Kubernetes' distributed environment, easily resolved with retries +- **Resource conflicts**: When multiple actors modify resources simultaneously, conflicts can be resolved by reconciling again +- **Transparency**: Automatic retries make error handling completely transparent when successful + +## Managing State + +Thanks to Kubernetes resources' declarative nature, operators dealing only with Kubernetes resources can operate statelessly. They don't need to maintain resource state information since it should be possible to rebuild the complete resource state from its representation. + +### When State Management Becomes Necessary + +This stateless approach typically breaks down when dealing with external resources. You might need to track external state or allocated +values for future reconciliations. There are multiple options: + + +1. Putting state in the primary resource's status sub-resource. This is a bit more complex that might seem at the first look. + Refer to the [documentation](../documentation/reconciler.md#making-sure-the-primary-resource-is-up-to-date-for-the-next-reconciliation) + for further details. + +2. Store state in separate resources designed for this purpose: +- Kubernetes Secret or ConfigMap +- Dedicated Custom Resource with validated structure + +## Handling Informer Errors and Cache Sync Timeouts + +You can [configure](https://github.com/java-operator-sdk/java-operator-sdk/blob/2cb616c4c4fd0094ee6e3a0ef2a0ea82173372bf/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) whether the operator should stop when informer errors occur on startup. + +### Default Behavior +By default, if there's a startup error (e.g., the informer lacks permissions to list target resources for primary or secondary resources), the operator stops immediately. + +### Alternative Configuration +Set the flag to `false` to start the operator even when some informers fail to start. In this case: +- The operator continuously retries connection with exponential backoff +- This applies both to startup failures and runtime problems +- The operator only stops for fatal errors (currently when a resource cannot be deserialized) + +**Use case**: When watching multiple namespaces, it's better to start the operator so it can handle other namespaces while resolving permission issues in specific namespaces. + +### Cache Sync Timeout Impact +The `stopOnInformerErrorDuringStartup` setting affects [cache sync timeout](https://github.com/java-operator-sdk/java-operator-sdk/blob/114c4312c32b34688811df8dd7cea275878c9e73/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L177-L179) behavior: +- **If `true`**: Operator stops on cache sync timeout +- **If `false`**: After timeout, the controller starts reconciling resources even if some event source caches haven't synced yet + +## Graceful Shutdown + +You can provide sufficient time for the reconciler to process and complete ongoing events before shutting down. Simply set an appropriate duration value for `reconciliationTerminationTimeout` using `ConfigurationServiceOverrider`. + +```java +final var operator = new Operator(override -> override.withReconciliationTerminationTimeout(Duration.ofSeconds(5))); +``` diff --git a/docs/content/en/docs/glossary/_index.md b/docs/content/en/docs/glossary/_index.md index 187a0bfb4a..282a98d4df 100644 --- a/docs/content/en/docs/glossary/_index.md +++ b/docs/content/en/docs/glossary/_index.md @@ -1,24 +1,12 @@ --- title: Glossary -weight: 40 +weight: 100 --- -- **Primary Resource** - the resource that represents the desired state that the controller is - working to achieve. While this is often a Custom Resource, it can be also be a Kubernetes native - resource (Deployment, ConfigMap,...). -- **Secondary Resource** - any resource that the controller needs to manage the reach the desired - state represented by the primary resource. These resources can be created, updated, deleted or - simply read depending on the use case. For example, the `Deployment` controller manages - `ReplicaSet` instances when trying to realize the state represented by the `Deployment`. In - this scenario, the `Deployment` is the primary resource while `ReplicaSet` is one of the - secondary resources managed by the `Deployment` controller. -- **Dependent Resource** - a feature of JOSDK, to make it easier to manage secondary resources. A - dependent resource represents a secondary resource with related reconciliation logic. -- **Low-level API** - refers to the SDK APIs that don't use any of features (such as Dependent - Resources or Workflows) outside of the core -- [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) - interface. See - the [WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java) - . The same logic - is also implemented using - [Dependent Resource and Workflows](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java) \ No newline at end of file +- **Primary Resource** - The resource representing the desired state that the controller works to achieve. While often a Custom Resource, it can also be a native Kubernetes resource (Deployment, ConfigMap, etc.). + +- **Secondary Resource** - Any resource the controller needs to manage to reach the desired state represented by the primary resource. These can be created, updated, deleted, or simply read depending on the use case. For example, the `Deployment` controller manages `ReplicaSet` instances to realize the state represented by the `Deployment`. Here, `Deployment` is the primary resource while `ReplicaSet` is a secondary resource. + +- **Dependent Resource** - A JOSDK feature that makes managing secondary resources easier. A dependent resource represents a secondary resource with associated reconciliation logic. + +- **Low-level API** - SDK APIs that don't use features beyond the core [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) interface (such as Dependent Resources or Workflows). See the [WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java). The same logic is also implemented using [Dependent Resource and Workflows](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java). \ No newline at end of file diff --git a/docs/content/en/docs/intro-to-operators/_index.md b/docs/content/en/docs/intro-to-operators/_index.md deleted file mode 100644 index 54fc7d82a8..0000000000 --- a/docs/content/en/docs/intro-to-operators/_index.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Introduction to Operators - -weight: 10 ---- - -This page provides a selection of articles that gives an introduction to Kubernetes operators. - -## Operators in General - - [Implementing Kubernetes Operators in Java talk](https://www.youtube.com/watch?v=CvftaV-xrB4) - - [Introduction of the concept of Kubernetes Operators](https://blog.container-solutions.com/kubernetes-operators-explained) - - [Operator pattern explained in Kubernetes documentation](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) - - [An explanation why Java Operators makes sense](https://blog.container-solutions.com/cloud-native-java-infrastructure-automation-with-kubernetes-operators) - - [What are the problems an operator framework is solving](https://csviri.medium.com/deep-dive-building-a-kubernetes-operator-sdk-for-java-developers-5008218822cb) - - [Writing Kubernetes operators using JOSDK blog series](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk) - diff --git a/docs/content/en/docs/migration/_index.md b/docs/content/en/docs/migration/_index.md index 9d7ca4d7f2..115adab35d 100644 --- a/docs/content/en/docs/migration/_index.md +++ b/docs/content/en/docs/migration/_index.md @@ -1,4 +1,5 @@ --- title: Migrations +weight: 150 --- diff --git a/docs/content/en/docs/migration/v3-1-migration.md b/docs/content/en/docs/migration/v3-1-migration.md index e42c4a206a..b4b42d9a5e 100644 --- a/docs/content/en/docs/migration/v3-1-migration.md +++ b/docs/content/en/docs/migration/v3-1-migration.md @@ -13,7 +13,7 @@ renamed accordingly. Version 3.1 comes with a workflow engine that replaces the previous behavior of managed dependent resources. -See [Workflows documentation](https://javaoperatorsdk.io/docs/workflows) for further details. +See [Workflows documentation](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/workflows/) for further details. The primary impact after upgrade is a change of the order in which managed dependent resources are reconciled. They are now reconciled in parallel with optional ordering defined using the ['depends_on'](https://github.com/java-operator-sdk/java-operator-sdk/blob/df44917ef81725c10bbcb772ab7b434d511b13b9/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java#L23-L23) diff --git a/docs/content/en/docs/migration/v4-4-migration.md b/docs/content/en/docs/migration/v4-4-migration.md index 998e6ddf9a..913c08b843 100644 --- a/docs/content/en/docs/migration/v4-4-migration.md +++ b/docs/content/en/docs/migration/v4-4-migration.md @@ -55,7 +55,7 @@ explicitly to the Operator constructor, it is now recommended to provide that va ## Using Server-Side Apply in Dependent Resources From this version by -default [Dependent Resources](https://javaoperatorsdk.io/docs/dependent-resources) use +default [Dependent Resources](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/) use [Server Side Apply (SSA)](https://kubernetes.io/docs/reference/using-api/server-side-apply/) to create and update Kubernetes resources. A diff --git a/docs/content/en/docs/migration/v4-5-migration.md b/docs/content/en/docs/migration/v4-5-migration.md index eff1581d87..42e78d76dc 100644 --- a/docs/content/en/docs/migration/v4-5-migration.md +++ b/docs/content/en/docs/migration/v4-5-migration.md @@ -5,7 +5,7 @@ permalink: /docs/v4-5-migration --- Version 4.5 introduces improvements related to event handling for Dependent Resources, more precisely the -[caching and event handling](https://javaoperatorsdk.io/docs/dependent-resources#caching-and-event-handling-in-kubernetesdependentresource) +[caching and event handling](https://javaoperatorsdk.io/docs/documentation/dependent-resource-and-workflows/dependent-resources/#caching-and-event-handling-in-kubernetesdependentresource) features. As a result the Kubernetes resources managed using [KubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/73b1d8db926a24502c3a70da34f6bcac4f66b4eb/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java#L72-L72) or its subclasses, will add an annotation recording the resource's version whenever JOSDK updates or creates such diff --git a/docs/content/en/docs/patterns-and-best-practices/_index.md b/docs/content/en/docs/patterns-and-best-practices/_index.md deleted file mode 100644 index a2b3b716b6..0000000000 --- a/docs/content/en/docs/patterns-and-best-practices/_index.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Patterns and Best Practices -weight: 25 ---- - - -This document describes patterns and best practices, to build and run operators, and how to -implement them in terms of the Java Operator SDK (JOSDK). - -See also best practices -in [Operator SDK](https://sdk.operatorframework.io/docs/best-practices/best-practices/). - -## Implementing a Reconciler - -### Reconcile All The Resources All the Time - -The reconciliation can be triggered by events from multiple sources. It could be tempting to check -the events and reconcile just the related resource or subset of resources that the controller -manages. However, this is **considered an anti-pattern** for operators because the distributed -nature of Kubernetes makes it difficult to ensure that all events are always received. If, for -some reason, your operator doesn't receive some events, if you do not reconcile the whole state, -you might be operating with improper assumptions about the state of the cluster. This is why it -is important to always reconcile all the resources, no matter how tempting it might be to only -consider a subset. Luckily, JOSDK tries to make it as easy and efficient as possible by -providing smart caches to avoid unduly accessing the Kubernetes API server and by making sure -your reconciler is only triggered when needed. - -Since there is a consensus regarding this topic in the industry, JOSDK does not provide -event access from `Reconciler` implementations anymore starting with version 2 of the framework. - -### EventSources and Caching - -As mentioned above during a reconciliation best practice is to reconcile all the dependent resources -managed by the controller. This means that we want to compare a desired state with the actual -state of the cluster. Reading the actual state of a resource from the Kubernetes API Server -directly all the time would mean a significant load. Therefore, it's a common practice to -instead create a watch for the dependent resources and cache their latest state. This is done -following the Informer pattern. In Java Operator SDK, informers are wrapped into an `EventSource`, -to integrate it with the eventing system of the framework. This is implemented by the -`InformerEventSource` class. - -A new event that triggers the reconciliation is only propagated to the `Reconciler` when the actual -resource is already in cache. `Reconciler` implementations therefore only need to compare the -desired state with the observed one provided by the cached resource. If the resource cannot be -found in the cache, it therefore needs to be created. If the actual state doesn't match the -desired state, the resource needs to be updated. - -### Idempotency - -Since all resources should be reconciled when your `Reconciler` is triggered and reconciliations -can be triggered multiple times for any given resource, especially when retry policies are in -place, it is especially important that `Reconciler` implementations be idempotent, meaning that -the same observed state should result in exactly the same outcome. This also means that -operators should generally operate in stateless fashion. Luckily, since operators are usually -managing declarative resources, ensuring idempotency is usually not difficult. - -### Sync or Async Way of Resource Handling - -Depending on your use case, it's possible that your reconciliation logic needs to wait a -non-insignificant amount of time while the operator waits for resources to reach their desired -state. For example, you `Reconciler` might need to wait for a `Pod` to get ready before -performing additional actions. This problem can be approached either synchronously or -asynchronously. - -The asynchronous way is to just exit the reconciliation logic as soon as the `Reconciler` -determines that it cannot complete its full logic at this point in time. This frees resources to -process other primary resource events. However, this requires that adequate event sources are -put in place to monitor state changes of all the resources the operator waits for. When this is -done properly, any state change will trigger the `Reconciler` again and it will get the -opportunity to finish its processing - -The synchronous way would be to periodically poll the resources' state until they reach their -desired state. If this is done in the context of the `reconcile` method of your `Reconciler` -implementation, this would block the current thread for possibly a long time. It's therefore -usually recommended to use the asynchronous processing fashion. - -## Why have Automatic Retries? - -Automatic retries are in place by default and can be configured to your needs. It is also -possible to completely deactivate the feature, though we advise against it. The main reason -configure automatic retries for your `Reconciler` is due to the fact that errors occur quite -often due to the distributed nature of Kubernetes: transient network errors can be easily dealt -with by automatic retries. Similarly, resources can be modified by different actors at the same -time, so it's not unheard of to get conflicts when working with Kubernetes resources. Such -conflicts can usually be quite naturally resolved by reconciling the resource again. If it's -done automatically, the whole process can be completely transparent. - -## Managing State - -Thanks to the declarative nature of Kubernetes resources, operators that deal only with -Kubernetes resources can operate in a stateless fashion, i.e. they do not need to maintain -information about the state of these resources, as it should be possible to completely rebuild -the resource state from its representation (that's what declarative means, after all). -However, this usually doesn't hold true anymore when dealing with external resources, and it -might be necessary for the operator to keep track of this external state so that it is available -when another reconciliation occurs. While such state could be put in the primary resource's -status sub-resource, this could become quickly difficult to manage if a lot of state needs to be -tracked. It also goes against the best practice that a resource's status should represent the -actual resource state, when its spec represents the desired state. Putting state that doesn't -strictly represent the resource's actual state is therefore discouraged. Instead, it's -advised to put such state into a separate resource meant for this purpose such as a -Kubernetes Secret or ConfigMap or even a dedicated Custom Resource, which structure can be more -easily validated. - -## Stopping (or not) Operator in case of Informer Errors and Cache Sync Timeouts - -It can -be [configured](https://github.com/java-operator-sdk/java-operator-sdk/blob/2cb616c4c4fd0094ee6e3a0ef2a0ea82173372bf/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) -if the operator should stop in case of any informer error happens on startup. By default, if there ia an error on -startup and the informer for example has no permissions list the target resources (both the primary resource or -secondary resources) the operator will stop instantly. This behavior can be altered by setting the mentioned flag -to `false`, so operator will start even some informers are not started. In this case - same as in case when an informer -is started at first but experienced problems later - will continuously retry the connection indefinitely with an -exponential backoff. The operator will just stop if there is a fatal -error, [currently](https://github.com/java-operator-sdk/java-operator-sdk/blob/0e55c640bf8be418bc004e51a6ae2dcf7134c688/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java#L64-L66) -that is when a resource cannot be deserialized. The typical use case for changing this flag is when a list of namespaces -is watched by a controller. In is better to start up the operator, so it can handle other namespaces while there -might be a permission issue for some resources in another namespace. - -The `stopOnInformerErrorDuringStartup` has implication on [cache sync timeout](https://github.com/java-operator-sdk/java-operator-sdk/blob/114c4312c32b34688811df8dd7cea275878c9e73/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L177-L179) -behavior. If true operator will stop on cache sync timeout. if `false`, after the timeout the controller will start -reconcile resources even if one or more event source caches did not sync yet. - -## Graceful Shutdown - -You can provide sufficient time for the reconciler to process and complete the currently ongoing events before shutting down. -The configuration is simple. You just need to set an appropriate duration value for `reconciliationTerminationTimeout` using `ConfigurationServiceOverrider`. - -```java -final var overridden = new ConfigurationServiceOverrider(config) - .withReconciliationTerminationTimeout(Duration.ofSeconds(5)); - -final var operator = new Operator(overridden); -``` diff --git a/docs/content/en/docs/using-samples/_index.md b/docs/content/en/docs/using-samples/_index.md deleted file mode 100644 index 62319860ac..0000000000 --- a/docs/content/en/docs/using-samples/_index.md +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: Using sample Operators -weight: 30 ---- - -We have examples under [sample-operators](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators) -directory which are intended to demonstrate the usage of different components in different scenarios, but mainly are more real world -examples: - -* *webpage*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. -* *mysql-schema*: Operator managing schemas in a MySQL database. Shows how to manage non Kubernetes resources. -* *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps running in Tomcat. The intention - with this example to show how to manage multiple related custom resources and/or more controllers. - -# Implementing a Sample Operator - -Add [dependency](https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk) to your project with Maven: - -```xml - - - io.javaoperatorsdk - operator-framework - {see https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk for latest version} - -``` - -Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to generate the mappings -between controllers and custom resource classes: - -```groovy -dependencies { - implementation "io.javaoperatorsdk:operator-framework:${javaOperatorVersion}" - annotationProcessor "io.javaoperatorsdk:operator-framework:${javaOperatorVersion}" -} -``` - -Once you've added the dependency, define a main method initializing the Operator and registering a controller. - -```java -public class Runner { - - public static void main(String[] args) { - Operator operator = new Operator(); - operator.register(new WebPageReconciler()); - operator.start(); - } -} -``` - -The Controller implements the business logic and describes all the classes needed to handle the CRD. - -```java - -@ControllerConfiguration -public class WebPageReconciler implements Reconciler { - - // Return the changed resource, so it gets updated. See javadoc for details. - @Override - public UpdateControl reconcile(CustomService resource, - Context context) { - // ... your logic ... - return UpdateControl.patchStatus(resource); - } -} -``` - -A sample custom resource POJO representation - -```java - -@Group("sample.javaoperatorsdk") -@Version("v1") -public class WebPage extends CustomResource implements - Namespaced { -} - -public class WebServerSpec { - - private String html; - - public String getHtml() { - return html; - } - - public void setHtml(String html) { - this.html = html; - } -} -``` - -### Deactivating CustomResource implementations validation - -The operator will, by default, query the deployed CRDs to check that the `CustomResource` -implementations match what is known to the cluster. This requires an additional query to the cluster and, sometimes, -elevated privileges for the operator to be able to read the CRDs from the cluster. This validation is mostly meant to -help users new to operator development get started and avoid common mistakes. Advanced users or production deployments -might want to skip this step. This is done by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. - -### Automatic generation of CRDs - -To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following -dependencies to your project (in the background an annotation processor is used), with Maven: - -```xml - - - io.fabric8 - crd-generator-apt - provided - -``` - -or with Gradle: - -```groovy -dependencies { - annotationProcessor 'io.fabric8:crd-generator-apt:' - ... -} -``` - -The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use -the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using -the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: - -- `mycrs.java-operator-sdk.io-v1.yml` -- `mycrs.java-operator-sdk.io-v1beta1.yml` - -**NOTE:** -> Quarkus users using the `quarkus-operator-sdk` extension do not need to add any extra dependency to get their CRD generated as this is handled by the extension itself. - -### Quarkus - -A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based operators. - -Add [this dependency](https://search.maven.org/search?q=a:quarkus-operator-sdk) -to your project: - -```xml - - - io.quarkiverse.operatorsdk - quarkus-operator-sdk - {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} - - -``` - -Create an Application, Quarkus will automatically create and inject a `KubernetesClient` ( -or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that your application can -use. Below, you can see the minimal code you need to write to get your operator and controllers up and running: - -```java - -@QuarkusMain -public class QuarkusOperator implements QuarkusApplication { - - @Inject - Operator operator; - - public static void main(String... args) { - Quarkus.run(QuarkusOperator.class, args); - } - - @Override - public int run(String... args) throws Exception { - operator.start(); - Quarkus.waitForExit(); - return 0; - } -} -``` - -### Spring Boot - -You can also let Spring Boot wire your application together and automatically register the controllers. - -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk) to your project: - -```xml - - - io.javaoperatorsdk - operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for - latest version} - - -``` - -Create an Application - -```java - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} -``` - -You will also need a `@Configuration` to make sure that your reconciler is registered: - -```java - -@Configuration -public class Config { - - @Bean - public WebPageReconciler customServiceController() { - return new WebPageReconciler(); - } - - @Bean(initMethod = "start", destroyMethod = "stop") - @SuppressWarnings("rawtypes") - public Operator operator(List controllers) { - Operator operator = new Operator(); - controllers.forEach(operator::register); - return operator; - } -} -``` - -#### Spring Boot test support - -Adding the following dependency would let you mock the operator for the tests where loading the spring container is -necessary, but it doesn't need real access to a Kubernetes cluster. - -```xml - - - io.javaoperatorsdk - operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for - latest version} - - -``` - -Mock the operator: - -```java - -@SpringBootTest -@EnableMockOperator -public class SpringBootStarterSampleApplicationTest { - - @Test - void contextLoads() { - } -} -``` diff --git a/docs/hugo.toml b/docs/hugo.toml index b4535c08af..435cad2451 100644 --- a/docs/hugo.toml +++ b/docs/hugo.toml @@ -160,20 +160,20 @@ enable = false [params.links] [[params.links.user]] - name ="Twitter" - url = "https://twitter.com/javaoperatorsdk" - icon = "fab fa-twitter" - desc = "Follow us on Twitter to get the latest news!" + name ="BlueSky" + url = "https://bsky.app/profile/javaoperatorsdk.bsky.social" + icon = "fa-brands fa-bluesky" + desc = "Follow us on BlueSky to get the latest news!" [[params.links.user]] name = "Slack" url = "https://kubernetes.slack.com/archives/CAW0GV7A5" icon = "fab fa-slack" -desc = "Chat with other project developers" +desc = "Chat with other project developers on Kubernetes slack" [[params.links.user]] name = "Discord" url = "https://discord.gg/DacEhAy" icon = "fab fa-discord" -desc = "Chat with others on Discord" +desc = "Chat with others on our dedicated Discord server" #[[params.links.user]] # name = "Stack Overflow" # url = "https://example.org/stack" diff --git a/docs/static/images/cncf_logo2.png b/docs/static/images/cncf_logo2.png new file mode 100644 index 0000000000..e1236b7e87 Binary files /dev/null and b/docs/static/images/cncf_logo2.png differ diff --git a/docs/static/images/event-sources.png b/docs/static/images/event-sources.png new file mode 100644 index 0000000000..f37e1f72d9 Binary files /dev/null and b/docs/static/images/event-sources.png differ diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 67fe0d4b4c..c66a2d339f 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index d0bae9c140..7770b05ab8 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk operator-framework-bom - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators @@ -33,19 +33,12 @@ https://github.com/operator-framework/java-operator-sdk/tree/master - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - 1.7.0 - 3.2.7 + 3.2.8 3.3.1 - 3.11.2 - 2.44.3 + 3.12.0 + 3.0.0 + 0.9.0 @@ -78,6 +71,7 @@ com.diffplug.spotless spotless-maven-plugin + ${spotless.version} @@ -115,6 +109,17 @@ release + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.java + **/*E2E.java + **/InformerRelatedBehaviorTest.java + + + org.apache.maven.plugins maven-javadoc-plugin @@ -138,13 +143,13 @@ jar + verify org.apache.maven.plugins maven-gpg-plugin - ${maven-gpg-plugin.version} sign-artifacts @@ -162,14 +167,15 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} true - ossrh - https://oss.sonatype.org/ - true + central + true + true + published diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a4dc87a3d3..5b4281a1ec 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 22072bb696..f65f6ae022 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -24,17 +24,17 @@ public class Operator implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); - private final ControllerManager controllerManager; - private final LeaderElectionManager leaderElectionManager; - private final ConfigurationService configurationService; + private ControllerManager controllerManager; + private LeaderElectionManager leaderElectionManager; + private ConfigurationService configurationService; private volatile boolean started = false; public Operator() { - this((KubernetesClient) null); + init(initConfigurationService(null, null), true); } Operator(KubernetesClient kubernetesClient) { - this(initConfigurationService(kubernetesClient, null)); + init(initConfigurationService(kubernetesClient, null), false); } /** @@ -46,12 +46,7 @@ public Operator() { * operator */ public Operator(ConfigurationService configurationService) { - this.configurationService = configurationService; - - final var executorServiceManager = configurationService.getExecutorServiceManager(); - controllerManager = new ControllerManager(executorServiceManager); - - leaderElectionManager = new LeaderElectionManager(controllerManager, configurationService); + init(configurationService, false); } /** @@ -62,10 +57,55 @@ public Operator(ConfigurationService configurationService) { * {@link ConfigurationService} values */ public Operator(Consumer overrider) { - this(initConfigurationService(null, overrider)); + init(initConfigurationService(null, overrider), false); + } + + /** + * In a deferred initialization scenario, the default constructor will typically be called to + * create a proxy instance, usually to be replaced at some later time when the dependents (in this + * case the ConfigurationService instance) are available. In this situation, we want to make it + * possible to not perform the initialization steps directly so this implementation makes it + * possible to not crash when a null ConfigurationService is passed only if deferred + * initialization is allowed + * + * @param configurationService the potentially {@code null} {@link ConfigurationService} to use + * for this operator + * @param allowDeferredInit whether or not deferred initialization of the configuration service is + * allowed + * @throws IllegalStateException if the specified configuration service is {@code null} but + * deferred initialization is not allowed + */ + private void init(ConfigurationService configurationService, boolean allowDeferredInit) { + if (configurationService == null) { + if (!allowDeferredInit) { + throw new IllegalStateException( + "Deferred initialization of ConfigurationService is not allowed"); + } + } else { + this.configurationService = configurationService; + + final var executorServiceManager = configurationService.getExecutorServiceManager(); + controllerManager = new ControllerManager(executorServiceManager); + + leaderElectionManager = new LeaderElectionManager(controllerManager, configurationService); + } } - private static ConfigurationService initConfigurationService( + /** + * Overridable by subclasses to enable deferred configuration, useful to avoid unneeded processing + * in injection scenarios, typically returning {@code null} here instead of performing any + * configuration + * + * @param client a potentially {@code null} {@link KubernetesClient} to initialize the operator's + * {@link ConfigurationService} with + * @param overrider a potentially {@code null} {@link ConfigurationServiceOverrider} consumer to + * override the default {@link ConfigurationService} with + * @return a ready to use {@link ConfigurationService} using values provided by the specified + * overrides and kubernetes client, if provided or {@code null} in case deferred + * initialization is possible, in which case it is up to the extension to ensure that the + * {@link ConfigurationService} is properly set before the operator instance is used + */ + protected ConfigurationService initConfigurationService( KubernetesClient client, Consumer overrider) { // initialize the client if the user didn't provide one if (client == null) { @@ -232,8 +272,8 @@ public

RegisteredController

register( * * @param reconciler part of the reconciler to register * @param configOverrider consumer to use to change config values - * @return registered controller * @param

the {@code HasMetadata} type associated with the reconciler + * @return registered controller */ public

RegisteredController

register( Reconciler

reconciler, Consumer> configOverrider) { @@ -266,4 +306,14 @@ boolean isStarted() { public ConfigurationService getConfigurationService() { return configurationService; } + + /** + * Make it possible for extensions to set the {@link ConfigurationService} after the operator has + * been initialized + * + * @param configurationService the {@link ConfigurationService} to use for this operator + */ + protected void setConfigurationService(ConfigurationService configurationService) { + init(configurationService, false); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index ea7c58acfb..a2d3d72e5f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -28,6 +28,8 @@ public class ReconcilerUtils { protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io"; private static final String GET_SPEC = "getSpec"; private static final String SET_SPEC = "setSpec"; + private static final String SET_STATUS = "setStatus"; + private static final String GET_STATUS = "getStatus"; private static final Pattern API_URI_PATTERN = Pattern.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*"); // NOSONAR: input is controlled @@ -135,11 +137,23 @@ public static Object getSpec(HasMetadata resource) { return cr.getSpec(); } + return getSpecOrStatus(resource, GET_SPEC); + } + + public static Object getStatus(HasMetadata resource) { + // optimize CustomResource case + if (resource instanceof CustomResource cr) { + return cr.getStatus(); + } + return getSpecOrStatus(resource, GET_STATUS); + } + + private static Object getSpecOrStatus(HasMetadata resource, String getMethod) { try { - Method getSpecMethod = resource.getClass().getMethod(GET_SPEC); + Method getSpecMethod = resource.getClass().getMethod(getMethod); return getSpecMethod.invoke(resource); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw noSpecException(resource, e); + throw noMethodException(resource, e, getMethod); } } @@ -151,31 +165,46 @@ public static Object setSpec(HasMetadata resource, Object spec) { return null; } + return setSpecOrStatus(resource, spec, SET_SPEC); + } + + @SuppressWarnings("unchecked") + public static Object setStatus(HasMetadata resource, Object status) { + // optimize CustomResource case + if (resource instanceof CustomResource cr) { + cr.setStatus(status); + return null; + } + return setSpecOrStatus(resource, status, SET_STATUS); + } + + private static Object setSpecOrStatus( + HasMetadata resource, Object spec, String setterMethodName) { try { Class resourceClass = resource.getClass(); // if given spec is null, find the method just using its name - Method setSpecMethod; + Method setMethod; if (spec != null) { - setSpecMethod = resourceClass.getMethod(SET_SPEC, spec.getClass()); + setMethod = resourceClass.getMethod(setterMethodName, spec.getClass()); } else { - setSpecMethod = + setMethod = Arrays.stream(resourceClass.getMethods()) - .filter(method -> SET_SPEC.equals(method.getName())) + .filter(method -> setterMethodName.equals(method.getName())) .findFirst() - .orElseThrow(() -> noSpecException(resource, null)); + .orElseThrow(() -> noMethodException(resource, null, setterMethodName)); } - return setSpecMethod.invoke(resource, spec); + return setMethod.invoke(resource, spec); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw noSpecException(resource, e); + throw noMethodException(resource, e, setterMethodName); } } - private static IllegalStateException noSpecException( - HasMetadata resource, ReflectiveOperationException e) { + private static IllegalStateException noMethodException( + HasMetadata resource, ReflectiveOperationException e, String methodName) { return new IllegalStateException( - "No spec found on resource " + resource.getClass().getName(), e); + "No method: " + methodName + " found on resource " + resource.getClass().getName(), e); } public static T loadYaml(Class clazz, Class loader, String yaml) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java index b7fbce7f07..0495131d79 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java @@ -5,6 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -22,7 +23,7 @@ public class RuntimeInfo { private final Operator operator; public RuntimeInfo(Operator operator) { - this.registeredControllers = operator.getRegisteredControllers(); + this.registeredControllers = Collections.unmodifiableSet(operator.getRegisteredControllers()); this.operator = operator; } @@ -30,6 +31,7 @@ public boolean isStarted() { return operator.isStarted(); } + @SuppressWarnings("unused") public Set getRegisteredControllers() { checkIfStarted(); return registeredControllers; @@ -80,4 +82,23 @@ public Map> unhealthyEventSource } return res; } + + /** + * Retrieves the {@link RegisteredController} associated with the specified controller name or + * {@code null} if no such controller is registered. + * + * @param controllerName the name of the {@link RegisteredController} to retrieve + * @return the {@link RegisteredController} associated with the specified controller name or + * {@code null} if no such controller is registered + * @since 5.1.2 + */ + @SuppressWarnings({"unchecked", "unused"}) + public RegisteredController getRegisteredController( + String controllerName) { + checkIfStarted(); + return registeredControllers.stream() + .filter(rc -> rc.getConfiguration().getName().equals(controllerName)) + .findFirst() + .orElse(null); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 438f7d91a9..891f199dbe 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -30,6 +30,12 @@ import static io.javaoperatorsdk.operator.api.config.ControllerConfiguration.CONTROLLER_NAME_AS_FIELD_MANAGER; +/** + * A default {@link ConfigurationService} implementation, resolving {@link Reconciler}s + * configuration when it has already been resolved before. If this behavior is not adequate, please + * use {@link AbstractConfigurationService} instead as a base for your {@code ConfigurationService} + * implementation. + */ public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -149,10 +155,12 @@ private static void configureFromAnnotatedReconciler( @Override protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { - logger.warn( - "Configuration for reconciler '{}' was not found. {}", - reconcilerKey, - reconcilersNameMessage); + if (!createIfNeeded()) { + logger.warn( + "Configuration for reconciler '{}' was not found. {}", + reconcilerKey, + reconcilersNameMessage); + } } @SuppressWarnings("unused") @@ -318,6 +326,13 @@ private

ResolvedControllerConfiguration

controllerCon informerConfig); } + /** + * @deprecated This method was meant to allow subclasses to prevent automatic creation of the + * configuration when not found. This functionality is now removed, if you want to be able to + * prevent automated, on-demand creation of a reconciler's configuration, please use the + * {@link AbstractConfigurationService} implementation instead as base for your extension. + */ + @Deprecated(forRemoval = true) protected boolean createIfNeeded() { return true; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 3ffc913c5e..41134e64ac 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -13,6 +13,8 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.CustomResource; @@ -26,7 +28,6 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceUpdaterMatcher; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -394,9 +395,6 @@ default boolean shouldUseSSA( Class dependentResourceType, Class resourceType, KubernetesDependentResourceConfig config) { - if (ResourceUpdaterMatcher.class.isAssignableFrom(dependentResourceType)) { - return false; - } Boolean useSSAConfig = Optional.ofNullable(config).map(KubernetesDependentResourceConfig::useSSA).orElse(null); // don't use SSA for certain resources by default, only if explicitly overridden @@ -442,12 +440,40 @@ default Set> defaultNonSSAResource() { * * @return if special annotation should be used for dependent resource to filter events * @since 4.5.0 - * @return if special annotation should be used for dependent resource to filter events */ default boolean previousAnnotationForDependentResourcesEventFiltering() { return true; } + /** + * For dependent resources, the framework can add an annotation to filter out events resulting + * directly from the framework's operation. There are, however, some resources that do not follow + * the Kubernetes API conventions that changes in metadata should not increase the generation of + * the resource (as recorded in the {@code generation} field of the resource's {@code metadata}). + * For these resources, this convention is not respected and results in a new event for the + * framework to process. If that particular case is not handled correctly in the resource matcher, + * the framework will consider that the resource doesn't match the desired state and therefore + * triggers an update, which in turn, will re-add the annotation, thus starting the loop again, + * infinitely. + * + *

As a workaround, we automatically skip adding previous annotation for those well-known + * resources. Note that if you are sure that the matcher works for your use case, and it should in + * most instances, you can remove the resource type from the blocklist. + * + *

The consequence of adding a resource type to the set is that the framework will not use + * event filtering to prevent events, initiated by changes made by the framework itself as a + * result of its processing of dependent resources, to trigger the associated reconciler again. + * + *

Note that this method only takes effect if annotating dependent resources to prevent + * dependent resources events from triggering the associated reconciler again is activated as + * controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()} + * + * @return a Set of resource classes where the previous version annotation won't be used. + */ + default Set> withPreviousAnnotationForDependentResourcesBlocklist() { + return Set.of(Deployment.class, StatefulSet.class); + } + /** * If the event logic should parse the resourceVersion to determine the ordering of dependent * resource events. This is typically not needed. @@ -459,7 +485,6 @@ default boolean previousAnnotationForDependentResourcesEventFiltering() { * * @return if resource version should be parsed (as integer) * @since 4.5.0 - * @return if resource version should be parsed (as integer) */ default boolean parseResourceVersionsForEventFilteringAndCaching() { return false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index f420be0fff..be86cbe312 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -40,6 +40,7 @@ public class ConfigurationServiceOverrider { private Boolean parseResourceVersions; private Boolean useSSAToPatchPrimaryResource; private Boolean cloneSecondaryResourcesWhenGettingFromCache; + private Set> previousAnnotationUsageBlocklist; @SuppressWarnings("rawtypes") private DependentResourceFactory dependentResourceFactory; @@ -188,6 +189,12 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC return this; } + public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist( + Set> blocklist) { + this.previousAnnotationUsageBlocklist = blocklist; + return this; + } + public ConfigurationService build() { return new BaseConfigurationService(original.getVersion(), cloner, client) { @Override @@ -247,13 +254,20 @@ public boolean closeClientOnStop() { @Override public ExecutorService getExecutorService() { - return overriddenValueOrDefault(executorService, ConfigurationService::getExecutorService); + if (executorService != null) { + return executorService; + } else { + return super.getExecutorService(); + } } @Override public ExecutorService getWorkflowExecutorService() { - return overriddenValueOrDefault( - workflowExecutorService, ConfigurationService::getWorkflowExecutorService); + if (workflowExecutorService != null) { + return workflowExecutorService; + } else { + return super.getWorkflowExecutorService(); + } } @Override @@ -328,6 +342,14 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() { cloneSecondaryResourcesWhenGettingFromCache, ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache); } + + @Override + public Set> + withPreviousAnnotationForDependentResourcesBlocklist() { + return overriddenValueOrDefault( + previousAnnotationUsageBlocklist, + ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist); + } }; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index f11fc47eef..3b6f94a025 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -134,6 +134,39 @@ public static Class getTypeArgumentFromExtendedClassByIndex(Class clazz, i } } + public static Class getTypeArgumentFromHierarchyByIndex(Class clazz, int index) { + return getTypeArgumentFromHierarchyByIndex(clazz, null, index); + } + + public static Class getTypeArgumentFromHierarchyByIndex( + Class clazz, Class expectedImplementedInterface, int index) { + Class c = clazz; + while (!(c.getGenericSuperclass() instanceof ParameterizedType)) { + c = c.getSuperclass(); + } + Class actualTypeArgument = + (Class) ((ParameterizedType) c.getGenericSuperclass()).getActualTypeArguments()[index]; + if (expectedImplementedInterface != null + && !expectedImplementedInterface.isAssignableFrom(actualTypeArgument)) { + throw new IllegalArgumentException( + GENERIC_PARAMETER_TYPE_ERROR_PREFIX + + clazz.getName() + + "because it doesn't extend a class that is parametrized with the type that" + + " implements " + + expectedImplementedInterface.getSimpleName() + + ". Please provide the resource type in the constructor (e.g.," + + " super(Deployment.class)."); + } else if (expectedImplementedInterface == null && actualTypeArgument.equals(Object.class)) { + throw new IllegalArgumentException( + GENERIC_PARAMETER_TYPE_ERROR_PREFIX + + clazz.getName() + + " because it doesn't extend a class that is parametrized with the type we want to" + + " retrieve or because it's Object.class. Please provide the resource type in the " + + "constructor (e.g., super(Deployment.class)."); + } + return actualTypeArgument; + } + public static Class getFirstTypeArgumentFromInterface( Class clazz, Class expectedImplementedInterface) { return getTypeArgumentFromInterfaceByIndex(clazz, expectedImplementedInterface, 0); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java deleted file mode 100644 index a0c9dc67ae..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -public interface DependentResourceConfigurationProvider { - @SuppressWarnings("rawtypes") - Object getConfigurationFor(DependentResourceSpec spec); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index 9fb5ad4c82..2369d5f523 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -194,6 +194,14 @@ public Builder withNamespaces(Set namespaces) { return this; } + /** + * @since 5.1.1 + */ + public Builder withNamespaces(String... namespaces) { + config.withNamespaces(Set.of(namespaces)); + return this; + } + public Builder withNamespacesInheritedFromController() { withNamespaces(SAME_AS_CONTROLLER_NAMESPACES_SET); return this; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index e5fbaad68e..f47deb9734 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -46,6 +46,13 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { /** ExecutorService initialized by framework for workflows. Used for workflow standalone mode. */ ExecutorService getWorkflowExecutorService(); + /** + * Retrieves the primary resource. + * + * @return the primary resource associated with the current reconciliation + */ + P getPrimaryResource(); + /** * Retrieves the primary resource cache. * diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 29bf0b670f..d407ed0fc6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -44,7 +44,9 @@ /** * Optional configuration of the maximal interval the SDK will wait for a reconciliation request - * to happen before one will be automatically triggered. + * to happen before one will be automatically triggered. The intention behind this feature is to + * have a failsafe, not to artificially force repeated reconciliations. For that use {@link + * UpdateControl#rescheduleAfter(long)}. * * @return the maximal reconciliation interval configuration */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index b5ea66f8bc..2acf8d13ca 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -44,18 +44,6 @@ public Set getSecondaryResources(Class expectedType) { return getSecondaryResourcesAsStream(expectedType).collect(Collectors.toSet()); } - @Override - public IndexedResourceCache

getPrimaryCache() { - return controller.getEventSourceManager().getControllerEventSource(); - } - - @Override - public boolean isNextReconciliationImminent() { - return controller - .getEventProcessor() - .isNextReconciliationImminent(ResourceID.fromResource(primaryResource)); - } - @Override public Stream getSecondaryResourcesAsStream(Class expectedType) { return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream() @@ -114,12 +102,25 @@ public ExecutorService getWorkflowExecutorService() { return controller.getExecutorServiceManager().workflowExecutorService(); } + @Override + public P getPrimaryResource() { + return primaryResource; + } + + @Override + public IndexedResourceCache

getPrimaryCache() { + return controller.getEventSourceManager().getControllerEventSource(); + } + + @Override + public boolean isNextReconciliationImminent() { + return controller + .getEventProcessor() + .isNextReconciliationImminent(ResourceID.fromResource(primaryResource)); + } + public DefaultContext

setRetryInfo(RetryInfo retryInfo) { this.retryInfo = retryInfo; return this; } - - public P getPrimaryResource() { - return primaryResource; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java new file mode 100644 index 0000000000..c61cc837c1 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java @@ -0,0 +1,232 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.function.UnaryOperator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + * Utility methods to patch the primary resource state and store it to the related cache, to make + * sure that the latest version of the resource is present for the next reconciliation. The main use + * case for such updates is to store state is resource status. + * + *

The way the framework handles this is with retryable updates with optimistic locking, and + * caches the updated resource from the response in an overlay cache on top of the Informer cache. + * If the update fails, it reads the primary resource from the cluster, applies the modifications + * again and retries the update. + */ +public class PrimaryUpdateAndCacheUtils { + + public static final int DEFAULT_MAX_RETRY = 10; + public static final int DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS = 10000; + public static final int DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS = 50; + + private PrimaryUpdateAndCacheUtils() {} + + private static final Logger log = LoggerFactory.getLogger(PrimaryUpdateAndCacheUtils.class); + + /** + * Updates the status with optimistic locking and caches the result for next reconciliation. For + * details see {@link #updateAndCacheResource}. + */ + public static

P updateStatusAndCacheResource( + P primary, Context

context, UnaryOperator

modificationFunction) { + return updateAndCacheResource( + primary, + context, + modificationFunction, + r -> context.getClient().resource(r).updateStatus()); + } + + /** + * Patches the status using JSON Merge Patch with optimistic locking and caches the result for + * next reconciliation. For details see {@link #updateAndCacheResource}. + */ + public static

P mergePatchStatusAndCacheResource( + P primary, Context

context, UnaryOperator

modificationFunction) { + return updateAndCacheResource( + primary, context, modificationFunction, r -> context.getClient().resource(r).patchStatus()); + } + + /** + * Patches the status using JSON Patch with optimistic locking and caches the result for next + * reconciliation. For details see {@link #updateAndCacheResource}. + */ + public static

P patchStatusAndCacheResource( + P primary, Context

context, UnaryOperator

modificationFunction) { + return updateAndCacheResource( + primary, + context, + UnaryOperator.identity(), + r -> context.getClient().resource(r).editStatus(modificationFunction)); + } + + /** + * Patches the status using Server Side Apply with optimistic locking and caches the result for + * next reconciliation. For details see {@link #updateAndCacheResource}. + */ + public static

P ssaPatchStatusAndCacheResource( + P primary, P freshResourceWithStatus, Context

context) { + return updateAndCacheResource( + primary, + context, + r -> freshResourceWithStatus, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + /** + * Same as {@link #updateAndCacheResource(HasMetadata, Context, UnaryOperator, UnaryOperator, int, + * long,long)} using the default maximum retry number as defined by {@link #DEFAULT_MAX_RETRY} and + * default cache maximum polling time and period as defined, respectively by {@link + * #DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS} and {@link #DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS}. + * + * @param resourceToUpdate original resource to update + * @param context of reconciliation + * @param modificationFunction modifications to make on primary + * @param updateMethod the update method implementation + * @param

primary type + * @return the updated resource + */ + public static

P updateAndCacheResource( + P resourceToUpdate, + Context

context, + UnaryOperator

modificationFunction, + UnaryOperator

updateMethod) { + return updateAndCacheResource( + resourceToUpdate, + context, + modificationFunction, + updateMethod, + DEFAULT_MAX_RETRY, + DEFAULT_RESOURCE_CACHE_TIMEOUT_MILLIS, + DEFAULT_RESOURCE_CACHE_POLL_PERIOD_MILLIS); + } + + /** + * Modifies the primary using the specified modification function, then uses the modified resource + * for the request to update with provided update method. As the {@code resourceVersion} field of + * the modified resource is set to the value found in the specified resource to update, the update + * operation will therefore use optimistic locking on the server. If the request fails on + * optimistic update, we read the resource again from the K8S API server and retry the whole + * process. In short, we make sure we always update the resource with optimistic locking, then we + * cache the resource in an internal cache. Without further going into details, the optimistic + * locking is needed so we can reliably handle the caching. + * + * @param resourceToUpdate original resource to update + * @param context of reconciliation + * @param modificationFunction modifications to make on primary + * @param updateMethod the update method implementation + * @param maxRetry maximum number of retries before giving up + * @param cachePollTimeoutMillis maximum amount of milliseconds to wait for the updated resource + * to appear in cache + * @param cachePollPeriodMillis cache polling period, in milliseconds + * @param

primary type + * @return the updated resource + */ + public static

P updateAndCacheResource( + P resourceToUpdate, + Context

context, + UnaryOperator

modificationFunction, + UnaryOperator

updateMethod, + int maxRetry, + long cachePollTimeoutMillis, + long cachePollPeriodMillis) { + + if (log.isDebugEnabled()) { + log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resourceToUpdate)); + } + P modified = null; + int retryIndex = 0; + while (true) { + try { + modified = modificationFunction.apply(resourceToUpdate); + modified + .getMetadata() + .setResourceVersion(resourceToUpdate.getMetadata().getResourceVersion()); + var updated = updateMethod.apply(modified); + context + .eventSourceRetriever() + .getControllerEventSource() + .handleRecentResourceUpdate( + ResourceID.fromResource(resourceToUpdate), updated, resourceToUpdate); + return updated; + } catch (KubernetesClientException e) { + log.trace("Exception during patch for resource: {}", resourceToUpdate); + retryIndex++; + // only retry on conflict (409) and unprocessable content (422) which + // can happen if JSON Patch is not a valid request since there was + // a concurrent request which already removed another finalizer: + // List element removal from a list is by index in JSON Patch + // so if addressing a second finalizer but first is meanwhile removed + // it is a wrong request. + if (e.getCode() != 409 && e.getCode() != 422) { + throw e; + } + if (retryIndex > maxRetry) { + log.warn("Retry exhausted, last desired resource: {}", modified); + throw new OperatorException( + "Exceeded maximum (" + + maxRetry + + ") retry attempts to patch resource: " + + ResourceID.fromResource(resourceToUpdate), + e); + } + log.debug( + "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", + resourceToUpdate.getMetadata().getName(), + resourceToUpdate.getMetadata().getNamespace(), + e.getCode()); + resourceToUpdate = + pollLocalCache( + context, resourceToUpdate, cachePollTimeoutMillis, cachePollPeriodMillis); + } + } + } + + private static

P pollLocalCache( + Context

context, P staleResource, long timeoutMillis, long pollDelayMillis) { + try { + var resourceId = ResourceID.fromResource(staleResource); + var startTime = LocalTime.now(); + final var timeoutTime = startTime.plus(timeoutMillis, ChronoUnit.MILLIS); + while (timeoutTime.isAfter(LocalTime.now())) { + log.debug("Polling cache for resource: {}", resourceId); + var cachedResource = context.getPrimaryCache().get(resourceId).orElseThrow(); + if (!cachedResource + .getMetadata() + .getResourceVersion() + .equals(staleResource.getMetadata().getResourceVersion())) { + return context + .getControllerConfiguration() + .getConfigurationService() + .getResourceCloner() + .clone(cachedResource); + } + Thread.sleep(pollDelayMillis); + } + throw new OperatorException("Timeout of resource polling from cache for resource"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new OperatorException(e); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 1b5eefd7ff..1bd98c12d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -21,8 +21,7 @@ private UpdateControl(P resource, boolean patchResource, boolean patchStatus) { } /** - * Preferred way to update the status. It does not do optimistic locking. Uses JSON Patch to patch - * the resource. + * Preferred way to update the status. Uses JSON Patch to patch the resource. * *

Note that this does not work, if the {@link CustomResource#initStatus()} is implemented, * since it breaks the diffing process. Don't implement it if using this method. There is also an diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java index 8803c15b8c..d6a2971515 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java @@ -4,6 +4,7 @@ import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ConfiguredDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.DependentResourceNode; @SuppressWarnings({"rawtypes", "unchecked"}) public interface DependentResourceFactory< @@ -36,4 +37,13 @@ default Class associatedResourceType(D spec) { dependentResourceClass, DependentResource.class, null, null); return dr != null ? dr.resourceType() : null; } + + default DependentResourceNode createNodeFrom(D spec, DependentResource dependentResource) { + return new DependentResourceNode( + spec.getReconcileCondition(), + spec.getDeletePostCondition(), + spec.getReadyCondition(), + spec.getActivationCondition(), + dependentResource); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java index 2c337f3cd7..720ce0227c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java @@ -11,11 +11,8 @@ public interface InformerWrappingEventSourceHealthIndicator i.getStatus() != Status.HEALTHY) - .findAny(); - - return nonUp.isPresent() ? Status.UNHEALTHY : Status.HEALTHY; + var hasNonHealthy = + informerHealthIndicators().values().stream().anyMatch(i -> i.getStatus() != Status.HEALTHY); + return hasNonHealthy ? Status.UNHEALTHY : Status.HEALTHY; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java index d90b5e8918..6c0a5c95ba 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java @@ -101,9 +101,15 @@ public String apiVersion() { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - GroupVersionKind that = (GroupVersionKind) o; - return Objects.equals(apiVersion, that.apiVersion) && Objects.equals(kind, that.kind); + if (!(o instanceof GroupVersionKind that)) return false; + return Objects.equals(apiVersion, that.apiVersion) + && Objects.equals(kind, that.kind) + && specificEquals(that) + && that.specificEquals(this); + } + + protected boolean specificEquals(GroupVersionKind that) { + return true; } @Override @@ -113,13 +119,6 @@ public int hashCode() { @Override public String toString() { - return "GroupVersionKind{" - + "apiVersion='" - + apiVersion - + '\'' - + ", kind='" - + kind - + '\'' - + '}'; + return toGVKString(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java index 5cee9467f1..7f2674892f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; @@ -23,13 +24,22 @@ public abstract class AbstractEventSourceHolderDependentResource< private boolean isCacheFillerEventSource; protected String eventSourceNameToUse; + @SuppressWarnings("unchecked") + protected AbstractEventSourceHolderDependentResource() { + this(null, null); + } + protected AbstractEventSourceHolderDependentResource(Class resourceType) { this(resourceType, null); } protected AbstractEventSourceHolderDependentResource(Class resourceType, String name) { super(name); - this.resourceType = resourceType; + if (resourceType == null) { + this.resourceType = (Class) Utils.getTypeArgumentFromHierarchyByIndex(getClass(), 0); + } else { + this.resourceType = resourceType; + } } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java index 1148895709..4c828b7eb9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java @@ -21,6 +21,8 @@ public abstract class AbstractExternalDependentResource< private InformerEventSource externalStateEventSource; + protected AbstractExternalDependentResource() {} + @SuppressWarnings("unchecked") protected AbstractExternalDependentResource(Class resourceType) { super(resourceType); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java index 659b8b4720..3cf93cba53 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java @@ -16,6 +16,8 @@ public abstract class AbstractPollingDependentResource public static final Duration DEFAULT_POLLING_PERIOD = Duration.ofMillis(5000); private Duration pollingPeriod; + protected AbstractPollingDependentResource() {} + protected AbstractPollingDependentResource(Class resourceType) { this(resourceType, DEFAULT_POLLING_PERIOD); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java index 8cbe9f48d5..c0181207d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -14,6 +14,8 @@ public abstract class PerResourcePollingDependentResource implements PerResourcePollingEventSource.ResourceFetcher { + public PerResourcePollingDependentResource() {} + public PerResourcePollingDependentResource(Class resourceType) { super(resourceType); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java index afe4302fc3..392ac6d894 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java @@ -18,6 +18,8 @@ public abstract class CRUDKubernetesDependentResource implements Creator, Updater, GarbageCollected

{ + public CRUDKubernetesDependentResource() {} + public CRUDKubernetesDependentResource(Class resourceType) { super(resourceType); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java index 549f26437a..3b3c11b006 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java @@ -20,6 +20,8 @@ public class CRUDNoGCKubernetesDependentResource extends KubernetesDependentResource implements Creator, Updater, Deleter

{ + public CRUDNoGCKubernetesDependentResource() {} + public CRUDNoGCKubernetesDependentResource(Class resourceType) { super(resourceType); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java index f1dbb97cb4..3ed1fd5c4d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java @@ -15,8 +15,17 @@ public GenericKubernetesDependentResource(GroupVersionKind groupVersionKind) { this(GroupVersionKindPlural.from(groupVersionKind)); } + public GenericKubernetesDependentResource(GroupVersionKind groupVersionKind, String name) { + this(GroupVersionKindPlural.from(groupVersionKind), name); + } + public GenericKubernetesDependentResource(GroupVersionKindPlural groupVersionKind) { - super(GenericKubernetesResource.class); + super(GenericKubernetesResource.class, null); + this.groupVersionKind = groupVersionKind; + } + + public GenericKubernetesDependentResource(GroupVersionKindPlural groupVersionKind, String name) { + super(GenericKubernetesResource.class, name); this.groupVersionKind = groupVersionKind; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java index 641fea25b6..9771aba3dc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; +import java.util.Objects; import java.util.Optional; import io.fabric8.kubernetes.api.Pluralize; @@ -35,6 +36,24 @@ protected GroupVersionKindPlural(GroupVersionKind gvk, String plural) { : null)); } + @Override + protected boolean specificEquals(GroupVersionKind that) { + if (plural == null) { + return true; + } + return that instanceof GroupVersionKindPlural gvkp && gvkp.plural.equals(plural); + } + + @Override + public int hashCode() { + return plural != null ? Objects.hash(super.hashCode(), plural) : super.hashCode(); + } + + @Override + public String toString() { + return toGVKString() + (plural != null ? " (plural: " + plural + ")" : ""); + } + /** * Creates a new GroupVersionKindPlural from the specified {@link GroupVersionKind}. * diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index 4e32246a38..484ffb64c8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -27,5 +27,24 @@ boolean createResourceOnlyIfNotExistingWithSSA() default */ BooleanWithUndefined useSSA() default BooleanWithUndefined.UNDEFINED; + /** + * The underlying Informer event source configuration + * + * @return the {@link Informer} configuration + */ Informer informer() default @Informer; + + /** + * The specific matcher implementation to use when Server-Side Apply (SSA) is used, when case the + * default one isn't working appropriately. Typically, this could be needed to cover border cases + * with some Kubernetes resources that are modified by their controllers to normalize or add + * default values, which could result in infinite loops with the default matcher. Using a specific + * matcher could also be an optimization decision if determination of whether two resources match + * can be done faster than what can be done with the default exhaustive algorithm. + * + * @return the class of the specific matcher to use for the associated dependent resources + * @since 5.1 + */ + Class matcher() default + SSABasedGenericKubernetesResourceMatcher.class; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java index 6f1d7e3a64..7d68b0e106 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java @@ -22,10 +22,22 @@ public KubernetesDependentResourceConfig configFrom( DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA; Boolean useSSA = null; + SSABasedGenericKubernetesResourceMatcher matcher = + SSABasedGenericKubernetesResourceMatcher.getInstance(); if (configAnnotation != null) { createResourceOnlyIfNotExistingWithSSA = configAnnotation.createResourceOnlyIfNotExistingWithSSA(); useSSA = configAnnotation.useSSA().asBoolean(); + + // check if we have a specific matcher + Class> dependentResourceClass = + (Class>) spec.getDependentResourceClass(); + final var context = + Utils.contextFor( + controllerConfig, dependentResourceClass, configAnnotation.annotationType()); + matcher = + Utils.instantiate( + configAnnotation.matcher(), SSABasedGenericKubernetesResourceMatcher.class, context); } var informerConfiguration = @@ -35,7 +47,7 @@ public KubernetesDependentResourceConfig configFrom( controllerConfig); return new KubernetesDependentResourceConfig<>( - useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfiguration); + useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfiguration, matcher); } @SuppressWarnings({"unchecked"}) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 382ac7525c..69d145866d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -37,9 +37,13 @@ public abstract class KubernetesDependentResource> { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); + private final boolean garbageCollected = this instanceof GarbageCollected; private KubernetesDependentResourceConfig kubernetesDependentResourceConfig; private volatile Boolean useSSA; + private volatile Boolean usePreviousAnnotationForEventFiltering; + + public KubernetesDependentResource() {} public KubernetesDependentResource(Class resourceType) { this(resourceType, null); @@ -78,15 +82,17 @@ public R create(R desired, P primary, Context

context) { } public R update(R actual, R desired, P primary, Context

context) { + boolean useSSA = useSSA(context); if (log.isDebugEnabled()) { log.debug( - "Updating actual resource: {} version: {}", + "Updating actual resource: {} version: {}; SSA: {}", ResourceID.fromResource(actual), - actual.getMetadata().getResourceVersion()); + actual.getMetadata().getResourceVersion(), + useSSA); } R updatedResource; addMetadata(false, actual, desired, primary, context); - if (useSSA(context)) { + if (useSSA) { updatedResource = prepare(context, desired, primary, "Updating") .fieldManager(context.getControllerConfiguration().fieldManager()) @@ -112,7 +118,9 @@ public Result match(R actualResource, R desired, P primary, Context

contex addMetadata(true, actualResource, desired, primary, context); if (useSSA(context)) { matches = - SSABasedGenericKubernetesResourceMatcher.getInstance() + configuration() + .map(KubernetesDependentResourceConfig::matcher) + .orElse(SSABasedGenericKubernetesResourceMatcher.getInstance()) .matches(actualResource, desired, context); } else { matches = @@ -160,10 +168,19 @@ protected boolean useSSA(Context

context) { } private boolean usePreviousAnnotation(Context

context) { - return context - .getControllerConfiguration() - .getConfigurationService() - .previousAnnotationForDependentResourcesEventFiltering(); + if (usePreviousAnnotationForEventFiltering == null) { + usePreviousAnnotationForEventFiltering = + context + .getControllerConfiguration() + .getConfigurationService() + .previousAnnotationForDependentResourcesEventFiltering() + && !context + .getControllerConfiguration() + .getConfigurationService() + .withPreviousAnnotationForDependentResourcesBlocklist() + .contains(this.resourceType()); + } + return usePreviousAnnotationForEventFiltering; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java index c3424750d2..6f626d2628 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -10,14 +10,25 @@ public class KubernetesDependentResourceConfig { private final Boolean useSSA; private final boolean createResourceOnlyIfNotExistingWithSSA; private final InformerConfiguration informerConfig; + private final SSABasedGenericKubernetesResourceMatcher matcher; public KubernetesDependentResourceConfig( Boolean useSSA, boolean createResourceOnlyIfNotExistingWithSSA, InformerConfiguration informerConfig) { + this(useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfig, null); + } + + public KubernetesDependentResourceConfig( + Boolean useSSA, + boolean createResourceOnlyIfNotExistingWithSSA, + InformerConfiguration informerConfig, + SSABasedGenericKubernetesResourceMatcher matcher) { this.useSSA = useSSA; this.createResourceOnlyIfNotExistingWithSSA = createResourceOnlyIfNotExistingWithSSA; this.informerConfig = informerConfig; + this.matcher = + matcher != null ? matcher : SSABasedGenericKubernetesResourceMatcher.getInstance(); } public boolean createResourceOnlyIfNotExistingWithSSA() { @@ -31,4 +42,8 @@ public Boolean useSSA() { public InformerConfiguration informerConfig() { return informerConfig; } + + public SSABasedGenericKubernetesResourceMatcher matcher() { + return matcher; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java index 7694fe1d46..371fb700c3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java @@ -8,6 +8,7 @@ public final class KubernetesDependentResourceConfigBuilder informerConfiguration; + private SSABasedGenericKubernetesResourceMatcher matcher; public KubernetesDependentResourceConfigBuilder() {} @@ -29,8 +30,14 @@ public KubernetesDependentResourceConfigBuilder withKubernetesDependentInform return this; } + public KubernetesDependentResourceConfigBuilder withSSAMatcher( + SSABasedGenericKubernetesResourceMatcher matcher) { + this.matcher = matcher; + return this; + } + public KubernetesDependentResourceConfig build() { return new KubernetesDependentResourceConfig<>( - useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfiguration); + useSSA, createResourceOnlyIfNotExistingWithSSA, informerConfiguration, matcher); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java similarity index 56% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java index 7193085b63..fd1dcff49c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java @@ -2,32 +2,34 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.ResourceRequirements; /** - * Sanitizes the {@link ResourceRequirements} in the containers of a pair of {@link PodTemplateSpec} - * instances. + * Sanitizes the {@link ResourceRequirements} and the {@link EnvVar} in the containers of a pair of + * {@link PodTemplateSpec} instances. * *

When the sanitizer finds a mismatch in the structure of the given templates, before it gets to - * the nested resource limits and requests, it returns early without fixing the actual map. This is - * an optimization because the given templates will anyway differ at this point. This means we do - * not have to attempt to sanitize the resources for these use cases, since there will anyway be an - * update of the K8s resource. + * the nested fields, it returns early without fixing the actual map. This is an optimization + * because the given templates will anyway differ at this point. This means we do not have to + * attempt to sanitize the fields for these use cases, since there will anyway be an update of the + * K8s resource. * *

The algorithm traverses the whole template structure because we need the actual and desired - * {@link Quantity} instances to compare their numerical amount. Using the {@link + * {@link Quantity} and {@link EnvVar} instances. Using the {@link * GenericKubernetesResource#get(Map, Object...)} shortcut would need to create new instances just * for the sanitization check. */ -class ResourceRequirementsSanitizer { +class PodTemplateSpecSanitizer { - static void sanitizeResourceRequirements( + static void sanitizePodTemplateSpec( final Map actualMap, final PodTemplateSpec actualTemplate, final PodTemplateSpec desiredTemplate) { @@ -37,19 +39,19 @@ static void sanitizeResourceRequirements( if (actualTemplate.getSpec() == null || desiredTemplate.getSpec() == null) { return; } - sanitizeResourceRequirements( + sanitizePodTemplateSpec( actualMap, actualTemplate.getSpec().getInitContainers(), desiredTemplate.getSpec().getInitContainers(), "initContainers"); - sanitizeResourceRequirements( + sanitizePodTemplateSpec( actualMap, actualTemplate.getSpec().getContainers(), desiredTemplate.getSpec().getContainers(), "containers"); } - private static void sanitizeResourceRequirements( + private static void sanitizePodTemplateSpec( final Map actualMap, final List actualContainers, final List desiredContainers, @@ -57,11 +59,17 @@ private static void sanitizeResourceRequirements( int containers = desiredContainers.size(); if (containers == actualContainers.size()) { for (int containerIndex = 0; containerIndex < containers; containerIndex++) { - var desiredContainer = desiredContainers.get(containerIndex); - var actualContainer = actualContainers.get(containerIndex); + final var desiredContainer = desiredContainers.get(containerIndex); + final var actualContainer = actualContainers.get(containerIndex); if (!desiredContainer.getName().equals(actualContainer.getName())) { return; } + sanitizeEnvVars( + actualMap, + actualContainer.getEnv(), + desiredContainer.getEnv(), + containerPath, + containerIndex); sanitizeResourceRequirements( actualMap, actualContainer.getResources(), @@ -116,12 +124,11 @@ private static void sanitizeQuantities( "resources", quantityPath)) .map(Map.class::cast) - .filter(m -> m.size() == desiredResource.size()) .ifPresent( m -> actualResource.forEach( (key, actualQuantity) -> { - var desiredQuantity = desiredResource.get(key); + final var desiredQuantity = desiredResource.get(key); if (desiredQuantity == null) { return; } @@ -138,4 +145,53 @@ private static void sanitizeQuantities( } })); } + + @SuppressWarnings("unchecked") + private static void sanitizeEnvVars( + final Map actualMap, + final List actualEnvVars, + final List desiredEnvVars, + final String containerPath, + final int containerIndex) { + if (desiredEnvVars.isEmpty() || actualEnvVars.isEmpty()) { + return; + } + Optional.ofNullable( + GenericKubernetesResource.get( + actualMap, "spec", "template", "spec", containerPath, containerIndex, "env")) + .map(List.class::cast) + .ifPresent( + envVars -> + actualEnvVars.forEach( + actualEnvVar -> { + final var actualEnvVarName = actualEnvVar.getName(); + final var actualEnvVarValue = actualEnvVar.getValue(); + // check if the actual EnvVar value string is not null or the desired EnvVar + // already contains the same EnvVar name with a non empty EnvVar value + final var isDesiredEnvVarEmpty = + hasEnvVarNoEmptyValue(actualEnvVarName, desiredEnvVars); + if (actualEnvVarValue != null || isDesiredEnvVarEmpty) { + return; + } + envVars.stream() + .filter( + envVar -> + ((Map) envVar) + .get("name") + .equals(actualEnvVarName)) + // add the actual EnvVar value with an empty string to prevent a + // resource update + .forEach(envVar -> ((Map) envVar).put("value", "")); + })); + } + + private static boolean hasEnvVarNoEmptyValue( + final String envVarName, final List envVars) { + return envVars.stream() + .anyMatch( + envVar -> + Objects.equals(envVarName, envVar.getName()) + && envVar.getValue() != null + && !envVar.getValue().isEmpty()); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java deleted file mode 100644 index d893ff3e86..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface ResourceUpdaterMatcher { - - R updateResource(R actual, R desired, Context context); - - boolean matches(R actual, R desired, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java index 1868c2872d..4954dfd17a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java @@ -31,7 +31,7 @@ import com.github.difflib.DiffUtils; import com.github.difflib.UnifiedDiffUtils; -import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceRequirementsSanitizer.sanitizeResourceRequirements; +import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.PodTemplateSpecSanitizer.sanitizePodTemplateSpec; /** * Matches the actual state on the server vs the desired state. Based on the managedFields of SSA. @@ -55,18 +55,6 @@ public class SSABasedGenericKubernetesResourceMatcher { public static final String APPLY_OPERATION = "Apply"; public static final String DOT_KEY = "."; - @SuppressWarnings("rawtypes") - private static final SSABasedGenericKubernetesResourceMatcher INSTANCE = - new SSABasedGenericKubernetesResourceMatcher<>(); - - private static final List IGNORED_METADATA = - List.of("creationTimestamp", "deletionTimestamp", "generation", "selfLink", "uid"); - - @SuppressWarnings("unchecked") - public static SSABasedGenericKubernetesResourceMatcher getInstance() { - return INSTANCE; - } - private static final String F_PREFIX = "f:"; private static final String K_PREFIX = "k:"; private static final String V_PREFIX = "v:"; @@ -76,9 +64,21 @@ public static SSABasedGenericKubernetesResourceMatcher(); + + private static final List IGNORED_METADATA = + List.of("creationTimestamp", "deletionTimestamp", "generation", "selfLink", "uid"); + private static final Logger log = LoggerFactory.getLogger(SSABasedGenericKubernetesResourceMatcher.class); + @SuppressWarnings("unchecked") + public static SSABasedGenericKubernetesResourceMatcher getInstance() { + return INSTANCE; + } + @SuppressWarnings("unchecked") public boolean matches(R actual, R desired, Context context) { var optionalManagedFieldsEntry = @@ -110,7 +110,7 @@ public boolean matches(R actual, R desired, Context context) { removeIrrelevantValues(desiredMap); - var matches = prunedActual.equals(desiredMap); + var matches = matches(prunedActual, desiredMap, actual, desired, context); if (!matches && log.isDebugEnabled() && LoggingUtils.isNotSensitiveResource(desired)) { var diff = getDiff(prunedActual, desiredMap, objectMapper); log.debug( @@ -125,58 +125,61 @@ public boolean matches(R actual, R desired, Context context) { return matches; } - private String getDiff( - Map prunedActualMap, + /** + * Compares the desired and actual resources for equality. + * + *

This method can be overridden to implement custom matching logic. The {@code actualMap} is a + * cleaned-up version of the actual resource with managed fields and irrelevant values removed. + * + * @param actualMap the actual resource represented as a map + * @param desiredMap the desired resource represented as a map + * @param actual the actual resource object + * @param desired the desired resource object + * @param context the current matching context + * @return {@code true} if the resources are equal, otherwise {@code false} + */ + protected boolean matches( + Map actualMap, Map desiredMap, - KubernetesSerialization serialization) { - var actualYaml = serialization.asYaml(sortMap(prunedActualMap)); - var desiredYaml = serialization.asYaml(sortMap(desiredMap)); - if (log.isTraceEnabled()) { - log.trace("Pruned actual resource:\n {} \ndesired resource:\n {} ", actualYaml, desiredYaml); - } - - var patch = DiffUtils.diff(actualYaml.lines().toList(), desiredYaml.lines().toList()); - var unifiedDiff = - UnifiedDiffUtils.generateUnifiedDiff("", "", actualYaml.lines().toList(), patch, 1); - return String.join("\n", unifiedDiff); + R actual, + R desired, + Context context) { + return actualMap.equals(desiredMap); } - @SuppressWarnings("unchecked") - Map sortMap(Map map) { - var sortedKeys = new ArrayList<>(map.keySet()); - Collections.sort(sortedKeys); - - var sortedMap = new LinkedHashMap(); - for (var key : sortedKeys) { - var value = map.get(key); - if (value instanceof Map) { - sortedMap.put(key, sortMap((Map) value)); - } else if (value instanceof List) { - sortedMap.put(key, sortListItems((List) value)); - } else { - sortedMap.put(key, value); - } + private Optional checkIfFieldManagerExists(R actual, String fieldManager) { + var targetManagedFields = + actual.getMetadata().getManagedFields().stream() + // Only the apply operations are interesting for us since those were created properly be + // SSA patch. An update can be present with same fieldManager when migrating and having + // the same field manager name + .filter( + f -> + f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) + .toList(); + if (targetManagedFields.isEmpty()) { + log.debug( + "No field manager exists for resource: {} with name: {} and operation {}", + actual.getKind(), + actual.getMetadata().getName(), + APPLY_OPERATION); + return Optional.empty(); } - return sortedMap; - } - - @SuppressWarnings("unchecked") - List sortListItems(List list) { - var sortedList = new ArrayList<>(); - for (var item : list) { - if (item instanceof Map) { - sortedList.add(sortMap((Map) item)); - } else if (item instanceof List) { - sortedList.add(sortListItems((List) item)); - } else { - sortedList.add(item); - } + // this should not happen in theory + if (targetManagedFields.size() > 1) { + throw new OperatorException( + "More than one field manager exists with name: " + + fieldManager + + " in resource: " + + actual.getKind() + + " with name: " + + actual.getMetadata().getName()); } - return sortedList; + return Optional.of(targetManagedFields.get(0)); } /** Correct for known issue with SSA */ - private void sanitizeState(R actual, R desired, Map actualMap) { + protected void sanitizeState(R actual, R desired, Map actualMap) { if (actual instanceof StatefulSet actualStatefulSet && desired instanceof StatefulSet desiredStatefulSet) { var actualSpec = actualStatefulSet.getSpec(); @@ -200,22 +203,22 @@ private void sanitizeState(R actual, R desired, Map actualMap) { } } } - sanitizeResourceRequirements(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate()); + sanitizePodTemplateSpec(actualMap, actualSpec.getTemplate(), desiredSpec.getTemplate()); } else if (actual instanceof Deployment actualDeployment && desired instanceof Deployment desiredDeployment) { - sanitizeResourceRequirements( + sanitizePodTemplateSpec( actualMap, actualDeployment.getSpec().getTemplate(), desiredDeployment.getSpec().getTemplate()); } else if (actual instanceof ReplicaSet actualReplicaSet && desired instanceof ReplicaSet desiredReplicaSet) { - sanitizeResourceRequirements( + sanitizePodTemplateSpec( actualMap, actualReplicaSet.getSpec().getTemplate(), desiredReplicaSet.getSpec().getTemplate()); } else if (actual instanceof DaemonSet actualDaemonSet && desired instanceof DaemonSet desiredDaemonSet) { - sanitizeResourceRequirements( + sanitizePodTemplateSpec( actualMap, actualDaemonSet.getSpec().getTemplate(), desiredDaemonSet.getSpec().getTemplate()); @@ -223,20 +226,7 @@ private void sanitizeState(R actual, R desired, Map actualMap) { } @SuppressWarnings("unchecked") - private static void removeIrrelevantValues(Map desiredMap) { - var metadata = (Map) desiredMap.get(METADATA_KEY); - metadata.remove(NAME_KEY); - metadata.remove(NAMESPACE_KEY); - IGNORED_METADATA.forEach(metadata::remove); - if (metadata.isEmpty()) { - desiredMap.remove(METADATA_KEY); - } - desiredMap.remove(KIND_KEY); - desiredMap.remove(API_VERSION_KEY); - } - - @SuppressWarnings("unchecked") - private static void keepOnlyManagedFields( + static void keepOnlyManagedFields( Map result, Map actualMap, Map managedFields, @@ -270,7 +260,7 @@ private static void keepOnlyManagedFields( } } else { // this should handle the case when the value is complex in the actual map (not just a - // simple value). + // simple value) result.put(keyInActual, actualMap.get(keyInActual)); } } else { @@ -282,30 +272,33 @@ private static void keepOnlyManagedFields( } } - @SuppressWarnings("unchecked") - private static void fillResultsAndTraverseFurther( - Map result, - Map actualMap, - Map managedFields, - KubernetesSerialization objectMapper, - String key, - String keyInActual, - Object managedFieldValue) { - var emptyMapValue = new HashMap(); - result.put(keyInActual, emptyMapValue); - var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); - log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue); - keepOnlyManagedFields( - emptyMapValue, - (Map) actualMapValue, - (Map) managedFields.get(key), - objectMapper); - } - private static boolean isNestedValue(Map managedFieldValue) { return !managedFieldValue.isEmpty(); } + private static boolean isListKeyEntrySet(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); + } + + private static boolean isSetValueField(Set> managedEntrySet) { + return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); + } + + /** + * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that + * those are added when there are more subfields of a referenced field. See test samples. Does not + * seem to provide additional functionality, so can be just skipped for now. + */ + private static boolean isKeyPrefixedSkippingDotKey( + Set> managedEntrySet, String prefix) { + var iterator = managedEntrySet.iterator(); + var managedFieldEntry = iterator.next(); + if (managedFieldEntry.getKey().equals(DOT_KEY)) { + managedFieldEntry = iterator.next(); + } + return managedFieldEntry.getKey().startsWith(prefix); + } + /** * List entries referenced by key, or when "k:" prefix is used. It works in a way that it selects * the target element based on the field(s) in "k:" for example when there is a list of element of @@ -350,6 +343,36 @@ private static void handleListKeyEntrySet( }); } + @SuppressWarnings("unchecked") + private static Map.Entry> selectListEntryBasedOnKey( + String key, List> values, KubernetesSerialization objectMapper) { + Map ids = objectMapper.unmarshal(key, Map.class); + var possibleTargets = new ArrayList>(1); + int lastIndex = -1; + for (int i = 0; i < values.size(); i++) { + var value = values.get(i); + if (value.entrySet().containsAll(ids.entrySet())) { + possibleTargets.add(value); + lastIndex = i; + } + } + if (possibleTargets.isEmpty()) { + throw new IllegalStateException( + "Cannot find list element for key: " + + key + + " in map: " + + values.stream().map(Map::keySet).toList()); + } + if (possibleTargets.size() > 1) { + throw new IllegalStateException( + "More targets found in list element for key: " + + key + + " in map: " + + values.stream().map(Map::keySet).toList()); + } + return new AbstractMap.SimpleEntry<>(lastIndex, possibleTargets.get(0)); + } + /** * Set values, the {@code "v:"} prefix. Form in managed fields: {@code * "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}}. @@ -385,90 +408,87 @@ public static Object parseKeyValue( return objectMapper.unmarshal(stringValue.trim(), type); } - private static boolean isSetValueField(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, V_PREFIX); + @SuppressWarnings("unchecked") + private static void fillResultsAndTraverseFurther( + Map result, + Map actualMap, + Map managedFields, + KubernetesSerialization objectMapper, + String key, + String keyInActual, + Object managedFieldValue) { + var emptyMapValue = new HashMap(); + result.put(keyInActual, emptyMapValue); + var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap()); + log.debug("key: {} actual map value: managedFieldValue: {}", keyInActual, managedFieldValue); + keepOnlyManagedFields( + emptyMapValue, + (Map) actualMapValue, + (Map) managedFields.get(key), + objectMapper); } - private static boolean isListKeyEntrySet(Set> managedEntrySet) { - return isKeyPrefixedSkippingDotKey(managedEntrySet, K_PREFIX); + @SuppressWarnings("unchecked") + private static void removeIrrelevantValues(Map desiredMap) { + var metadata = (Map) desiredMap.get(METADATA_KEY); + metadata.remove(NAME_KEY); + metadata.remove(NAMESPACE_KEY); + IGNORED_METADATA.forEach(metadata::remove); + if (metadata.isEmpty()) { + desiredMap.remove(METADATA_KEY); + } + desiredMap.remove(KIND_KEY); + desiredMap.remove(API_VERSION_KEY); } - /** - * Sometimes (not always) the first subfield of a managed field ("f:") is ".:{}", it looks that - * those are added when there are more subfields of a referenced field. See test samples. Does not - * seem to provide additional functionality, so can be just skipped for now. - */ - private static boolean isKeyPrefixedSkippingDotKey( - Set> managedEntrySet, String prefix) { - var iterator = managedEntrySet.iterator(); - var managedFieldEntry = iterator.next(); - if (managedFieldEntry.getKey().equals(DOT_KEY)) { - managedFieldEntry = iterator.next(); + private static String getDiff( + Map prunedActualMap, + Map desiredMap, + KubernetesSerialization serialization) { + var actualYaml = serialization.asYaml(sortMap(prunedActualMap)); + var desiredYaml = serialization.asYaml(sortMap(desiredMap)); + if (log.isTraceEnabled()) { + log.trace("Pruned actual resource:\n {} \ndesired resource:\n {} ", actualYaml, desiredYaml); } - return managedFieldEntry.getKey().startsWith(prefix); + + var patch = DiffUtils.diff(actualYaml.lines().toList(), desiredYaml.lines().toList()); + var unifiedDiff = + UnifiedDiffUtils.generateUnifiedDiff("", "", actualYaml.lines().toList(), patch, 1); + return String.join("\n", unifiedDiff); } @SuppressWarnings("unchecked") - private static Map.Entry> selectListEntryBasedOnKey( - String key, List> values, KubernetesSerialization objectMapper) { - Map ids = objectMapper.unmarshal(key, Map.class); - var possibleTargets = new ArrayList>(1); - int lastIndex = -1; - for (int i = 0; i < values.size(); i++) { - var value = values.get(i); - if (value.entrySet().containsAll(ids.entrySet())) { - possibleTargets.add(value); - lastIndex = i; + static Map sortMap(Map map) { + var sortedKeys = new ArrayList<>(map.keySet()); + Collections.sort(sortedKeys); + + var sortedMap = new LinkedHashMap(); + for (var key : sortedKeys) { + var value = map.get(key); + if (value instanceof Map) { + sortedMap.put(key, sortMap((Map) value)); + } else if (value instanceof List) { + sortedMap.put(key, sortListItems((List) value)); + } else { + sortedMap.put(key, value); } } - if (possibleTargets.isEmpty()) { - throw new IllegalStateException( - "Cannot find list element for key: " - + key - + " in map: " - + values.stream().map(Map::keySet).toList()); - } - if (possibleTargets.size() > 1) { - throw new IllegalStateException( - "More targets found in list element for key: " - + key - + " in map: " - + values.stream().map(Map::keySet).toList()); - } - return new AbstractMap.SimpleEntry<>(lastIndex, possibleTargets.get(0)); + return sortedMap; } - private Optional checkIfFieldManagerExists(R actual, String fieldManager) { - var targetManagedFields = - actual.getMetadata().getManagedFields().stream() - // Only the apply operations are interesting for us since those were created properly be - // SSA - // Patch. An update can be present with same fieldManager when migrating and having the - // same - // field manager name. - .filter( - f -> - f.getManager().equals(fieldManager) && f.getOperation().equals(APPLY_OPERATION)) - .toList(); - if (targetManagedFields.isEmpty()) { - log.debug( - "No field manager exists for resource: {} with name: {} and operation {}", - actual.getKind(), - actual.getMetadata().getName(), - APPLY_OPERATION); - return Optional.empty(); - } - // this should not happen in theory - if (targetManagedFields.size() > 1) { - throw new OperatorException( - "More than one field manager exists with name: " - + fieldManager - + " in resource: " - + actual.getKind() - + " with name: " - + actual.getMetadata().getName()); + @SuppressWarnings("unchecked") + static List sortListItems(List list) { + var sortedList = new ArrayList<>(); + for (var item : list) { + if (item instanceof Map) { + sortedList.add(sortMap((Map) item)); + } else if (item instanceof List) { + sortedList.add(sortListItems((List) item)); + } else { + sortedList.add(item); + } } - return Optional.of(targetManagedFields.get(0)); + return sortedList; } private static String keyWithoutPrefix(String key) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java index 6629ed8f62..447f89ab30 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java @@ -111,8 +111,9 @@ protected void markAsExecuting( actualExecutions.put(dependentResourceNode, future); } + // Exception is required because of Kotlin protected synchronized void handleExceptionInExecutor( - DependentResourceNode dependentResourceNode, RuntimeException e) { + DependentResourceNode dependentResourceNode, Exception e) { createOrGetResultFor(dependentResourceNode).withError(e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java index 587c7fbdc8..ed02ef8f4e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -77,12 +77,10 @@ public Workflow

resolve(KubernetesClient client, ControllerConfiguration

c for (DependentResourceSpec spec : orderedSpecs) { final var dependentResource = resolve(spec, client, configuration); final var node = - new DependentResourceNode( - spec.getReconcileCondition(), - spec.getDeletePostCondition(), - spec.getReadyCondition(), - spec.getActivationCondition(), - dependentResource); + configuration + .getConfigurationService() + .dependentResourceFactory() + .createNodeFrom(spec, dependentResource); alreadyResolved.put(dependentResource.name(), node); spec.getDependsOn().forEach(depend -> node.addDependsOnRelation(alreadyResolved.get(depend))); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java index 87646a56d9..c456b44ef2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java @@ -8,7 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @SuppressWarnings("rawtypes") -class DependentResourceNode { +public class DependentResourceNode { private final List dependsOn = new LinkedList<>(); private final List parents = new LinkedList<>(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/KubernetesResourceDeletedCondition.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/KubernetesResourceDeletedCondition.java index 4137ac9519..3cf464d46b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/KubernetesResourceDeletedCondition.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/KubernetesResourceDeletedCondition.java @@ -4,13 +4,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -/** - * A condition implementation meant to be used as a delete post-condition on Kubernetes dependent +/* A condition implementation meant to be used as a delete post-condition on Kubernetes dependent * resources to prevent the workflow from proceeding until the associated resource is actually - * deleted from the server (or, at least, doesn't have any finalizers anymore). This is needed in - * cases where a cleaning process depends on resources being actually removed from the server - * because, by default, workflows simply request the deletion but do NOT wait for the resources to - * be actually deleted. + * deleted from the server. */ public class KubernetesResourceDeletedCondition implements Condition { @@ -20,10 +16,6 @@ public boolean isMet( HasMetadata primary, Context context) { var optionalResource = dependentResource.getSecondaryResource(primary, context); - if (optionalResource.isEmpty()) { - return true; - } else { - return optionalResource.orElseThrow().getMetadata().getFinalizers().isEmpty(); - } + return optionalResource.isEmpty(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java index 740d10710d..4efadff05f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java @@ -19,7 +19,8 @@ public void run() { try { doRun(dependentResourceNode); - } catch (RuntimeException e) { + } catch (Exception e) { + // Exception is required because of Kotlin workflowExecutor.handleExceptionInExecutor(dependentResourceNode, e); } finally { workflowExecutor.handleNodeExecutionFinish(dependentResourceNode); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index 065e790ba4..9e29305b51 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -144,9 +144,11 @@ protected void doRun(DependentResourceNode dependentResourceNode) { log.debug("Reconciling for primary: {} node: {} ", primaryID, dependentResourceNode); ReconcileResult reconcileResult = dependentResource.reconcile(primary, context); final var detailBuilder = createOrGetResultFor(dependentResourceNode); - detailBuilder.withReconcileResult(reconcileResult).markAsVisited(); - if (isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode)) { + boolean isReadyPostconditionMet = + isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode); + detailBuilder.withReconcileResult(reconcileResult).markAsVisited(); + if (isReadyPostconditionMet) { log.debug( "Setting already reconciled for: {} primaryID: {}", dependentResourceNode, primaryID); handleDependentsReconcile(dependentResourceNode); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 566996af0e..e029e287a0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -102,8 +102,16 @@ public synchronized void handleEvent(Event event) { try { log.debug("Received event: {}", event); + final var optionalState = resourceStateManager.getOrCreateOnResourceEvent(event); + if (optionalState.isEmpty()) { + log.debug( + "Skipping event, since no state present and it is not a resource event. Resource ID:" + + " {}", + event.getRelatedCustomResourceID()); + return; + } + var state = optionalState.orElseThrow(); final var resourceID = event.getRelatedCustomResourceID(); - final var state = resourceStateManager.getOrCreate(event.getRelatedCustomResourceID()); MDCUtils.addResourceIDInfo(resourceID); metrics.receivedEvent(event, metricsMetadata); handleEventMarking(event, state); @@ -196,7 +204,7 @@ private void handleEventMarking(Event event, ResourceState state) { // event as below. markEventReceived(state); } - } else if (!state.deleteEventPresent() || !state.processedMarkForDeletionPresent()) { + } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { markEventReceived(state); } else if (log.isDebugEnabled()) { log.debug( @@ -349,21 +357,29 @@ private void retryAwareErrorLogging( boolean eventPresent, Exception exception, ExecutionScope

executionScope) { - if (!eventPresent - && !retry.isLastAttempt() - && exception instanceof KubernetesClientException ex) { - if (ex.getCode() == HttpURLConnection.HTTP_CONFLICT) { - log.debug( - "Full client conflict error during event processing {}", executionScope, exception); - log.warn( - "Resource Kubernetes Resource Creator/Update Conflict during reconciliation. Message:" - + " {} Resource name: {}", - ex.getMessage(), - ex.getFullResourceName()); - return; - } + if (!retry.isLastAttempt() + && exception instanceof KubernetesClientException ex + && ex.getCode() == HttpURLConnection.HTTP_CONFLICT) { + log.debug("Full client conflict error during event processing {}", executionScope, exception); + log.info( + "Resource Kubernetes Resource Creator/Update Conflict during reconciliation. Message:" + + " {} Resource name: {}", + ex.getMessage(), + ex.getFullResourceName()); + } else if (eventPresent || !retry.isLastAttempt()) { + log.warn( + "Uncaught error during event processing {} - but another reconciliation will be attempted" + + " because a superseding event has been received or another retry attempt is" + + " pending.", + executionScope, + exception); + } else { + log.error( + "Uncaught error during event processing {} - no superseding event is present and this is" + + " the retry last attempt", + executionScope, + exception); } - log.error("Error during event processing {}", executionScope, exception); } private void cleanupOnSuccessfulExecution(ExecutionScope

executionScope) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 02b91f6dd0..8b07bf110b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -208,6 +208,7 @@ public Stream> getEventSourcesStream() { return eventSources.flatMappedSources(); } + @Override public ControllerEventSource

getControllerEventSource() { return eventSources.controllerEventSource(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java index 066a7f5808..c5a219a026 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; public interface EventSourceRetriever

{ @@ -17,6 +18,8 @@ default EventSource getEventSourceFor(Class dependentType) { List> getEventSourcesFor(Class dependentType); + ControllerEventSource

getControllerEventSource(); + /** * Registers (and starts) the specified {@link EventSource} dynamically during the reconciliation. * If an EventSource is already registered with the specified name, the registration will be diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/NamedEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/NamedEventSource.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index ee861982b1..90bdc93979 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -1,10 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; @@ -16,6 +18,7 @@ import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; @@ -180,10 +183,8 @@ private PostExecutionControl

reconcileExecution( return createPostExecutionControl(updatedCustomResource, updateControl); } - @SuppressWarnings("unchecked") private PostExecutionControl

handleErrorStatusHandler( P resource, P originalResource, Context

context, Exception e) throws Exception { - RetryInfo retryInfo = context .getRetryInfo() @@ -212,9 +213,41 @@ public boolean isLastAttempt() { P updatedResource = null; if (errorStatusUpdateControl.getResource().isPresent()) { - updatedResource = - customResourceFacade.patchStatus( - errorStatusUpdateControl.getResource().orElseThrow(), originalResource); + try { + updatedResource = + customResourceFacade.patchStatus( + errorStatusUpdateControl.getResource().orElseThrow(), originalResource); + } catch (Exception ex) { + int code = ex instanceof KubernetesClientException kcex ? kcex.getCode() : -1; + Level exceptionLevel = Level.ERROR; + String failedMessage = ""; + if (context.isNextReconciliationImminent() + || !(errorStatusUpdateControl.isNoRetry() || retryInfo.isLastAttempt())) { + if (code == HttpURLConnection.HTTP_CONFLICT + || (originalResource.getMetadata().getResourceVersion() != null && code == 422)) { + exceptionLevel = Level.DEBUG; + failedMessage = " due to conflict"; + log.info( + "ErrorStatusUpdateControl.patchStatus of {} failed due to a conflict, but the next" + + " reconciliation is imminent.", + ResourceID.fromResource(originalResource)); + } else { + exceptionLevel = Level.WARN; + failedMessage = ", but will be retried soon,"; + } + } + + log.atLevel(exceptionLevel) + .log( + "ErrorStatusUpdateControl.patchStatus failed{} for {} with UID: {} and version: {}" + + " for error {}", + failedMessage, + ResourceID.fromResource(originalResource), + getUID(resource), + getVersion(resource), + e.getMessage(), + ex); + } } if (errorStatusUpdateControl.isNoRetry()) { PostExecutionControl

postExecutionControl; @@ -334,7 +367,11 @@ private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalRe } private P patchResource(P resource, P originalResource) { - log.debug("Updating resource: {} with version: {}", getUID(resource), getVersion(resource)); + log.debug( + "Updating resource: {} with version: {}; SSA: {}", + getUID(resource), + getVersion(resource), + useSSA); log.trace("Resource before update: {}", resource); final var finalizerName = configuration().getFinalizerName(); @@ -372,8 +409,13 @@ public P conflictRetryingPatch( } catch (KubernetesClientException e) { log.trace("Exception during patch for resource: {}", resource); retryIndex++; - // only retry on conflict (HTTP 409), otherwise fail - if (e.getCode() != 409) { + // only retry on conflict (409) and unprocessable content (422) which + // can happen if JSON Patch is not a valid request since there was + // a concurrent request which already removed another finalizer: + // List element removal from a list is by index in JSON Patch + // so if addressing a second finalizer but first is meanwhile removed + // it is a wrong request. + if (e.getCode() != 409 && e.getCode() != 422) { throw e; } if (retryIndex >= MAX_UPDATE_RETRY) { @@ -383,6 +425,11 @@ public P conflictRetryingPatch( + ") retry attempts to patch resource: " + ResourceID.fromResource(resource)); } + log.debug( + "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", + resource.getMetadata().getName(), + resource.getMetadata().getNamespace(), + e.getCode()); resource = customResourceFacade.getResource( resource.getMetadata().getNamespace(), resource.getMetadata().getName()); @@ -465,7 +512,11 @@ private R editStatus(R resource, R originalResource) { clonedOriginal.getMetadata().setResourceVersion(null); resource.getMetadata().setResourceVersion(null); var res = resource(clonedOriginal); - return res.editStatus(r -> resource); + return res.editStatus( + r -> { + ReconcilerUtils.setStatus(r, ReconcilerUtils.getStatus(resource)); + return r; + }); } finally { // restore initial resource version clonedOriginal.getMetadata().setResourceVersion(resourceVersion); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java index 6932e1ca5e..481fd317ff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java @@ -2,15 +2,33 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; + class ResourceStateManager { // maybe we should have a way for users to specify a hint on the amount of CRs their reconciler // will process to avoid under- or over-sizing the state maps and avoid too many resizing that // take time and memory? private final Map states = new ConcurrentHashMap<>(100); + public Optional getOrCreateOnResourceEvent(Event event) { + var resourceId = event.getRelatedCustomResourceID(); + var state = states.get(event.getRelatedCustomResourceID()); + if (state != null) { + return Optional.of(state); + } + if (event instanceof ResourceEvent) { + state = new ResourceState(resourceId); + states.put(resourceId, state); + return Optional.of(state); + } else { + return Optional.empty(); + } + } + public ResourceState getOrCreate(ResourceID resourceID) { return states.computeIfAbsent(resourceID, ResourceState::new); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java index 7b8853b4ae..328f3854bd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java @@ -4,7 +4,16 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; +/** + * Maps secondary resource to primary resources. + * + * @param secondary resource type + */ @FunctionalInterface public interface SecondaryToPrimaryMapper { + /** + * @param resource - secondary + * @return set of primary resource IDs + */ Set toPrimaryResourceIDs(R resource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 688a88ae22..c029a54170 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -20,50 +21,45 @@ import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; /** - * Wraps informer(s) so it is connected to the eventing system of the framework. Note that since - * it's it is built on top of Informers, it also support caching resources using caching from - * fabric8 client Informer caches and additional caches described below. + * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since + * this is built on top of Fabric8 client Informers, it also supports caching resources using + * caching from informer caches as well as additional caches described below. * *

InformerEventSource also supports two features to better handle events and caching of - * resources on top of Informers from fabric8 Kubernetes client. These two features implementation - * wise are related to each other:
+ * resources on top of Informers from the Fabric8 Kubernetes client. These two features are related + * to each other as follows: * - *

1. API that allows to make sure the cache contains the fresh resource after an update. This is - * important for {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and - * mainly for {@link - * io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} so after - * reconcile if getResource() called always return the fresh resource. To achieve this - * handleRecentResourceUpdate() and handleRecentResourceCreate() needs to be called explicitly after - * resource created/updated using the kubernetes client. (These calls are done automatically by - * KubernetesDependentResource implementation.). In the background this will store the new resource - * in a temporary cache {@link TemporaryResourceCache} which do additional checks. After a new event - * is received the cachec object is removed from this cache, since in general then it is already in - * the cache of informer.
+ *

    + *
  1. Ensuring the cache contains the fresh resource after an update. This is important for + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and mainly + * for {@link + * io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} so + * that {@link + * io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource#getSecondaryResource(HasMetadata, + * Context)} always returns the latest version of the resource after a reconciliation. To + * achieve this {@link #handleRecentResourceUpdate(ResourceID, HasMetadata, HasMetadata)} and + * {@link #handleRecentResourceCreate(ResourceID, HasMetadata)} need to be called explicitly + * after a resource is created or updated using the kubernetes client. These calls are done + * automatically by the KubernetesDependentResource implementation. In the background this + * will store the new resource in a temporary cache {@link TemporaryResourceCache} which does + * additional checks. After a new event is received the cached object is removed from this + * cache, since it is then usually already in the informer cache. + *
  2. Avoiding unneeded reconciliations after resources are created or updated. This filters out + * events that are the results of updates and creates made by the controller itself because we + * typically don't want the associated informer to trigger an event causing a useless + * reconciliation (as the change originates from the reconciler itself). For the details see + * {@link #canSkipEvent(HasMetadata, HasMetadata, ResourceID)} and related usage. + *
* - *

2. Additional API is provided that is meant to be used with the combination of the previous - * one, and the goal is to filter out events that are the results of updates and creates made by the - * controller itself. For example if in reconciler a ConfigMaps is created, there should be an - * Informer in place to handle change events of that ConfigMap, but since it has bean created (or - * updated) by the reconciler this should not trigger an additional reconciliation by default. In - * order to achieve this prepareForCreateOrUpdateEventFiltering(..) method needs to be called before - * the operation of the k8s client. And the operation from point 1. after the k8s client call. See - * it's usage in CreateUpdateEventFilterTestReconciler integration test for the usage. (Again this - * is managed for the developer if using dependent resources.)
- * Roughly it works in a way that before the K8S API call is made, we set mark the resource ID, and - * from that point informer won't propagate events further just will start record them. After the - * client operation is done, it's checked and analysed what events were received and based on that - * it will propagate event or not and/or put the new resource into the temporal cache - so if the - * event not arrived yet about the update will be able to filter it in the future. - * - * @param resource type watching - * @param

type of the primary resource + * @param resource type being watched + * @param

type of the associated primary resource */ public class InformerEventSource extends ManagedInformerEventSource> implements ResourceEventHandler { - private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); public static final String PREVIOUS_ANNOTATION_KEY = "javaoperatorsdk.io/previous"; + private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); // we need direct control for the indexer to propagate the just update resource also to the index private final PrimaryToSecondaryIndex primaryToSecondaryIndex; private final PrimaryToSecondaryMapper

primaryToSecondaryMapper; @@ -99,7 +95,7 @@ private InformerEventSource( parseResourceVersions); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); - if (primaryToSecondaryMapper == null) { + if (useSecondaryToPrimaryIndex()) { primaryToSecondaryIndex = // The index uses the secondary to primary mapper (always present) to build the index new DefaultPrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper()); @@ -161,6 +157,14 @@ public void onDelete(R resource, boolean b) { } } + @Override + public synchronized void start() { + super.start(); + // this makes sure that on first reconciliation all resources are + // present on the index + manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate); + } + private synchronized void onAddOrUpdate( Operation operation, R newObject, R oldObject, Runnable superOnOp) { var resourceID = ResourceID.fromResource(newObject); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index c07ffdbf46..2bb6dcbc75 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -204,8 +204,8 @@ public boolean isRunning() { public Status getStatus() { var status = isRunning() && hasSynced() && isWatching() ? Status.HEALTHY : Status.UNHEALTHY; log.debug( - "Informer status: {} for for type: {}, namespace: {}, details[ is running: {}, has synced:" - + " {}, is watching: {} ]", + "Informer status: {} for type: {}, namespace: {}, details [is running: {}, has synced: {}," + + " is watching: {}]", status, informer.getApiTypeClass().getSimpleName(), namespaceIdentifier, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 001dd1ab41..7ed46a97f3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -83,9 +83,10 @@ public static SecondaryToPrimaryMapper fromOwnerRefer public static SecondaryToPrimaryMapper fromOwnerReferences( String apiVersion, String kind, boolean clusterScope) { + String correctApiVersion = apiVersion.startsWith("/") ? apiVersion.substring(1) : apiVersion; return resource -> resource.getMetadata().getOwnerReferences().stream() - .filter(r -> r.getKind().equals(kind) && r.getApiVersion().equals(apiVersion)) + .filter(r -> r.getKind().equals(kind) && r.getApiVersion().equals(correctApiVersion)) .map(or -> ResourceID.fromOwnerReference(resource, or, clusterScope)) .collect(Collectors.toSet()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 247cdb9aa5..af75a5abc4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -9,7 +9,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -126,9 +126,7 @@ public synchronized void putResource(T newResource, String previousResourceVersi knownResourceVersions.add(newResource.getMetadata().getResourceVersion()); } var resourceId = ResourceID.fromResource(newResource); - var cachedResource = - getResourceFromCache(resourceId) - .orElse(managedInformerEventSource.get(resourceId).orElse(null)); + var cachedResource = managedInformerEventSource.get(resourceId).orElse(null); boolean moveAhead = false; if (previousResourceVersion == null && cachedResource == null) { @@ -167,9 +165,9 @@ public synchronized boolean isKnownResourceVersion(T resource) { } /** - * @return true if {@link InformerEventSourceConfiguration#parseResourceVersions()} is enabled and - * the resourceVersion of newResource is numerically greater than cachedResource, otherwise - * false + * @return true if {@link ConfigurationService#parseResourceVersionsForEventFilteringAndCaching()} + * is enabled and the resourceVersion of newResource is numerically greater than + * cachedResource, otherwise false */ private boolean isLaterResourceVersion(ResourceID resourceId, T newResource, T cachedResource) { try { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index b7e9740552..fe7c9ce391 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -73,7 +73,8 @@ public void run() { } getStateAndFillCache(); healthy.set(true); - } catch (RuntimeException e) { + } catch (Exception e) { + // Exception is required because of Kotlin healthy.set(false); log.error("Error during polling.", e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index 53c0d328a8..a6801b5f39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; @@ -77,6 +78,11 @@ public void stop() { } } + @Override + public Status getStatus() { + return isRunning() ? Status.HEALTHY : Status.UNHEALTHY; + } + @Override public Set getSecondaryResources(HasMetadata primary) { return Set.of(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java similarity index 54% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java index 9bdd54ca8d..42309d5c44 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java @@ -1,31 +1,25 @@ package io.javaoperatorsdk.operator; -import org.junit.jupiter.api.BeforeEach; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; @SuppressWarnings("rawtypes") -class OperatorTest { - - private final KubernetesClient kubernetesClient = MockKubernetesClient.client(ConfigMap.class); - private Operator operator; - - @BeforeEach - void initOperator() { - operator = new Operator(kubernetesClient); - } +class OperatorIT { @Test void shouldBePossibleToRetrieveNumberOfRegisteredControllers() { + final var operator = new Operator(); assertEquals(0, operator.getRegisteredControllersNumber()); operator.register(new FooReconciler()); @@ -34,6 +28,7 @@ void shouldBePossibleToRetrieveNumberOfRegisteredControllers() { @Test void shouldBePossibleToRetrieveRegisteredControllerByName() { + final var operator = new Operator(); final var reconciler = new FooReconciler(); final var name = ReconcilerUtils.getNameFor(reconciler); @@ -51,12 +46,42 @@ void shouldBePossibleToRetrieveRegisteredControllerByName() { assertEquals(maybeController.get(), registeredControllers.stream().findFirst().orElseThrow()); } - @ControllerConfiguration - private static class FooReconciler implements Reconciler { + @Test + void shouldThrowExceptionIf() { + final var operator = new OperatorExtension(); + assertNotNull(operator); + operator.setConfigurationService(ConfigurationService.newOverriddenConfigurationService(null)); + assertNotNull(operator.getConfigurationService()); + + // should fail because the implementation is not providing a valid configuration service when + // constructing the operator + assertThrows( + IllegalStateException.class, + () -> new OperatorExtension(MockKubernetesClient.client(ConfigMap.class))); + } + private static class FooReconciler implements Reconciler { @Override public UpdateControl reconcile(ConfigMap resource, Context context) { return UpdateControl.noUpdate(); } } + + private static class OperatorExtension extends Operator { + public OperatorExtension() {} + + public OperatorExtension(KubernetesClient client) { + super(client); + } + + /** + * Overridden to mimic deferred initialization (or rather the fact that we don't want to do that + * processing at this time so return null). + */ + @Override + protected ConfigurationService initConfigurationService( + KubernetesClient client, Consumer overrider) { + return null; + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index abc83b94ff..ad77196068 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.api.model.rbac.ClusterRole; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder; import io.fabric8.kubernetes.client.CustomResource; @@ -116,6 +117,29 @@ void setsSpecCustomResourceWithReflection() { assertThat(tomcat.getSpec().getReplicas()).isEqualTo(1); } + @Test + void setsStatusWithReflection() { + Deployment deployment = new Deployment(); + DeploymentStatus status = new DeploymentStatus(); + status.setReplicas(2); + + ReconcilerUtils.setStatus(deployment, status); + + assertThat(deployment.getStatus().getReplicas()).isEqualTo(2); + } + + @Test + void getsStatusWithReflection() { + Deployment deployment = new Deployment(); + DeploymentStatus status = new DeploymentStatus(); + status.setReplicas(2); + deployment.setStatus(status); + + var res = ReconcilerUtils.getStatus(deployment); + + assertThat(((DeploymentStatus) res).getReplicas()).isEqualTo(2); + } + @Test void loadYamlAsBuilder() { DeploymentBuilder builder = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java index 2467df75aa..4f30458d68 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java @@ -3,12 +3,14 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotEquals; class ConfigurationServiceOverriderTest { @@ -26,30 +28,32 @@ public R clone(R object) { } }; + final BaseConfigurationService config = + new BaseConfigurationService(null) { + @Override + public boolean checkCRDAndValidateLocalModel() { + return false; + } + + @Override + public Metrics getMetrics() { + return METRICS; + } + + @Override + public Cloner getResourceCloner() { + return CLONER; + } + + @Override + public Optional getLeaderElectionConfiguration() { + return Optional.of(LEADER_ELECTION_CONFIGURATION); + } + }; + @Test void overrideShouldWork() { - final var config = - new BaseConfigurationService(null) { - @Override - public boolean checkCRDAndValidateLocalModel() { - return false; - } - - @Override - public Metrics getMetrics() { - return METRICS; - } - - @Override - public Cloner getResourceCloner() { - return CLONER; - } - - @Override - public Optional getLeaderElectionConfiguration() { - return Optional.of(LEADER_ELECTION_CONFIGURATION); - } - }; + final var overridden = new ConfigurationServiceOverrider(config) .checkingCRDAndValidateLocalModel(true) @@ -86,4 +90,17 @@ public R clone(R object) { assertNotEquals( config.reconciliationTerminationTimeout(), overridden.reconciliationTerminationTimeout()); } + + @Test + void threadCountConfiguredProperly() { + final var overridden = + new ConfigurationServiceOverrider(config) + .withConcurrentReconciliationThreads(13) + .withConcurrentWorkflowExecutorThreads(14) + .build(); + assertThat(((ThreadPoolExecutor) overridden.getExecutorService()).getMaximumPoolSize()) + .isEqualTo(13); + assertThat(((ThreadPoolExecutor) overridden.getWorkflowExecutorService()).getMaximumPoolSize()) + .isEqualTo(14); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java index 33191a8141..837ad7463a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -89,6 +89,22 @@ private io.javaoperatorsdk.operator.api.config.ControllerConfiguration create return configurationService.configFor(reconciler); } + @Test + void overridingNamespacesShouldNotThrowNPE() { + var configuration = createConfiguration(new NullReconciler()); + configuration = + ControllerConfigurationOverrider.override(configuration).settingNamespaces().build(); + assertTrue(configuration.getInformerConfig().watchAllNamespaces()); + } + + private static class NullReconciler implements Reconciler { + @Override + public UpdateControl reconcile(HasMetadata resource, Context context) + throws Exception { + return null; + } + } + @Test void overridingNamespacesShouldWork() { var configuration = createConfiguration(new WatchCurrentReconciler()); @@ -359,21 +375,11 @@ public UpdateControl reconcile(ConfigMap resource, Context } public static class ReadOnlyDependent extends KubernetesDependentResource - implements GarbageCollected { - - public ReadOnlyDependent() { - super(ConfigMap.class); - } - } + implements GarbageCollected {} @KubernetesDependent(informer = @Informer(namespaces = Constants.WATCH_ALL_NAMESPACES)) public static class WatchAllNSDependent extends KubernetesDependentResource - implements GarbageCollected { - - public WatchAllNSDependent() { - super(ConfigMap.class); - } - } + implements GarbageCollected {} @Workflow(dependents = @Dependent(type = OverriddenNSDependent.class)) @ControllerConfiguration( @@ -394,10 +400,6 @@ public static class OverriddenNSDependent implements GarbageCollected { private static final String DEP_NS = "dependentNS"; - - public OverriddenNSDependent() { - super(ConfigMap.class); - } } @Workflow( @@ -415,12 +417,7 @@ public UpdateControl reconcile(ConfigMap resource, Context private static class NamedDependentResource extends KubernetesDependentResource - implements GarbageCollected { - - public NamedDependentResource() { - super(ConfigMap.class); - } - } + implements GarbageCollected {} private static class ExternalDependentResource implements DependentResource, diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 2b75b399c2..a2246f018a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -118,10 +118,5 @@ public UpdateControl reconcile(ConfigMap resource, Context } public static class TestKubernetesDependentResource - extends KubernetesDependentResource { - - public TestKubernetesDependentResource() { - super(Deployment.class); - } - } + extends KubernetesDependentResource {} } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java index dd3caf0bd0..27bd2b9dae 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java @@ -144,20 +144,10 @@ public UpdateControl reconcile(ConfigMap resource, Context } public static class ConfigMapDep extends KubernetesDependentResource - implements GarbageCollected { - - public ConfigMapDep() { - super(ConfigMap.class); - } - } + implements GarbageCollected {} public static class ServiceDep extends KubernetesDependentResource - implements GarbageCollected { - - public ServiceDep() { - super(Service.class); - } - } + implements GarbageCollected {} @CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE) @Configured( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java index 296974c4cd..b289d68b22 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java @@ -9,19 +9,19 @@ import io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class DefaultContextTest { - Secret primary = new Secret(); - Controller mockController = mock(Controller.class); + private final Secret primary = new Secret(); + private final Controller mockController = mock(); - DefaultContext context = new DefaultContext<>(null, mockController, primary); + private final DefaultContext context = new DefaultContext<>(null, mockController, primary); @Test + @SuppressWarnings("unchecked") void getSecondaryResourceReturnsEmptyOptionalOnNonActivatedDRType() { var mockManager = mock(EventSourceManager.class); when(mockController.getEventSourceManager()).thenReturn(mockManager); @@ -30,7 +30,14 @@ void getSecondaryResourceReturnsEmptyOptionalOnNonActivatedDRType() { .thenThrow(new NoEventSourceForClassException(ConfigMap.class)); var res = context.getSecondaryResource(ConfigMap.class); - assertThat(res).isEmpty(); } + + @Test + void setRetryInfo() { + RetryInfo retryInfo = mock(); + var newContext = context.setRetryInfo(retryInfo); + assertThat(newContext).isSameAs(context); + assertThat(newContext.getRetryInfo()).hasValue(retryInfo); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java new file mode 100644 index 0000000000..80a254b50f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java @@ -0,0 +1,164 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils.DEFAULT_MAX_RETRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PrimaryUpdateAndCacheUtilsTest { + + Context context = mock(Context.class); + KubernetesClient client = mock(KubernetesClient.class); + Resource resource = mock(Resource.class); + IndexedResourceCache primaryCache = mock(IndexedResourceCache.class); + + @BeforeEach + void setup() { + when(context.getClient()).thenReturn(client); + var esr = mock(EventSourceRetriever.class); + when(context.eventSourceRetriever()).thenReturn(esr); + when(esr.getControllerEventSource()).thenReturn(mock(ControllerEventSource.class)); + var mixedOp = mock(MixedOperation.class); + when(client.resources(any())).thenReturn(mixedOp); + when(mixedOp.inNamespace(any())).thenReturn(mixedOp); + when(mixedOp.withName(any())).thenReturn(resource); + when(resource.get()).thenReturn(TestUtils.testCustomResource1()); + when(context.getPrimaryCache()).thenReturn(primaryCache); + + var controllerConfiguration = mock(ControllerConfiguration.class); + when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); + var configService = mock(ConfigurationService.class); + when(controllerConfiguration.getConfigurationService()).thenReturn(configService); + when(configService.getResourceCloner()) + .thenReturn( + new Cloner() { + @Override + public R clone(R object) { + return new KubernetesSerialization().clone(object); + } + }); + } + + @Test + void handlesUpdate() { + var updated = + PrimaryUpdateAndCacheUtils.updateAndCacheResource( + TestUtils.testCustomResource1(), + context, + r -> { + var res = TestUtils.testCustomResource1(); + // setting this to null to test if value set in the implementation + res.getMetadata().setResourceVersion(null); + res.getSpec().setValue("updatedValue"); + return res; + }, + r -> { + // checks if the resource version is set from the original resource + assertThat(r.getMetadata().getResourceVersion()).isEqualTo("1"); + var res = TestUtils.testCustomResource1(); + res.setSpec(r.getSpec()); + res.getMetadata().setResourceVersion("2"); + return res; + }); + + assertThat(updated.getMetadata().getResourceVersion()).isEqualTo("2"); + assertThat(updated.getSpec().getValue()).isEqualTo("updatedValue"); + } + + @Test + void retriesConflicts() { + var updateOperation = mock(UnaryOperator.class); + + when(updateOperation.apply(any())) + .thenThrow(new KubernetesClientException("", 409, null)) + .thenReturn(TestUtils.testCustomResource1()); + var freshResource = TestUtils.testCustomResource1(); + + freshResource.getMetadata().setResourceVersion("2"); + when(primaryCache.get(any())).thenReturn(Optional.of(freshResource)); + + var updated = + PrimaryUpdateAndCacheUtils.updateAndCacheResource( + TestUtils.testCustomResource1(), + context, + r -> { + var res = TestUtils.testCustomResource1(); + res.getSpec().setValue("updatedValue"); + return res; + }, + updateOperation); + + assertThat(updated).isNotNull(); + verify(primaryCache, times(1)).get(any()); + } + + @Test + void throwsIfRetryExhausted() { + var updateOperation = mock(UnaryOperator.class); + + when(updateOperation.apply(any())).thenThrow(new KubernetesClientException("", 409, null)); + var stubbing = when(primaryCache.get(any())); + + for (int i = 0; i < DEFAULT_MAX_RETRY; i++) { + var resource = TestUtils.testCustomResource1(); + resource.getMetadata().setResourceVersion("" + i); + stubbing = stubbing.thenReturn(Optional.of(resource)); + } + assertThrows( + OperatorException.class, + () -> + PrimaryUpdateAndCacheUtils.updateAndCacheResource( + TestUtils.testCustomResource1(), + context, + UnaryOperator.identity(), + updateOperation)); + verify(primaryCache, times(DEFAULT_MAX_RETRY)).get(any()); + } + + @Test + void cachePollTimeouts() { + var updateOperation = mock(UnaryOperator.class); + + when(updateOperation.apply(any())).thenThrow(new KubernetesClientException("", 409, null)); + when(primaryCache.get(any())).thenReturn(Optional.of(TestUtils.testCustomResource1())); + + var ex = + assertThrows( + OperatorException.class, + () -> + PrimaryUpdateAndCacheUtils.updateAndCacheResource( + TestUtils.testCustomResource1(), + context, + UnaryOperator.identity(), + updateOperation, + 2, + 50L, + 10L)); + assertThat(ex.getMessage()).contains("Timeout"); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java index f705160059..d871668c4d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java @@ -61,19 +61,31 @@ void pluralShouldOnlyBeProvidedIfExplicitlySet() { @Test void pluralShouldBeEmptyIfNotProvided() { final var kind = "MyKind"; - var gvk = - GroupVersionKindPlural.gvkWithPlural(new GroupVersionKind("josdk.io", "v1", kind), null); + final var original = new GroupVersionKind("josdk.io", "v1", kind); + var gvk = GroupVersionKindPlural.gvkWithPlural(original, null); assertThat(gvk.getPlural()).isEmpty(); assertThat(gvk.getPluralOrDefault()) .isEqualTo(GroupVersionKindPlural.getDefaultPluralFor(kind)); + assertThat(gvk).isEqualTo(original); + assertThat(original).isEqualTo(gvk); + assertThat(gvk.hashCode()).isEqualTo(original.hashCode()); } @Test void pluralShouldOverrideDefaultComputedVersionIfProvided() { - var gvk = - GroupVersionKindPlural.gvkWithPlural( - new GroupVersionKind("josdk.io", "v1", "MyKind"), "MyPlural"); + final var original = new GroupVersionKind("josdk.io", "v1", "MyKind"); + final var gvk = GroupVersionKindPlural.gvkWithPlural(original, "MyPlural"); assertThat(gvk.getPlural()).hasValue("MyPlural"); + assertThat(gvk).isNotEqualTo(original); + assertThat(original).isNotEqualTo(gvk); + assertThat(gvk.hashCode()).isNotEqualTo(original.hashCode()); + } + + @Test + void equals() { + final var original = new GroupVersionKind("josdk.io", "v1", "MyKind"); + assertEquals(original, original); + assertFalse(original.equals(null)); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 0d85ee7225..3062e360e2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -186,10 +186,6 @@ HasMetadata createPrimary(String caseName) { private static class ServiceAccountDR extends KubernetesDependentResource { - public ServiceAccountDR() { - super(ServiceAccount.class); - } - @Override protected ServiceAccount desired(HasMetadata primary, Context context) { return new ServiceAccountBuilder() diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java new file mode 100644 index 0000000000..6c48311f4b --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static io.javaoperatorsdk.operator.api.config.Utils.GENERIC_PARAMETER_TYPE_ERROR_PREFIX; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KubernetesDependentResourceTest { + + @ParameterizedTest + @ValueSource( + classes = { + TestDeploymentDependentResource.class, + ChildTestDeploymentDependentResource.class, + GrandChildTestDeploymentDependentResource.class, + ChildTypeWithValidKubernetesDependentResource.class, + ConstructorOverridedCorrectDeployementDependentResource.class + }) + void checkResourceTypeDerivationWithInheritance(Class clazz) throws Exception { + KubernetesDependentResource dependentResource = + (KubernetesDependentResource) clazz.getDeclaredConstructor().newInstance(); + assertThat(dependentResource).isInstanceOf(KubernetesDependentResource.class); + assertThat(dependentResource.resourceType()).isEqualTo(Deployment.class); + } + + private static class TestDeploymentDependentResource + extends KubernetesDependentResource {} + + private static class ChildTestDeploymentDependentResource + extends TestDeploymentDependentResource {} + + private static class GrandChildTestDeploymentDependentResource + extends ChildTestDeploymentDependentResource {} + + private static class ChildTypeWithValidKubernetesDependentResource + extends KubernetesDependentResource {} + + private static class ConstructorOverridedCorrectDeployementDependentResource + extends KubernetesDependentResource { + public ConstructorOverridedCorrectDeployementDependentResource() { + super(Deployment.class); + } + } + + @Test + void validateInvalidTypeDerivationTypesThrowException() { + assertThatThrownBy(() -> new InvalidChildTestDeploymentDependentResource()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + GENERIC_PARAMETER_TYPE_ERROR_PREFIX + + InvalidChildTestDeploymentDependentResource.class.getName() + + " because it doesn't extend a class that is parametrized with the type we want to" + + " retrieve or because it's Object.class. Please provide the resource type in the " + + "constructor (e.g., super(Deployment.class)."); + assertThatThrownBy(() -> new InvalidGrandChildTestDeploymentDependentResource()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + GENERIC_PARAMETER_TYPE_ERROR_PREFIX + + InvalidGrandChildTestDeploymentDependentResource.class.getName() + + " because it doesn't extend a class that is parametrized with the type we want to" + + " retrieve or because it's Object.class. Please provide the resource type in the " + + "constructor (e.g., super(Deployment.class)."); + } + + private static class InvalidChildTestDeploymentDependentResource + extends ChildTypeWithValidKubernetesDependentResource {} + + private static class InvalidGrandChildTestDeploymentDependentResource + extends InvalidChildTestDeploymentDependentResource {} +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java new file mode 100644 index 0000000000..e7756b45bc --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java @@ -0,0 +1,451 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.ListAssert; +import org.assertj.core.api.MapAssert; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import io.javaoperatorsdk.operator.MockKubernetesClient; + +import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.PodTemplateSpecSanitizer.sanitizePodTemplateSpec; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests the {@link PodTemplateSpecSanitizer} with combinations of matching and mismatching K8s + * resources, using a mix of containers and init containers, as well as resource requests and limits + * along with environment variables. + */ +class PodTemplateSpecSanitizerTest { + + private final Map actualMap = mock(); + + private final KubernetesClient client = MockKubernetesClient.client(HasMetadata.class); + private final KubernetesSerialization serialization = client.getKubernetesSerialization(); + + @Test + void testSanitizePodTemplateSpec_whenTemplateIsNull_doNothing() { + final var template = new PodTemplateSpecBuilder().build(); + + sanitizePodTemplateSpec(actualMap, null, template); + sanitizePodTemplateSpec(actualMap, template, null); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizePodTemplateSpec_whenTemplateSpecIsNull_doNothing() { + final var template = new PodTemplateSpecBuilder().withSpec(null).build(); + final var templateWithSpec = new PodTemplateSpecBuilder().withNewSpec().endSpec().build(); + + sanitizePodTemplateSpec(actualMap, template, templateWithSpec); + sanitizePodTemplateSpec(actualMap, templateWithSpec, template); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizePodTemplateSpec_whenContainerSizeMismatch_doNothing() { + final var template = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .endContainer() + .endSpec() + .build(); + final var templateWithTwoContainers = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .endContainer() + .addNewContainer() + .withName("test-new") + .endContainer() + .endSpec() + .build(); + + sanitizePodTemplateSpec(actualMap, template, templateWithTwoContainers); + sanitizePodTemplateSpec(actualMap, templateWithTwoContainers, template); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizePodTemplateSpec_whenContainerNameMismatch_doNothing() { + final var template = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .endContainer() + .endSpec() + .build(); + final var templateWithNewContainerName = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test-new") + .endContainer() + .endSpec() + .build(); + + sanitizePodTemplateSpec(actualMap, template, templateWithNewContainerName); + sanitizePodTemplateSpec(actualMap, templateWithNewContainerName, template); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizePodTemplateSpec_whenResourceIsNull_doNothing() { + final var template = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .endContainer() + .endSpec() + .build(); + final var templateWithResource = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .withNewResources() + .endResources() + .endContainer() + .endSpec() + .build(); + + sanitizePodTemplateSpec(actualMap, template, templateWithResource); + sanitizePodTemplateSpec(actualMap, templateWithResource, template); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizeResourceRequirements_whenResourceKeyMismatch_doNothing() { + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.INIT_CONTAINER, + Map.of("cpu", new Quantity("2")), + Map.of("memory", new Quantity("4Gi")), + Map.of(), + Map.of()); + assertInitContainerResources(actualMap, "requests").hasSize(1).containsEntry("cpu", "2"); + assertInitContainerResources(actualMap, "limits").isNull(); + } + + @Test + void testSanitizePodTemplateSpec_whenResourcesHaveSameAmountAndFormat_doNothing() { + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.CONTAINER, + Map.of("memory", new Quantity("4Gi")), + Map.of("memory", new Quantity("4Gi")), + Map.of("cpu", new Quantity("2")), + Map.of("cpu", new Quantity("2"))); + assertContainerResources(actualMap, "requests").hasSize(1).containsEntry("memory", "4Gi"); + assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "2"); + } + + @Test + void testSanitizePodTemplateSpec_whenResourcesHaveNumericalAmountMismatch_doNothing() { + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.INIT_CONTAINER, + Map.of("cpu", new Quantity("2"), "memory", new Quantity("4Gi")), + Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Ti")), + Map.of("cpu", new Quantity("2")), + Map.of("cpu", new Quantity("4000m"))); + assertInitContainerResources(actualMap, "requests") + .hasSize(2) + .containsEntry("cpu", "2") + .containsEntry("memory", "4Gi"); + assertInitContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "2"); + } + + @Test + void + testSanitizePodTemplateSpec_whenResourcesHaveNumericalAmountMismatch_withEphemeralStorageAddedByOtherOperator_doNothing() { + // mimics an environment like GKE Autopilot that enforces ephemeral-storage requests and limits + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.INIT_CONTAINER, + Map.of( + "cpu", + new Quantity("2"), + "memory", + new Quantity("4Gi"), + "ephemeral-storage", + new Quantity("1Gi")), + Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Ti")), + Map.of("cpu", new Quantity("2"), "ephemeral-storage", new Quantity("1Gi")), + Map.of("cpu", new Quantity("4000m"))); + assertInitContainerResources(actualMap, "requests") + .hasSize(3) + .containsEntry("cpu", "2") + .containsEntry("memory", "4Gi") + .containsEntry("ephemeral-storage", "1Gi"); + assertInitContainerResources(actualMap, "limits") + .hasSize(2) + .containsEntry("cpu", "2") + .containsEntry("ephemeral-storage", "1Gi"); + } + + @Test + void + testSanitizeResourceRequirements_whenResourcesHaveAmountAndFormatMismatchWithSameNumericalAmount_thenSanitizeActualMap() { + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.CONTAINER, + Map.of("cpu", new Quantity("2"), "memory", new Quantity("4Gi")), + Map.of("cpu", new Quantity("2000m"), "memory", new Quantity("4096Mi")), + Map.of("cpu", new Quantity("4")), + Map.of("cpu", new Quantity("4000m"))); + assertContainerResources(actualMap, "requests") + .hasSize(2) + .containsEntry("cpu", "2000m") + .containsEntry("memory", "4096Mi"); + assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "4000m"); + } + + @Test + void + testSanitizeResourceRequirements_whenResourcesHaveAmountAndFormatMismatchWithSameNumericalAmount_withEphemeralStorageAddedByOtherOperator_thenSanitizeActualMap() { + // mimics an environment like GKE Autopilot that enforces ephemeral-storage requests and limits + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.CONTAINER, + Map.of( + "cpu", + new Quantity("2"), + "memory", + new Quantity("4Gi"), + "ephemeral-storage", + new Quantity("1Gi")), + Map.of("cpu", new Quantity("2000m"), "memory", new Quantity("4096Mi")), + Map.of("cpu", new Quantity("4"), "ephemeral-storage", new Quantity("1Gi")), + Map.of("cpu", new Quantity("4000m"))); + assertContainerResources(actualMap, "requests") + .hasSize(3) + .containsEntry("cpu", "2000m") + .containsEntry("memory", "4096Mi") + .containsEntry("ephemeral-storage", "1Gi"); + assertContainerResources(actualMap, "limits") + .hasSize(2) + .containsEntry("cpu", "4000m") + .containsEntry("ephemeral-storage", "1Gi"); + } + + @Test + void testSanitizePodTemplateSpec_whenEnvVarsIsEmpty_doNothing() { + final var template = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .endContainer() + .endSpec() + .build(); + final var templateWithEnvVars = + new PodTemplateSpecBuilder() + .withNewSpec() + .addNewContainer() + .withName("test") + .withEnv(List.of(new EnvVarBuilder().withName("FOO").withValue("foobar").build())) + .endContainer() + .endSpec() + .build(); + + sanitizePodTemplateSpec(actualMap, template, templateWithEnvVars); + sanitizePodTemplateSpec(actualMap, templateWithEnvVars, template); + verifyNoInteractions(actualMap); + } + + @Test + void testSanitizePodTemplateSpec_whenActualEnvVarValueIsNotEmpty_doNothing() { + final var actualMap = + sanitizeEnvVars( + ContainerType.CONTAINER, + List.of( + new EnvVarBuilder().withName("FOO").withValue("foo").build(), + new EnvVarBuilder().withName("BAR").withValue("bar").build()), + List.of( + new EnvVarBuilder().withName("FOO").withValue("bar").build(), + new EnvVarBuilder().withName("BAR").withValue("foo").build())); + assertContainerEnvVars(actualMap) + .hasSize(2) + .containsExactly( + Map.of("name", "FOO", "value", "foo"), Map.of("name", "BAR", "value", "bar")); + } + + @Test + void testSanitizePodTemplateSpec_whenActualAndDesiredEnvVarsAreDifferent_doNothing() { + final var actualMap = + sanitizeEnvVars( + ContainerType.INIT_CONTAINER, + List.of(new EnvVarBuilder().withName("FOO").withValue("foo").build()), + List.of(new EnvVarBuilder().withName("BAR").withValue("bar").build())); + assertInitContainerEnvVars(actualMap) + .hasSize(1) + .containsExactly(Map.of("name", "FOO", "value", "foo")); + } + + @Test + void testSanitizePodTemplateSpec_whenActualEnvVarIsEmpty_doNothing() { + final var actualMap = + sanitizeEnvVars( + ContainerType.INIT_CONTAINER, + List.of( + new EnvVarBuilder().withName("FOO").withValue("").build(), + new EnvVarBuilder().withName("BAR").withValue("").build()), + List.of( + new EnvVarBuilder().withName("FOO").withValue("foo").build(), + new EnvVarBuilder().withName("BAR").withValue("").build())); + assertInitContainerEnvVars(actualMap) + .hasSize(2) + .containsExactly(Map.of("name", "FOO", "value", ""), Map.of("name", "BAR", "value", "")); + } + + @Test + void testSanitizePodTemplateSpec_whenActualEnvVarIsNull_doNothing() { + final var actualMap = + sanitizeEnvVars( + ContainerType.CONTAINER, + List.of( + new EnvVarBuilder().withName("FOO").withValue(null).build(), + new EnvVarBuilder().withName("BAR").withValue(null).build()), + List.of( + new EnvVarBuilder().withName("FOO").withValue("foo").build(), + new EnvVarBuilder().withName("BAR").withValue(" ").build())); + assertContainerEnvVars(actualMap) + .hasSize(2) + .containsExactly(Map.of("name", "FOO"), Map.of("name", "BAR")); + } + + @Test + void + testSanitizePodTemplateSpec_whenActualEnvVarIsNull_withDesiredEnvVarEmpty_thenSanitizeActualMap() { + final var actualMap = + sanitizeEnvVars( + ContainerType.CONTAINER, + List.of( + new EnvVarBuilder().withName("FOO").withValue(null).build(), + new EnvVarBuilder().withName("BAR").withValue(null).build()), + List.of( + new EnvVarBuilder().withName("FOO").withValue("").build(), + new EnvVarBuilder().withName("BAR").withValue("").build())); + assertContainerEnvVars(actualMap) + .hasSize(2) + .containsExactly(Map.of("name", "FOO", "value", ""), Map.of("name", "BAR", "value", "")); + } + + private Map sanitizeRequestsAndLimits( + final ContainerType type, + final Map actualRequests, + final Map desiredRequests, + final Map actualLimits, + final Map desiredLimits) { + return sanitize( + type, actualRequests, desiredRequests, actualLimits, desiredLimits, List.of(), List.of()); + } + + private Map sanitizeEnvVars( + final ContainerType type, + final List actualEnvVars, + final List desiredEnvVars) { + return sanitize(type, Map.of(), Map.of(), Map.of(), Map.of(), actualEnvVars, desiredEnvVars); + } + + @SuppressWarnings("unchecked") + private Map sanitize( + final ContainerType type, + final Map actualRequests, + final Map desiredRequests, + final Map actualLimits, + final Map desiredLimits, + final List actualEnvVars, + final List desiredEnvVars) { + final var actual = createStatefulSet(type, actualRequests, actualLimits, actualEnvVars); + final var desired = createStatefulSet(type, desiredRequests, desiredLimits, desiredEnvVars); + final var actualMap = serialization.convertValue(actual, Map.class); + sanitizePodTemplateSpec( + actualMap, actual.getSpec().getTemplate(), desired.getSpec().getTemplate()); + return actualMap; + } + + private enum ContainerType { + CONTAINER, + INIT_CONTAINER, + } + + private static StatefulSet createStatefulSet( + final ContainerType type, + final Map requests, + final Map limits, + final List envVars) { + var builder = new StatefulSetBuilder().withNewSpec().withNewTemplate().withNewSpec(); + if (type == ContainerType.CONTAINER) { + builder = + builder + .addNewContainer() + .withName("test") + .withNewResources() + .withRequests(requests) + .withLimits(limits) + .endResources() + .withEnv(envVars) + .endContainer(); + } else { + builder = + builder + .addNewInitContainer() + .withName("test") + .withNewResources() + .withRequests(requests) + .withLimits(limits) + .endResources() + .withEnv(envVars) + .endInitContainer(); + } + return builder.endSpec().endTemplate().endSpec().build(); + } + + private static MapAssert assertContainerResources( + final Map actualMap, final String resourceName) { + return assertThat( + GenericKubernetesResource.>get( + actualMap, "spec", "template", "spec", "containers", 0, "resources", resourceName)); + } + + private static MapAssert assertInitContainerResources( + final Map actualMap, final String resourceName) { + return assertThat( + GenericKubernetesResource.>get( + actualMap, "spec", "template", "spec", "initContainers", 0, "resources", resourceName)); + } + + private static ListAssert> assertContainerEnvVars( + final Map actualMap) { + return assertThat( + GenericKubernetesResource.>>get( + actualMap, "spec", "template", "spec", "containers", 0, "env")); + } + + private static ListAssert> assertInitContainerEnvVars( + final Map actualMap) { + return assertThat( + GenericKubernetesResource.>>get( + actualMap, "spec", "template", "spec", "initContainers", 0, "env")); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizerTest.java deleted file mode 100644 index 79f3640883..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceRequirementsSanitizerTest.java +++ /dev/null @@ -1,265 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import java.util.Map; - -import org.assertj.core.api.MapAssert; -import org.junit.jupiter.api.Test; - -import io.fabric8.kubernetes.api.model.GenericKubernetesResource; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; -import io.fabric8.kubernetes.api.model.Quantity; -import io.fabric8.kubernetes.api.model.apps.StatefulSet; -import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.KubernetesSerialization; -import io.javaoperatorsdk.operator.MockKubernetesClient; - -import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceRequirementsSanitizer.sanitizeResourceRequirements; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * Tests the {@link ResourceRequirementsSanitizer} with combinations of matching and mismatching K8s - * resources, using a mix of containers and init containers, as well as resource requests and - * limits. - */ -class ResourceRequirementsSanitizerTest { - - private final Map actualMap = mock(); - - private final KubernetesClient client = MockKubernetesClient.client(HasMetadata.class); - private final KubernetesSerialization serialization = client.getKubernetesSerialization(); - - @Test - void testSanitizeResourceRequirements_whenTemplateIsNull_doNothing() { - final var template = new PodTemplateSpecBuilder().build(); - - sanitizeResourceRequirements(actualMap, null, template); - sanitizeResourceRequirements(actualMap, template, null); - verifyNoInteractions(actualMap); - } - - @Test - void testSanitizeResourceRequirements_whenTemplateSpecIsNull_doNothing() { - final var template = new PodTemplateSpecBuilder().withSpec(null).build(); - final var templateWithSpec = new PodTemplateSpecBuilder().withNewSpec().endSpec().build(); - - sanitizeResourceRequirements(actualMap, template, templateWithSpec); - sanitizeResourceRequirements(actualMap, templateWithSpec, template); - verifyNoInteractions(actualMap); - } - - @Test - void testSanitizeResourceRequirements_whenContainerSizeMismatch_doNothing() { - final var template = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test") - .endContainer() - .endSpec() - .build(); - final var templateWithTwoContainers = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test") - .endContainer() - .addNewContainer() - .withName("test-new") - .endContainer() - .endSpec() - .build(); - - sanitizeResourceRequirements(actualMap, template, templateWithTwoContainers); - sanitizeResourceRequirements(actualMap, templateWithTwoContainers, template); - verifyNoInteractions(actualMap); - } - - @Test - void testSanitizeResourceRequirements_whenContainerNameMismatch_doNothing() { - final var template = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test") - .endContainer() - .endSpec() - .build(); - final var templateWithNewContainerName = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test-new") - .endContainer() - .endSpec() - .build(); - - sanitizeResourceRequirements(actualMap, template, templateWithNewContainerName); - sanitizeResourceRequirements(actualMap, templateWithNewContainerName, template); - verifyNoInteractions(actualMap); - } - - @Test - void testSanitizeResourceRequirements_whenResourceIsNull_doNothing() { - final var template = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test") - .endContainer() - .endSpec() - .build(); - final var templateWithResource = - new PodTemplateSpecBuilder() - .withNewSpec() - .addNewContainer() - .withName("test") - .withNewResources() - .endResources() - .endContainer() - .endSpec() - .build(); - - sanitizeResourceRequirements(actualMap, template, templateWithResource); - sanitizeResourceRequirements(actualMap, templateWithResource, template); - verifyNoInteractions(actualMap); - } - - @Test - void testSanitizeResourceRequirements_whenResourceSizeMismatch_doNothing() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.CONTAINER, - Map.of("cpu", new Quantity("2")), - Map.of(), - Map.of("cpu", new Quantity("4")), - Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Gi"))); - assertContainerResources(actualMap, "requests").hasSize(1).containsEntry("cpu", "2"); - assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "4"); - } - - @Test - void testSanitizeResourceRequirements_whenResourceKeyMismatch_doNothing() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.INIT_CONTAINER, - Map.of("cpu", new Quantity("2")), - Map.of("memory", new Quantity("4Gi")), - Map.of(), - Map.of()); - assertInitContainerResources(actualMap, "requests").hasSize(1).containsEntry("cpu", "2"); - assertInitContainerResources(actualMap, "limits").isNull(); - } - - @Test - void testSanitizeResourceRequirements_whenResourcesHaveSameAmountAndFormat_doNothing() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.CONTAINER, - Map.of("memory", new Quantity("4Gi")), - Map.of("memory", new Quantity("4Gi")), - Map.of("cpu", new Quantity("2")), - Map.of("cpu", new Quantity("2"))); - assertContainerResources(actualMap, "requests").hasSize(1).containsEntry("memory", "4Gi"); - assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "2"); - } - - @Test - void testSanitizeResourceRequirements_whenResourcesHaveNumericalAmountMismatch_doNothing() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.INIT_CONTAINER, - Map.of("cpu", new Quantity("2"), "memory", new Quantity("4Gi")), - Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Ti")), - Map.of("cpu", new Quantity("2")), - Map.of("cpu", new Quantity("4000m"))); - assertInitContainerResources(actualMap, "requests") - .hasSize(2) - .containsEntry("cpu", "2") - .containsEntry("memory", "4Gi"); - assertInitContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "2"); - } - - @Test - void - testSanitizeResourceRequirements_whenResourcesHaveAmountAndFormatMismatchWithSameNumericalAmount_thenSanitizeActualMap() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.CONTAINER, - Map.of("cpu", new Quantity("2"), "memory", new Quantity("4Gi")), - Map.of("cpu", new Quantity("2000m"), "memory", new Quantity("4096Mi")), - Map.of("cpu", new Quantity("4")), - Map.of("cpu", new Quantity("4000m"))); - assertContainerResources(actualMap, "requests") - .hasSize(2) - .containsEntry("cpu", "2000m") - .containsEntry("memory", "4096Mi"); - assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "4000m"); - } - - @SuppressWarnings("unchecked") - private Map sanitizeRequestsAndLimits( - final ContainerType type, - final Map actualRequests, - final Map desiredRequests, - final Map actualLimits, - final Map desiredLimits) { - final var actual = createStatefulSet(type, actualRequests, actualLimits); - final var desired = createStatefulSet(type, desiredRequests, desiredLimits); - final var actualMap = serialization.convertValue(actual, Map.class); - sanitizeResourceRequirements( - actualMap, actual.getSpec().getTemplate(), desired.getSpec().getTemplate()); - return actualMap; - } - - private enum ContainerType { - CONTAINER, - INIT_CONTAINER, - } - - private static StatefulSet createStatefulSet( - final ContainerType type, - final Map requests, - final Map limits) { - var builder = new StatefulSetBuilder().withNewSpec().withNewTemplate().withNewSpec(); - if (type == ContainerType.CONTAINER) { - builder = - builder - .addNewContainer() - .withName("test") - .withNewResources() - .withRequests(requests) - .withLimits(limits) - .endResources() - .endContainer(); - } else { - builder = - builder - .addNewInitContainer() - .withName("test") - .withNewResources() - .withRequests(requests) - .withLimits(limits) - .endResources() - .endInitContainer(); - } - return builder.endSpec().endTemplate().endSpec().build(); - } - - private static MapAssert assertContainerResources( - final Map actualMap, final String resourceName) { - return assertThat( - GenericKubernetesResource.>get( - actualMap, "spec", "template", "spec", "containers", 0, "resources", resourceName)); - } - - private static MapAssert assertInitContainerResources( - final Map actualMap, final String resourceName) { - return assertThat( - GenericKubernetesResource.>get( - actualMap, "spec", "template", "spec", "initContainers", 0, "resources", resourceName)); - } -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index 69bdf59aff..e441516d46 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -11,23 +10,27 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.apps.DaemonSet; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.ReplicaSet; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class SSABasedGenericKubernetesResourceMatcherTest { - private final Context mockedContext = mock(); + private final Context mockedContext = mock(); private final SSABasedGenericKubernetesResourceMatcher matcher = SSABasedGenericKubernetesResourceMatcher.getInstance(); @@ -39,12 +42,61 @@ void setup() { when(mockedContext.getClient()).thenReturn(client); final var configurationService = mock(ConfigurationService.class); + when(configurationService.shouldUseSSA(any(), any(), any())).thenReturn(true); final var controllerConfiguration = mock(ControllerConfiguration.class); when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); when(controllerConfiguration.fieldManager()).thenReturn("controller"); when(mockedContext.getControllerConfiguration()).thenReturn(controllerConfiguration); } + @Test + void noMatchWhenNoMatchingController() { + var desired = loadResource("nginx-deployment.yaml", Deployment.class); + var actual = + loadResource("deployment-with-managed-fields-additional-controller.yaml", Deployment.class); + actual + .getMetadata() + .getManagedFields() + .removeIf(managedFieldsEntry -> managedFieldsEntry.getManager().equals("controller")); + + assertThat(matcher.matches(actual, desired, mockedContext)).isFalse(); + } + + @Test + void exceptionWhenDuplicateController() { + var desired = loadResource("nginx-deployment.yaml", Deployment.class); + var actual = + loadResource("deployment-with-managed-fields-additional-controller.yaml", Deployment.class); + actual.getMetadata().getManagedFields().stream() + .filter(managedFieldsEntry -> managedFieldsEntry.getManager().equals("controller")) + .findFirst() + .ifPresent( + managedFieldsEntry -> actual.getMetadata().getManagedFields().add(managedFieldsEntry)); + + assertThatThrownBy(() -> matcher.matches(actual, desired, mockedContext)) + .isInstanceOf(OperatorException.class) + .hasMessage( + "More than one field manager exists with name: controller in resource: Deployment with" + + " name: test"); + } + + @Test + void matchWithSensitiveResource() { + var desired = loadResource("secret-desired.yaml", Secret.class); + var actual = loadResource("secret.yaml", Secret.class); + + assertThat(matcher.matches(actual, desired, mockedContext)).isTrue(); + } + + @Test + void noMatchWithSensitiveResource() { + var desired = loadResource("secret-desired.yaml", Secret.class); + var actual = loadResource("secret.yaml", Secret.class); + actual.getData().put("key1", "dmFsMg=="); + + assertThat(matcher.matches(actual, desired, mockedContext)).isFalse(); + } + @Test void checksIfAddsNotAddedByController() { var desired = loadResource("nginx-deployment.yaml", Deployment.class); @@ -54,7 +106,40 @@ void checksIfAddsNotAddedByController() { assertThat(matcher.matches(actual, desired, mockedContext)).isTrue(); } - // In the example the owner reference in a list is referenced by "k:", while all the fields are + @Test + void throwExceptionWhenManagedListEntryNotFound() { + var desired = loadResource("nginx-deployment.yaml", Deployment.class); + var actual = + loadResource("deployment-with-managed-fields-additional-controller.yaml", Deployment.class); + final var container = actual.getSpec().getTemplate().getSpec().getContainers().get(0); + container.setName("foobar"); + + assertThatThrownBy(() -> matcher.matches(actual, desired, mockedContext)) + .isInstanceOf(IllegalStateException.class) + .hasMessage( + "Cannot find list element for key: {\"name\":\"nginx\"} in map: [[image," + + " imagePullPolicy, name, ports, resources, terminationMessagePath," + + " terminationMessagePolicy]]"); + } + + @Test + void throwExceptionWhenDuplicateManagedListEntryFound() { + var desired = loadResource("nginx-deployment.yaml", Deployment.class); + var actual = + loadResource("deployment-with-managed-fields-additional-controller.yaml", Deployment.class); + final var container = actual.getSpec().getTemplate().getSpec().getContainers().get(0); + actual.getSpec().getTemplate().getSpec().getContainers().add(container); + + assertThatThrownBy(() -> matcher.matches(actual, desired, mockedContext)) + .isInstanceOf(IllegalStateException.class) + .hasMessage( + "More targets found in list element for key: {\"name\":\"nginx\"} in map: [[image," + + " imagePullPolicy, name, ports, resources, terminationMessagePath," + + " terminationMessagePolicy], [image, imagePullPolicy, name, ports, resources," + + " terminationMessagePath, terminationMessagePolicy]]"); + } + + // in the example the owner reference in a list is referenced by "k:", while all the fields are // managed but not listed @Test void emptyListElementMatchesAllFields() { @@ -116,45 +201,11 @@ void addedLabelInDesiredMakesMatchFail() { } @Test - @SuppressWarnings("unchecked") - void sortListItemsTest() { - var nestedMap1 = new HashMap(); - nestedMap1.put("z", 26); - nestedMap1.put("y", 25); - - var nestedMap2 = new HashMap(); - nestedMap2.put("b", 26); - nestedMap2.put("c", 25); - nestedMap2.put("a", 24); - - var unsortedListItems = List.of(1, nestedMap1, nestedMap2); - var sortedListItems = matcher.sortListItems(unsortedListItems); - assertThat(sortedListItems).element(0).isEqualTo(1); - - var sortedNestedMap1 = (Map) sortedListItems.get(1); - assertThat(sortedNestedMap1.keySet()).containsExactly("y", "z"); - - var sortedNestedMap2 = (Map) sortedListItems.get(2); - assertThat(sortedNestedMap2.keySet()).containsExactly("a", "b", "c"); - } - - @Test - @SuppressWarnings("unchecked") - void testSortMapWithNestedMap() { - var nestedMap = new HashMap(); - nestedMap.put("z", 26); - nestedMap.put("y", 25); - - var unsortedMap = new HashMap(); - unsortedMap.put("b", nestedMap); - unsortedMap.put("a", 1); - unsortedMap.put("c", 2); - - var sortedMap = matcher.sortMap(unsortedMap); - assertThat(sortedMap.keySet()).containsExactly("a", "b", "c"); + void withFinalizer() { + var desired = loadResource("secret-with-finalizer-desired.yaml", Secret.class); + var actual = loadResource("secret-with-finalizer.yaml", Secret.class); - var sortedNestedMap = (Map) sortedMap.get("b"); - assertThat(sortedNestedMap.keySet()).containsExactly("y", "z"); + assertThat(matcher.matches(actual, desired, mockedContext)).isTrue(); } @ParameterizedTest @@ -203,6 +254,23 @@ void testSanitizeState_statefulSetWithResources_withMismatch() { assertThat(matcher.matches(actualStatefulSet, desiredStatefulSet, mockedContext)).isFalse(); } + @Test + void testSanitizeState_statefulSet_withResourceTypeMismatch() { + var desiredReplicaSet = loadResource("sample-rs-resources-desired.yaml", ReplicaSet.class); + var actualStatefulSet = loadResource("sample-sts-resources.yaml", StatefulSet.class); + + assertThat(matcher.matches(actualStatefulSet, desiredReplicaSet, mockedContext)).isFalse(); + } + + @Test + void testSanitizeState_deployment_withResourceTypeMismatch() { + var desiredReplicaSet = loadResource("sample-rs-resources-desired.yaml", ReplicaSet.class); + var actualDeployment = + loadResource("deployment-with-managed-fields-additional-controller.yaml", Deployment.class); + + assertThat(matcher.matches(actualDeployment, desiredReplicaSet, mockedContext)).isFalse(); + } + @Test void testSanitizeState_replicaSetWithResources() { var desiredReplicaSet = loadResource("sample-rs-resources-desired.yaml", ReplicaSet.class); @@ -220,6 +288,14 @@ void testSanitizeState_replicaSetWithResources_withMismatch() { assertThat(matcher.matches(actualReplicaSet, desiredReplicaSet, mockedContext)).isFalse(); } + @Test + void testSanitizeState_replicaSet_withResourceTypeMismatch() { + var desiredDaemonSet = loadResource("sample-ds-resources-desired.yaml", DaemonSet.class); + var actualReplicaSet = loadResource("sample-rs-resources.yaml", ReplicaSet.class); + + assertThat(matcher.matches(actualReplicaSet, desiredDaemonSet, mockedContext)).isFalse(); + } + @Test void testSanitizeState_daemonSetWithResources() { var desiredDaemonSet = loadResource("sample-ds-resources-desired.yaml", DaemonSet.class); @@ -236,8 +312,99 @@ void testSanitizeState_daemonSetWithResources_withMismatch() { assertThat(matcher.matches(actualDaemonSet, desiredDaemonSet, mockedContext)).isFalse(); } + @Test + void testSanitizeState_daemonSet_withResourceTypeMismatch() { + var desiredReplicaSet = loadResource("sample-rs-resources-desired.yaml", ReplicaSet.class); + var actualDaemonSet = loadResource("sample-ds-resources.yaml", DaemonSet.class); + + assertThat(matcher.matches(actualDaemonSet, desiredReplicaSet, mockedContext)).isFalse(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testCustomMatcher_returnsExpectedMatchBasedOnReadOnlyLabel(boolean readOnly) { + var dr = new ConfigMapDR(); + dr.configureWith( + new KubernetesDependentResourceConfigBuilder() + .withSSAMatcher(new ReadOnlyAwareMatcher<>()) + .build()); + var desiredConfigMap = + loadResource("configmap.empty-owner-reference-desired.yaml", ConfigMap.class); + desiredConfigMap.getData().put("key1", "another value"); + var actualConfigMap = loadResource("configmap.empty-owner-reference.yaml", ConfigMap.class); + actualConfigMap.getMetadata().getLabels().put("readonly", Boolean.toString(readOnly)); + + HasMetadata primary = mock(); + assertThat(dr.match(actualConfigMap, desiredConfigMap, primary, mockedContext).matched()) + .isEqualTo(readOnly); + } + + @Test + void keepOnlyManagedFields_withInvalidManagedFieldsKey() { + assertThatThrownBy( + () -> + SSABasedGenericKubernetesResourceMatcher.keepOnlyManagedFields( + Map.of(), + Map.of(), + Map.of("invalid", 1), + mockedContext.getClient().getKubernetesSerialization())) // + .isInstanceOf(IllegalStateException.class) // + .hasMessage("Key: invalid has no prefix: f:"); + } + + @Test + @SuppressWarnings("unchecked") + void testSortMap() { + final var unsortedMap = Map.of("b", Map.of("z", 26, "y", 25), "a", List.of("w", "v"), "c", 2); + + var sortedMap = SSABasedGenericKubernetesResourceMatcher.sortMap(unsortedMap); + assertThat(sortedMap.keySet()).containsExactly("a", "b", "c"); + + var sortedNestedMap = (Map) sortedMap.get("b"); + assertThat(sortedNestedMap.keySet()).containsExactly("y", "z"); + } + + @Test + @SuppressWarnings("unchecked") + void testSortListItems() { + final var unsortedList = + List.of(1, Map.of("z", 26, "y", 25), Map.of("b", 26, "c", 25, "a", 24), List.of("w", "v")); + + var sortedListItems = SSABasedGenericKubernetesResourceMatcher.sortListItems(unsortedList); + assertThat(sortedListItems).element(0).isEqualTo(1); + + var sortedNestedMap1 = (Map) sortedListItems.get(1); + assertThat(sortedNestedMap1.keySet()).containsExactly("y", "z"); + + var sortedNestedMap2 = (Map) sortedListItems.get(2); + assertThat(sortedNestedMap2.keySet()).containsExactly("a", "b", "c"); + } + private static R loadResource(String fileName, Class clazz) { return ReconcilerUtils.loadYaml( clazz, SSABasedGenericKubernetesResourceMatcherTest.class, fileName); } + + private static class ConfigMapDR extends KubernetesDependentResource { + public ConfigMapDR() { + super(ConfigMap.class); + } + } + + private static class ReadOnlyAwareMatcher + extends SSABasedGenericKubernetesResourceMatcher { + @Override + protected boolean matches( + Map actualMap, + Map desiredMap, + T actual, + T desired, + Context context) { + var readonly = actual.getMetadata().getLabels().get("readonly"); + if (readonly != null && readonly.equals("true")) { + return true; + } + return actualMap.equals(desiredMap); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index fe2e6e9514..9819eb7ee9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -276,6 +276,30 @@ void cancelScheduleOnceEventsOnSuccessfulExecution() { verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); } + @Test + void skipsGenericEventIfNoResourceEventReceivedBefore() { + var crID = new ResourceID("test-cr", TEST_NAMESPACE); + eventProcessor = + spy( + new EventProcessor( + controllerConfiguration(null, LinearRateLimiter.deactivatedRateLimiter()), + reconciliationDispatcherMock, + eventSourceManagerMock, + metricsMock)); + + verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); + + eventProcessor.start(); + eventProcessor.handleEvent(new Event(crID)); + + await() + .pollDelay(Duration.ofMillis(100)) + .untilAsserted( + () -> { + verify(reconciliationDispatcherMock, never()).handleExecution(any()); + }); + } + @Test void startProcessedMarkedEventReceivedBefore() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); @@ -287,7 +311,7 @@ void startProcessedMarkedEventReceivedBefore() { eventSourceManagerMock, metricsMock)); when(controllerEventSourceMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); - eventProcessor.handleEvent(new Event(crID)); + eventProcessor.handleEvent(new ResourceEvent(ResourceAction.ADDED, crID, testCustomResource())); verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index 2c4d9fa4f3..487ba25885 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -4,6 +4,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; + import static org.assertj.core.api.Assertions.assertThat; class ResourceStateManagerTest { @@ -87,4 +91,26 @@ public void listsResourceIDSWithEventsPresent() { assertThat(res).hasSize(1); assertThat(res.get(0).getId()).isEqualTo(sampleResourceID2); } + + @Test + void createStateOnlyOnResourceEvent() { + var state = manager.getOrCreateOnResourceEvent(new Event(new ResourceID("newEvent"))); + + assertThat(state).isEmpty(); + + state = + manager.getOrCreateOnResourceEvent( + new ResourceEvent( + ResourceAction.ADDED, new ResourceID("newEvent"), TestUtils.testCustomResource())); + + assertThat(state).isNotNull(); + } + + @Test + void createsOnlyResourceEventReturnsPreviouslyCreatedState() { + manager.getOrCreate(new ResourceID("newEvent")); + + var res = manager.getOrCreateOnResourceEvent(new Event(new ResourceID("newEvent"))); + assertThat(res).isNotNull(); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 3205bca523..a08989c8ce 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -57,7 +57,12 @@ void setup() { .thenReturn(mock(SecondaryToPrimaryMapper.class)); when(informerEventSourceConfiguration.getResourceClass()).thenReturn(Deployment.class); - informerEventSource = new InformerEventSource<>(informerEventSourceConfiguration, clientMock); + informerEventSource = + new InformerEventSource<>(informerEventSourceConfiguration, clientMock) { + // mocking start + @Override + public synchronized void start() {} + }; var mockControllerConfig = mock(ControllerConfiguration.class); when(mockControllerConfig.getConfigurationService()).thenReturn(new BaseConfigurationService()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/MappersTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/MappersTest.java index e8e9d79857..fe091c9698 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/MappersTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/MappersTest.java @@ -29,6 +29,31 @@ void secondaryToPrimaryMapperFromOwnerReference() { assertThat(res).contains(ResourceID.fromResource(primary)); } + @Test + void secondaryToPrimaryMapperFromOwnerReferenceWhereGroupIdIsEmpty() { + var primary = + new ConfigMapBuilder() + .withNewMetadata() + .withName("test") + .withNamespace("default") + .endMetadata() + .build(); + primary.getMetadata().setUid(UUID.randomUUID().toString()); + var secondary = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("test1") + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .build(); + secondary.addOwnerReference(primary); + + var res = Mappers.fromOwnerReferences(ConfigMap.class).toPrimaryResourceIDs(secondary); + + assertThat(res).contains(ResourceID.fromResource(primary)); + } + @Test void secondaryToPrimaryMapperFromOwnerReferenceFiltersByType() { var primary = TestUtils.testCustomResource(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index d31408beb6..e62888832f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -19,7 +19,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class TemporaryResourceCacheTest { +class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java index 0f7d26446d..448537e9aa 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java @@ -87,7 +87,7 @@ void propagatesEventOnNewResourceForPrimary() throws InterruptedException { } @Test - void updatesHealthIndicatorBasedOnExceptionsInFetcher() throws InterruptedException { + void updatesHealthIndicatorBasedOnExceptionsInFetcher() { when(resourceFetcher.fetchResources()).thenReturn(testResponseWithOneValue()); pollingEventSource.start(); assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 9396411777..825bc6adab 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.source.timer; -import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; @@ -12,6 +11,7 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -42,6 +42,7 @@ public void schedulesOnce() { untilAsserted(() -> assertThat(eventHandler.events).hasSize(1)); untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandler.events).hasSize(1)); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -52,6 +53,7 @@ public void canCancelOnce() { source.cancelOnceSchedule(resourceID); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -62,6 +64,7 @@ public void canRescheduleOnceEvent() { source.scheduleOnce(resourceID, 2 * PERIOD); untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandler.events).hasSize(1)); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -72,23 +75,29 @@ public void deRegistersOnceEventSources() { source.onResourceDeleted(customResource); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test - public void eventNotRegisteredIfStopped() throws IOException { + public void eventNotRegisteredIfStopped() { var resourceID = ResourceID.fromResource(TestUtils.testCustomResource()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); source.stop(); assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> source.scheduleOnce(resourceID, PERIOD)); + assertThat(source.getStatus()).isEqualTo(Status.UNHEALTHY); } @Test - public void eventNotFiredIfStopped() throws IOException { + public void eventNotFiredIfStopped() { source.scheduleOnce(ResourceID.fromResource(TestUtils.testCustomResource()), PERIOD); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); + source.stop(); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.UNHEALTHY); } private void untilAsserted(ThrowingRunnable assertion) { diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/configmap.empty-owner-reference-desired.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/configmap.empty-owner-reference-desired.yaml index 3a9d018266..01d27e39b3 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/configmap.empty-owner-reference-desired.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/configmap.empty-owner-reference-desired.yaml @@ -10,5 +10,3 @@ metadata: uid: 1ef74cb4-dbbd-45ef-9caf-aa76186594ea data: key1: "val1" - - diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml index d82b5c8933..38358a16c0 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/deployment-with-managed-fields-additional-controller.yaml @@ -25,6 +25,7 @@ metadata: f:image: {} f:name: {} f:ports: + .: {} k:{"containerPort":80,"protocol":"TCP"}: .: {} f:containerPort: {} diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod-desired.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod-desired.yaml index 92ece6df00..e400532fad 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod-desired.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod-desired.yaml @@ -18,4 +18,4 @@ spec: - name: shared-data mountPath: /data command: ["/bin/sh"] - args: ["-c", "echo Level Up Blue Team! > /data/index.html"] \ No newline at end of file + args: ["-c", "echo Level Up Blue Team! > /data/index.html"] diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod.yaml index e1334117b6..6a5f2d82b4 100644 --- a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod.yaml +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/multi-container-pod.yaml @@ -211,4 +211,4 @@ status: podIPs: - ip: 10.244.0.3 qosClass: BestEffort - startTime: "2023-06-08T11:50:59Z" \ No newline at end of file + startTime: "2023-06-08T11:50:59Z" diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-desired.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-desired.yaml new file mode 100644 index 0000000000..29f9866592 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-desired.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test1 + namespace: default +data: + key1: "dmFsMQ==" diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer-desired.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer-desired.yaml new file mode 100644 index 0000000000..f0e64b1a60 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer-desired.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + finalizers: + - test-finalizer + name: test1 + namespace: default +data: + key1: "dmFsMQ==" diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer.yaml new file mode 100644 index 0000000000..fa9ffc13a0 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret-with-finalizer.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +data: + key1: "dmFsMQ==" +kind: Secret +metadata: + creationTimestamp: "2023-06-07T11:08:34Z" + finalizers: + - test-finalizer + managedFields: + - apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:data: + f:key1: {} + f:metadata: + f:finalizers: + .: {} + v:"test-finalizer": {} + manager: controller + operation: Apply + time: "2023-06-07T11:08:34Z" + name: test1 + namespace: default + resourceVersion: "400" + uid: 1d47f98f-ff1e-46d8-bbb5-6658ec488ae2 diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret.yaml new file mode 100644 index 0000000000..a6dc3b3c3e --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/secret.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +data: + key1: "dmFsMQ==" +kind: Secret +metadata: + creationTimestamp: "2023-06-07T11:08:34Z" + managedFields: + - apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:data: + f:key1: {} + manager: controller + operation: Apply + time: "2023-06-07T11:08:34Z" + name: test1 + namespace: default + resourceVersion: "400" + uid: 1d47f98f-ff1e-46d8-bbb5-6658ec488ae2 diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index d2075303be..5aeeb92d45 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 00bf7e8380..794bc11d9a 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -111,6 +111,10 @@ public T create(T resource) { return kubernetesClient.resource(resource).inNamespace(namespace).create(); } + public T serverSideApply(T resource) { + return kubernetesClient.resource(resource).inNamespace(namespace).serverSideApply(); + } + public T replace(T resource) { return kubernetesClient.resource(resource).inNamespace(namespace).replace(); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index f0bb5194b2..54cb57544d 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -42,7 +42,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension { private static final Logger LOGGER = LoggerFactory.getLogger(LocallyRunOperatorExtension.class); - private static final int CRD_DELETE_TIMEOUT = 1000; + private static final int CRD_DELETE_TIMEOUT = 5000; private static final Set appliedCRDs = new HashSet<>(); private static final boolean deleteCRDs = Boolean.parseBoolean(System.getProperty("testsuite.deleteCRDs", "true")); @@ -54,6 +54,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension { private final List> additionalCustomResourceDefinitions; private final Map registeredControllers; private final Map crdMappings; + private final Consumer beforeStartHook; private LocallyRunOperatorExtension( List reconcilers, @@ -68,7 +69,8 @@ private LocallyRunOperatorExtension( Consumer configurationServiceOverrider, Function namespaceNameSupplier, Function perClassNamespaceNameSupplier, - List additionalCrds) { + List additionalCrds, + Consumer beforeStartHook) { super( infrastructure, infrastructureTimeout, @@ -82,6 +84,7 @@ private LocallyRunOperatorExtension( this.portForwards = portForwards; this.localPortForwards = new ArrayList<>(portForwards.size()); this.additionalCustomResourceDefinitions = additionalCustomResourceDefinitions; + this.beforeStartHook = beforeStartHook; configurationServiceOverrider = configurationServiceOverrider != null ? configurationServiceOverrider.andThen( @@ -298,6 +301,10 @@ protected void before(ExtensionContext context) { }); crdMappings.clear(); + if (beforeStartHook != null) { + beforeStartHook.accept(this); + } + LOGGER.debug("Starting the operator locally"); this.operator.start(); } @@ -343,7 +350,8 @@ private void deleteCrd(AppliedCRD appliedCRD, KubernetesClient client) { crd.withTimeoutInMillis(CRD_DELETE_TIMEOUT).delete(); LOGGER.debug("Deleted CRD with path: {}", appliedCRD.path); } catch (Exception ex) { - throw new IllegalStateException("Cannot delete CRD yaml: " + appliedCRD.path, ex); + LOGGER.warn( + "Cannot delete CRD yaml: {}. You might need to delete it manually.", appliedCRD.path, ex); } } @@ -355,6 +363,7 @@ public static class Builder extends AbstractBuilder { private final List portForwards; private final List> additionalCustomResourceDefinitions; private final List additionalCRDs = new ArrayList<>(); + private Consumer beforeStartHook; private KubernetesClient kubernetesClient; protected Builder() { @@ -423,6 +432,15 @@ public Builder withAdditionalCRD(String... paths) { return this; } + /** + * Used to initialize resources when the namespace is generated but the operator is not started + * yet. + */ + public Builder withBeforeStartHook(Consumer beforeStartHook) { + this.beforeStartHook = beforeStartHook; + return this; + } + public LocallyRunOperatorExtension build() { return new LocallyRunOperatorExtension( reconcilers, @@ -437,7 +455,8 @@ public LocallyRunOperatorExtension build() { configurationServiceOverrider, namespaceNameSupplier, perClassNamespaceNameSupplier, - additionalCRDs); + additionalCRDs, + beforeStartHook); } } diff --git a/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java b/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java similarity index 95% rename from operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java rename to operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java index 04ac7a91ae..e195ce8406 100644 --- a/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java +++ b/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*; -class LocallyRunOperatorExtensionTest { +class LocallyRunOperatorExtensionIT { @Test void getAdditionalCRDsFromFiles() { diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index e2aae87401..f1a100eb75 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT operator-framework @@ -125,6 +125,15 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + META-INF/fabric8/*.yml + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalCustomResource.java new file mode 100644 index 0000000000..3f9b61d1a8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("cfr") +public class ConcurrentFinalizerRemovalCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java new file mode 100644 index 0000000000..da166ce2c1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java @@ -0,0 +1,68 @@ +package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class ConcurrentFinalizerRemovalIT { + + private static final Logger log = LoggerFactory.getLogger(ConcurrentFinalizerRemovalIT.class); + public static final String TEST_RESOURCE_NAME = "test"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + // should work without a retry, thus not retry the whole reconciliation but to retry + // finalizer removal only. + .withReconciler( + new ConcurrentFinalizerRemovalReconciler1(), + o -> + o.withRetry(GenericRetry.noRetry()).withFinalizer("reconciler1.sample/finalizer")) + .withReconciler( + new ConcurrentFinalizerRemovalReconciler2(), + o -> + o.withRetry(GenericRetry.noRetry()).withFinalizer("reconciler2.sample/finalizer")) + .build(); + + @Test + void concurrentFinalizerRemoval() { + for (int i = 0; i < 10; i++) { + var resource = extension.create(createResource()); + await() + .untilAsserted( + () -> { + var res = + extension.get( + ConcurrentFinalizerRemovalCustomResource.class, TEST_RESOURCE_NAME); + assertThat(res.getMetadata().getFinalizers()).hasSize(2); + }); + resource.getMetadata().setResourceVersion(null); + extension.delete(resource); + + await() + .untilAsserted( + () -> { + var res = + extension.get( + ConcurrentFinalizerRemovalCustomResource.class, TEST_RESOURCE_NAME); + assertThat(res).isNull(); + }); + } + } + + public ConcurrentFinalizerRemovalCustomResource createResource() { + ConcurrentFinalizerRemovalCustomResource res = new ConcurrentFinalizerRemovalCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); + res.setSpec(new ConcurrentFinalizerRemovalSpec()); + res.getSpec().setNumber(0); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler1.java new file mode 100644 index 0000000000..b789255cb5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler1.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval; + +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class ConcurrentFinalizerRemovalReconciler1 + implements Reconciler, + Cleaner { + + @Override + public UpdateControl reconcile( + ConcurrentFinalizerRemovalCustomResource resource, + Context context) { + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup( + ConcurrentFinalizerRemovalCustomResource resource, + Context context) + throws Exception { + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler2.java new file mode 100644 index 0000000000..0b8993a8f5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalReconciler2.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval; + +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class ConcurrentFinalizerRemovalReconciler2 + implements Reconciler, + Cleaner { + + @Override + public UpdateControl reconcile( + ConcurrentFinalizerRemovalCustomResource resource, + Context context) { + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup( + ConcurrentFinalizerRemovalCustomResource resource, + Context context) + throws Exception { + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalSpec.java new file mode 100644 index 0000000000..d740721f30 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval; + +public class ConcurrentFinalizerRemovalSpec { + + private int number; + + public int getNumber() { + return number; + } + + public ConcurrentFinalizerRemovalSpec setNumber(int number) { + this.number = number; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java index cd63c708e9..a835dd2de6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.baseapi.patchresourceandstatusnossa; +import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -9,12 +10,14 @@ import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.TestUtils; +import static io.javaoperatorsdk.operator.baseapi.patchresourceandstatusnossa.PatchResourceAndStatusNoSSAReconciler.TEST_ANNOTATION; +import static io.javaoperatorsdk.operator.baseapi.patchresourceandstatusnossa.PatchResourceAndStatusNoSSAReconciler.TEST_ANNOTATION_VALUE; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class PatchResourceAndStatusNoSSAIT { @RegisterExtension - LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withConfigurationService(o -> o.withUseSSAToPatchPrimaryResource(false)) .withReconciler(PatchResourceAndStatusNoSSAReconciler.class) @@ -22,26 +25,47 @@ class PatchResourceAndStatusNoSSAIT { @Test void updatesSubResourceStatus() { + extension + .getReconcilerOfType(PatchResourceAndStatusNoSSAReconciler.class) + .setRemoveAnnotation(false); PatchResourceAndStatusNoSSACustomResource resource = createTestCustomResource("1"); - operator.create(resource); + extension.create(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events TestUtils.waitXms(300); PatchResourceAndStatusNoSSACustomResource customResource = - operator.get( + extension.get( PatchResourceAndStatusNoSSACustomResource.class, resource.getMetadata().getName()); - assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(1); + assertThat(TestUtils.getNumberOfExecutions(extension)).isEqualTo(1); assertThat(customResource.getStatus().getState()) .isEqualTo(PatchResourceAndStatusNoSSAStatus.State.SUCCESS); - assertThat( - customResource - .getMetadata() - .getAnnotations() - .get(PatchResourceAndStatusNoSSAReconciler.TEST_ANNOTATION)) - .isNotNull(); + assertThat(customResource.getMetadata().getAnnotations().get(TEST_ANNOTATION)).isNotNull(); + } + + @Test + void removeAnnotationCorrectlyUpdatesStatus() { + extension + .getReconcilerOfType(PatchResourceAndStatusNoSSAReconciler.class) + .setRemoveAnnotation(true); + PatchResourceAndStatusNoSSACustomResource resource = createTestCustomResource("1"); + resource.getMetadata().setAnnotations(Map.of(TEST_ANNOTATION, TEST_ANNOTATION_VALUE)); + extension.create(resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + TestUtils.waitXms(300); + + PatchResourceAndStatusNoSSACustomResource customResource = + extension.get( + PatchResourceAndStatusNoSSACustomResource.class, resource.getMetadata().getName()); + + assertThat(TestUtils.getNumberOfExecutions(extension)).isEqualTo(1); + assertThat(customResource.getStatus().getState()) + .isEqualTo(PatchResourceAndStatusNoSSAStatus.State.SUCCESS); + assertThat(customResource.getMetadata().getAnnotations().get(TEST_ANNOTATION)).isNull(); } void awaitStatusUpdated(String name) { @@ -50,7 +74,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { PatchResourceAndStatusNoSSACustomResource cr = - operator.get(PatchResourceAndStatusNoSSACustomResource.class, name); + extension.get(PatchResourceAndStatusNoSSACustomResource.class, name); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getState()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java index a104ca4185..2d3a282b01 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java @@ -22,6 +22,8 @@ public class PatchResourceAndStatusNoSSAReconciler public static final String TEST_ANNOTATION_VALUE = "TestAnnotationValue"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private volatile boolean removeAnnotation = false; + @Override public UpdateControl reconcile( PatchResourceAndStatusNoSSACustomResource resource, @@ -30,8 +32,12 @@ public UpdateControl reconcile( log.info("Value: " + resource.getSpec().getValue()); - resource.getMetadata().setAnnotations(new HashMap<>()); - resource.getMetadata().getAnnotations().put(TEST_ANNOTATION, TEST_ANNOTATION_VALUE); + if (removeAnnotation) { + resource.getMetadata().getAnnotations().remove(TEST_ANNOTATION); + } else { + resource.getMetadata().setAnnotations(new HashMap<>()); + resource.getMetadata().getAnnotations().put(TEST_ANNOTATION, TEST_ANNOTATION_VALUE); + } ensureStatusExists(resource); resource.getStatus().setState(PatchResourceAndStatusNoSSAStatus.State.SUCCESS); @@ -49,4 +55,8 @@ private void ensureStatusExists(PatchResourceAndStatusNoSSACustomResource resour public int getNumberOfExecutions() { return numberOfExecutions.get(); } + + public void setRemoveAnnotation(boolean removeAnnotation) { + this.removeAnnotation = removeAnnotation; + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Cluster.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Cluster.java index d0be7738a6..1d154cd6a8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Cluster.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Cluster.java @@ -9,4 +9,4 @@ @Group("sample.javaoperatorsdk") @Version("v1") @ShortNames("clu") -public class Cluster extends CustomResource implements Namespaced {} +public class Cluster extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/ClusterSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/ClusterSpec.java new file mode 100644 index 0000000000..b948bea6b4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/ClusterSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.primarytosecondary; + +public class ClusterSpec { + + private String clusterValue; + + public String getClusterValue() { + return clusterValue; + } + + public void setClusterValue(String clusterValue) { + this.clusterValue = clusterValue; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Job.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Job.java index 611898bd52..bec3598e73 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Job.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/Job.java @@ -9,4 +9,4 @@ @Group("sample.javaoperatorsdk") @Version("v1") @ShortNames("cjo") -public class Job extends CustomResource implements Namespaced {} +public class Job extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobReconciler.java index 1855f89b77..6c51a06b1c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobReconciler.java @@ -17,7 +17,7 @@ * needed, and to show the use cases when some mechanisms would not work without that. It's not * intended to be a reusable code as it is, rather serves for deeper understanding of the problem. */ -@ControllerConfiguration() +@ControllerConfiguration public class JobReconciler implements Reconciler { private static final String JOB_CLUSTER_INDEX = "job-cluster-index"; @@ -38,26 +38,37 @@ public JobReconciler(boolean addPrimaryToSecondaryMapper) { @Override public UpdateControl reconcile(Job resource, Context context) { - + Cluster cluster; if (!getResourceDirectlyFromCache) { // this is only possible when there is primary to secondary mapper - context - .getSecondaryResource(Cluster.class) - .orElseThrow(() -> new IllegalStateException("Secondary resource should be present")); + cluster = + context + .getSecondaryResource(Cluster.class) + .orElseThrow(() -> new IllegalStateException("Secondary resource should be present")); } else { // reading the resource from cache as alternative, works without primary to secondary mapper var informerEventSource = (InformerEventSource) context.eventSourceRetriever().getEventSourceFor(Cluster.class); - informerEventSource - .get( - new ResourceID( - resource.getSpec().getClusterName(), resource.getMetadata().getNamespace())) - .orElseThrow( - () -> new IllegalStateException("Secondary resource cannot be read from cache")); + cluster = + informerEventSource + .get( + new ResourceID( + resource.getSpec().getClusterName(), resource.getMetadata().getNamespace())) + .orElseThrow( + () -> new IllegalStateException("Secondary resource cannot be read from cache")); + } + if (resource.getStatus() == null) { + resource.setStatus(new JobStatus()); } numberOfExecutions.addAndGet(1); - return UpdateControl.noUpdate(); + // copy a value to job status, to we can test triggering + if (!cluster.getSpec().getClusterValue().equals(resource.getStatus().getValueFromCluster())) { + resource.getStatus().setValueFromCluster(cluster.getSpec().getClusterValue()); + return UpdateControl.patchStatus(resource); + } else { + return UpdateControl.noUpdate(); + } } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobStatus.java new file mode 100644 index 0000000000..d59634b295 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/JobStatus.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.primarytosecondary; + +public class JobStatus { + + private String valueFromCluster; + + public String getValueFromCluster() { + return valueFromCluster; + } + + public void setValueFromCluster(String valueFromCluster) { + this.valueFromCluster = valueFromCluster; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java index 9344cc787f..d82c24a55f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java @@ -16,8 +16,12 @@ class PrimaryToSecondaryIT { public static final String CLUSTER_NAME = "cluster1"; public static final int MIN_DELAY = 150; + public static final String CLUSTER_VALUE = "clusterValue"; + public static final String JOB_1 = "job1"; + public static final String CHANGED_VALUE = "CHANGED_VALUE"; + @RegisterExtension - LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withAdditionalCustomResourceDefinition(Cluster.class) .withReconciler(new JobReconciler()) @@ -25,22 +29,37 @@ class PrimaryToSecondaryIT { @Test void readsSecondaryInManyToOneCases() throws InterruptedException { - operator.create(cluster()); + var cluster = extension.create(cluster()); Thread.sleep(MIN_DELAY); - operator.create(job()); + extension.create(job()); + + await() + .pollDelay(Duration.ofMillis(300)) + .untilAsserted( + () -> { + assertThat(extension.getReconcilerOfType(JobReconciler.class).getNumberOfExecutions()) + .isEqualTo(1); + var job = extension.get(Job.class, JOB_1); + assertThat(job.getStatus()).isNotNull(); + assertThat(job.getStatus().getValueFromCluster()).isEqualTo(CLUSTER_VALUE); + }); + + cluster.getSpec().setClusterValue(CHANGED_VALUE); + extension.replace(cluster); + // cluster change triggers job reconciliations await() .pollDelay(Duration.ofMillis(300)) .untilAsserted( - () -> - assertThat( - operator.getReconcilerOfType(JobReconciler.class).getNumberOfExecutions()) - .isEqualTo(1)); + () -> { + var job = extension.get(Job.class, JOB_1); + assertThat(job.getStatus().getValueFromCluster()).isEqualTo(CHANGED_VALUE); + }); } public static Job job() { var job = new Job(); - job.setMetadata(new ObjectMetaBuilder().withName("job1").build()); + job.setMetadata(new ObjectMetaBuilder().withName(JOB_1).build()); job.setSpec(new JobSpec()); job.getSpec().setClusterName(CLUSTER_NAME); return job; @@ -49,6 +68,8 @@ public static Job job() { public static Cluster cluster() { Cluster cluster = new Cluster(); cluster.setMetadata(new ObjectMetaBuilder().withName(CLUSTER_NAME).build()); + cluster.setSpec(new ClusterSpec()); + cluster.getSpec().setClusterValue(CLUSTER_VALUE); return cluster; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java new file mode 100644 index 0000000000..0e31be2dfb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("ssfi") +public class SSAFinalizerIssueCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java new file mode 100644 index 0000000000..a830675518 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java @@ -0,0 +1,66 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class SSAFinalizerIssueIT { + + public static final String TEST_1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new SSAFinalizerIssueReconciler()) + .build(); + + /** + * Showcases possibly a case with SSA: When the resource is created with the same field manager + * that used by the controller, when adding a finalizer, it deletes other parts of the spec. + */ + @Test + void addingFinalizerRemoveListValues() { + var fieldManager = + extension + .getRegisteredControllerForReconcile(SSAFinalizerIssueReconciler.class) + .getConfiguration() + .fieldManager(); + + extension + .getKubernetesClient() + .resource(testResource()) + .inNamespace(extension.getNamespace()) + .patch( + new PatchContext.Builder() + .withFieldManager(fieldManager) + .withForce(true) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()); + + await() + .untilAsserted( + () -> { + var actual = extension.get(SSAFinalizerIssueCustomResource.class, TEST_1); + assertThat(actual.getFinalizers()).hasSize(1); + assertThat(actual.getSpec()).isNull(); + }); + } + + SSAFinalizerIssueCustomResource testResource() { + var res = new SSAFinalizerIssueCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_1).build()); + res.setSpec(new SSAFinalizerIssueSpec()); + res.getSpec().setValue("val"); + res.getSpec().setList(List.of("val1", "val2")); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java new file mode 100644 index 0000000000..441819834a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class SSAFinalizerIssueReconciler + implements Reconciler, + Cleaner { + + @Override + public DeleteControl cleanup( + SSAFinalizerIssueCustomResource resource, Context context) { + return DeleteControl.defaultDelete(); + } + + @Override + public UpdateControl reconcile( + SSAFinalizerIssueCustomResource resource, Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java new file mode 100644 index 0000000000..d1c9dd5966 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import java.util.List; + +public class SSAFinalizerIssueSpec { + + private String value; + + private List list = null; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java new file mode 100644 index 0000000000..471372af40 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +public class SSAFinalizerIssueStatus { + + private String configMapStatus; + + public String getConfigMapStatus() { + return configMapStatus; + } + + public void setConfigMapStatus(String configMapStatus) { + this.configMapStatus = configMapStatus; + } + + @Override + public String toString() { + return "TestCustomResourceStatus{" + "configMapStatus='" + configMapStatus + '\'' + '}'; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java new file mode 100644 index 0000000000..c4ee2dd1a5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("ssul") +public class SSASpecUpdateCustomResource + extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java new file mode 100644 index 0000000000..47953ccce0 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +public class SSASpecUpdateCustomResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java new file mode 100644 index 0000000000..e5f103dc69 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +public class SSASpecUpdateCustomResourceStatus { + + private Integer value = 0; + + public Integer getValue() { + return value; + } + + public SSASpecUpdateCustomResourceStatus setValue(Integer value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java new file mode 100644 index 0000000000..9cccd32e3c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class SSASpecUpdateIT { + + public static final String TEST_RESOURCE_NAME = "test"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder().withReconciler(SSASpecUpdateReconciler.class).build(); + + // showcases that if the spec of the resources is updated with SSA, but the finalizer + // is not explicitly added to the fresh resource, the update removes the finalizer + @Test + void showFinalizerRemovalWhenSpecUpdated() { + SSASpecUpdateCustomResource res = createResource(); + operator.create(res); + + await() + .untilAsserted( + () -> { + var actual = operator.get(SSASpecUpdateCustomResource.class, TEST_RESOURCE_NAME); + assertThat(actual.getSpec()).isNotNull(); + assertThat(actual.getFinalizers()).isEmpty(); + }); + } + + SSASpecUpdateCustomResource createResource() { + SSASpecUpdateCustomResource res = new SSASpecUpdateCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java new file mode 100644 index 0000000000..483060ad59 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class SSASpecUpdateReconciler + implements Reconciler, Cleaner { + + @Override + public UpdateControl reconcile( + SSASpecUpdateCustomResource resource, Context context) { + + var copy = createFreshCopy(resource); + copy.getSpec().setValue("value"); + context + .getClient() + .resource(copy) + .fieldManager(context.getControllerConfiguration().fieldManager()) + .serverSideApply(); + + return UpdateControl.noUpdate(); + } + + SSASpecUpdateCustomResource createFreshCopy(SSASpecUpdateCustomResource resource) { + var res = new SSASpecUpdateCustomResource(); + res.setMetadata( + new ObjectMetaBuilder() + .withName(resource.getMetadata().getName()) + .withNamespace(resource.getMetadata().getNamespace()) + .build()); + res.setSpec(new SSASpecUpdateCustomResourceSpec()); + return res; + } + + @Override + public DeleteControl cleanup( + SSASpecUpdateCustomResource resource, Context context) { + + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java new file mode 100644 index 0000000000..b9701c94bd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("ssac") +public class StartupSecondaryAccessCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java new file mode 100644 index 0000000000..61fc40803c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessReconciler.LABEL_KEY; +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessReconciler.LABEL_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class StartupSecondaryAccessIT { + + public static final int SECONDARY_NUMBER = 200; + + @RegisterExtension + static LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new StartupSecondaryAccessReconciler()) + .withBeforeStartHook( + ex -> { + var primary = new StartupSecondaryAccessCustomResource(); + primary.setMetadata(new ObjectMetaBuilder().withName("test1").build()); + primary = ex.serverSideApply(primary); + + for (int i = 0; i < SECONDARY_NUMBER; i++) { + ConfigMap cm = new ConfigMap(); + cm.setMetadata( + new ObjectMetaBuilder() + .withLabels(Map.of(LABEL_KEY, LABEL_VALUE)) + .withNamespace(ex.getNamespace()) + .withName("cm" + i) + .build()); + cm.addOwnerReference(primary); + ex.serverSideApply(cm); + } + }) + .build(); + + @Test + void reconcilerSeeAllSecondaryResources() { + var reconciler = extension.getReconcilerOfType(StartupSecondaryAccessReconciler.class); + + await().untilAsserted(() -> assertThat(reconciler.isReconciled()).isTrue()); + + assertThat(reconciler.isSecondaryAndCacheSameAmount()).isTrue(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java new file mode 100644 index 0000000000..a2c51fdafd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java @@ -0,0 +1,75 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessIT.SECONDARY_NUMBER; + +@ControllerConfiguration +public class StartupSecondaryAccessReconciler + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(StartupSecondaryAccessReconciler.class); + + public static final String LABEL_KEY = "app"; + public static final String LABEL_VALUE = "secondary-test"; + + private InformerEventSource cmInformer; + + private boolean secondaryAndCacheSameAmount = true; + private boolean reconciled = false; + + @Override + public UpdateControl reconcile( + StartupSecondaryAccessCustomResource resource, + Context context) { + + var secondary = context.getSecondaryResources(ConfigMap.class); + var cached = cmInformer.list().toList(); + + log.info( + "Secondary number: {}, cached: {}, expected: {}", + secondary.size(), + cached.size(), + SECONDARY_NUMBER); + + if (secondary.size() != cached.size()) { + secondaryAndCacheSameAmount = false; + } + reconciled = true; + return UpdateControl.noUpdate(); + } + + @Override + public List> prepareEventSources( + EventSourceContext context) { + cmInformer = + new InformerEventSource<>( + InformerEventSourceConfiguration.from( + ConfigMap.class, StartupSecondaryAccessCustomResource.class) + .withLabelSelector(LABEL_KEY + "=" + LABEL_VALUE) + .build(), + context); + return List.of(cmInformer); + } + + public boolean isSecondaryAndCacheSameAmount() { + return secondaryAndCacheSameAmount; + } + + public boolean isReconciled() { + return reconciled; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/PeriodicTriggerEventSource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/PeriodicTriggerEventSource.java new file mode 100644 index 0000000000..366777409a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/PeriodicTriggerEventSource.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; + +public class PeriodicTriggerEventSource

+ extends AbstractEventSource { + + public static final int DEFAULT_PERIOD = 30; + private final Timer timer = new Timer(); + private final IndexerResourceCache

primaryCache; + private final int period; + + public PeriodicTriggerEventSource(IndexerResourceCache

primaryCache) { + this(primaryCache, DEFAULT_PERIOD); + } + + public PeriodicTriggerEventSource(IndexerResourceCache

primaryCache, int period) { + super(Void.class); + this.primaryCache = primaryCache; + this.period = period; + } + + @Override + public Set getSecondaryResources(P primary) { + return Set.of(); + } + + @Override + public void start() throws OperatorException { + super.start(); + timer.schedule( + new TimerTask() { + @Override + public void run() { + primaryCache + .list() + .forEach(r -> getEventHandler().handleEvent(new Event(ResourceID.fromResource(r)))); + } + }, + 0, + period); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheCustomResource.java new file mode 100644 index 0000000000..e87d8e8714 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("spwl") +public class StatusPatchCacheCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java new file mode 100644 index 0000000000..9d0b923056 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class StatusPatchCacheIT { + + public static final String TEST_1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(StatusPatchCacheReconciler.class) + .build(); + + @Test + void testStatusAlwaysUpToDate() { + var reconciler = extension.getReconcilerOfType(StatusPatchCacheReconciler.class); + + extension.create(testResource()); + + // the reconciliation is periodically triggered, the status values should be increasing + // monotonically + await() + .pollDelay(Duration.ofSeconds(1)) + .pollInterval(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.errorPresent).isFalse(); + assertThat(reconciler.latestValue).isGreaterThan(10); + }); + } + + StatusPatchCacheCustomResource testResource() { + var res = new StatusPatchCacheCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_1).build()); + res.setSpec(new StatusPatchCacheSpec()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheReconciler.java new file mode 100644 index 0000000000..69215d6d01 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheReconciler.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +import java.util.List; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@ControllerConfiguration +public class StatusPatchCacheReconciler implements Reconciler { + + public volatile int latestValue = 0; + public volatile boolean errorPresent = false; + + @Override + public UpdateControl reconcile( + StatusPatchCacheCustomResource resource, Context context) { + + if (resource.getStatus() != null && resource.getStatus().getValue() != latestValue) { + errorPresent = true; + throw new IllegalStateException( + "status is not up to date. Latest value: " + + latestValue + + " status values: " + + resource.getStatus().getValue()); + } + + // test also resource update happening meanwhile reconciliation + resource.getSpec().setCounter(resource.getSpec().getCounter() + 1); + context.getClient().resource(resource).update(); + + var freshCopy = createFreshCopy(resource); + + freshCopy + .getStatus() + .setValue(resource.getStatus() == null ? 1 : resource.getStatus().getValue() + 1); + + var updated = + PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context); + latestValue = updated.getStatus().getValue(); + + return UpdateControl.noUpdate(); + } + + @Override + public List> prepareEventSources( + EventSourceContext context) { + // periodic event triggering for testing purposes + return List.of(new PeriodicTriggerEventSource<>(context.getPrimaryCache())); + } + + private StatusPatchCacheCustomResource createFreshCopy(StatusPatchCacheCustomResource resource) { + var res = new StatusPatchCacheCustomResource(); + res.setMetadata( + new ObjectMetaBuilder() + .withName(resource.getMetadata().getName()) + .withNamespace(resource.getMetadata().getNamespace()) + .build()); + res.setStatus(new StatusPatchCacheStatus()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheSpec.java new file mode 100644 index 0000000000..0885b6a858 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +public class StatusPatchCacheSpec { + + private int counter = 0; + + public int getCounter() { + return counter; + } + + public void setCounter(int counter) { + this.counter = counter; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheStatus.java new file mode 100644 index 0000000000..5918b2e3b8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.baseapi.statuscache; + +public class StatusPatchCacheStatus { + + private Integer value = 0; + + public Integer getValue() { + return value; + } + + public StatusPatchCacheStatus setValue(Integer value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiablePartConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiablePartConfigMapDependent.java index a4559055a4..c6f0759410 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiablePartConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiablePartConfigMapDependent.java @@ -14,10 +14,6 @@ public class UnmodifiablePartConfigMapDependent public static final String UNMODIFIABLE_INITIAL_DATA_KEY = "initialDataKey"; public static final String ACTUAL_DATA_KEY = "actualDataKey"; - public UnmodifiablePartConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( UnmodifiableDependentPartCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index 2f202436ff..25926e6405 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -414,12 +414,7 @@ public UpdateControl reconcile( @KubernetesDependent(useSSA = BooleanWithUndefined.TRUE) public static class WithAnnotation - extends CRUDKubernetesDependentResource { - - public WithAnnotation() { - super(ConfigMap.class); - } - } + extends CRUDKubernetesDependentResource {} } public static class MissingAnnotationReconciler implements Reconciler { @@ -443,18 +438,10 @@ public UpdateControl reconcile(ConfigMap resource, Context } private static class DefaultDependent - extends KubernetesDependentResource { - public DefaultDependent() { - super(ConfigMapReader.class); - } - } + extends KubernetesDependentResource {} @KubernetesDependent(useSSA = BooleanWithUndefined.FALSE) - private static class NonSSADependent extends KubernetesDependentResource { - public NonSSADependent() { - super(Service.class); - } - } + private static class NonSSADependent extends KubernetesDependentResource {} } public static class TestRetry implements Retry, AnnotationConfigurable { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/ConfigMapDeleterBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/ConfigMapDeleterBulkDependentResource.java index a9163a4ec3..cf3c96b82a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/ConfigMapDeleterBulkDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/ConfigMapDeleterBulkDependentResource.java @@ -21,10 +21,6 @@ public class ConfigMapDeleterBulkDependentResource public static final String ADDITIONAL_DATA_KEY = "additionalData"; public static final String INDEX_DELIMITER = "-"; - public ConfigMapDeleterBulkDependentResource() { - super(ConfigMap.class); - } - @Override public Map desiredResources( BulkDependentTestCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentResource.java index b61ad7c230..1eab400888 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentResource.java @@ -23,10 +23,6 @@ public class ReadOnlyBulkDependentResource public static final String INDEX_DELIMITER = "-"; - public ReadOnlyBulkDependentResource() { - super(ConfigMap.class); - } - @Override public Map getSecondaryResources( BulkDependentTestCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/ConfigMapDependentResource.java index dfb3cf2ee2..3c94775045 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/ConfigMapDependentResource.java @@ -19,10 +19,6 @@ public class ConfigMapDependentResource private static final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( CleanerForManagedDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java index 14ba61513a..2e37413766 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java @@ -11,9 +11,7 @@ public class ConfigMapDependentResource extends CRUDKubernetesDependentResource< ConfigMap, CreateOnlyIfNotExistingDependentWithSSACustomResource> { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } + public static final String DRKEY = "drkey"; @Override protected ConfigMap desired( @@ -25,7 +23,7 @@ protected ConfigMap desired( .withName(primary.getMetadata().getName()) .withNamespace(primary.getMetadata().getNamespace()) .build()); - configMap.setData(Map.of("drkey", "v")); + configMap.setData(Map.of(DRKEY, "v")); return configMap; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java index 5c1923fa55..3c41bae977 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -22,6 +23,9 @@ class CreateOnlyIfNotExistingDependentWithSSAIT { @RegisterExtension LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() + // for the sake of this test, we allow to manage ConfigMaps with SSA + // by removing it from the non SSA resources (it is not managed with SSA by default) + .withConfigurationService(o -> o.withDefaultNonSSAResource(Set.of())) .withReconciler(new CreateOnlyIfNotExistingDependentWithSSAReconciler()) .build(); @@ -41,7 +45,7 @@ void createsResourceOnlyIfNotExisting() { .untilAsserted( () -> { var currentCM = extension.get(ConfigMap.class, TEST_RESOURCE_NAME); - assertThat(currentCM.getData()).containsKey(KEY); + assertThat(currentCM.getData()).containsOnlyKeys(KEY); }); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java index 2ba4ee5ef0..71146df638 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java @@ -41,10 +41,6 @@ public static class ConfigMapDependentResource Updater, Deleter { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentAnnotationSecondaryMapperResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/CustomMappingConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/CustomMappingConfigMapDependentResource.java index 0a3aeba0e1..081cf31dbd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/CustomMappingConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/CustomMappingConfigMapDependentResource.java @@ -30,10 +30,6 @@ public class CustomMappingConfigMapDependentResource CUSTOM_TYPE_KEY, DependentCustomMappingCustomResource.class); - public CustomMappingConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentCustomMappingCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java index f4dc408825..30e0de5b7d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java @@ -15,10 +15,6 @@ public class ConfigMapDependentResource public static final String NAMESPACE = "default"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentDifferentNamespaceCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/FilteredDependentConfigMap.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/FilteredDependentConfigMap.java index 3ba4df63f4..3b12673b4c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/FilteredDependentConfigMap.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/FilteredDependentConfigMap.java @@ -15,10 +15,6 @@ public class FilteredDependentConfigMap extends CRUDKubernetesDependentResource { - public FilteredDependentConfigMap() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentFilterTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/ConfigMapDependentResource.java index 13879227e1..87b827c527 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/ConfigMapDependentResource.java @@ -13,10 +13,6 @@ public class ConfigMapDependentResource public static final String KEY = "key1"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentOperationEventFilterCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/ConfigMapDependentResource.java index 704febbf67..2a245a3721 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/ConfigMapDependentResource.java @@ -11,10 +11,6 @@ public class ConfigMapDependentResource extends CRUDKubernetesDependentResource { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentReInitializationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java index ae5cd25895..1b71c79448 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java @@ -44,6 +44,22 @@ void dependentResourceCanReferenceEachOther() { assertThat(operator.get(Secret.class, TEST_RESOURCE_NAME + i)).isNotNull(); } }); + + for (int i = 0; i < EXECUTION_NUMBER; i++) { + operator.delete(testResource(i)); + } + await() + .timeout(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + for (int i = 0; i < EXECUTION_NUMBER; i++) { + assertThat( + operator.get( + DependentResourceCrossRefResource.class, + testResource(i).getMetadata().getName())) + .isNull(); + } + }); } DependentResourceCrossRefResource testResource(int n) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java index 51a285aa4b..247174838c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java @@ -5,6 +5,9 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; @@ -26,6 +29,8 @@ @ControllerConfiguration public class DependentResourceCrossRefReconciler implements Reconciler { + private static final Logger log = + LoggerFactory.getLogger(DependentResourceCrossRefReconciler.class); public static final String SECRET_NAME = "secret"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -48,6 +53,7 @@ public ErrorStatusUpdateControl updateErrorSt DependentResourceCrossRefResource resource, Context context, Exception e) { + log.error("Status update on error", e); errorHappened = true; return ErrorStatusUpdateControl.noStatusUpdate(); } @@ -59,10 +65,6 @@ public boolean isErrorHappened() { public static class SecretDependentResource extends CRUDKubernetesDependentResource { - public SecretDependentResource() { - super(Secret.class); - } - @Override protected Secret desired( DependentResourceCrossRefResource primary, @@ -81,10 +83,6 @@ protected Secret desired( public static class ConfigMapDependentResource extends CRUDKubernetesDependentResource { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentResourceCrossRefResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/SSAConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/SSAConfigMapDependent.java index dc47f1f8df..49d2c1de44 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/SSAConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/SSAConfigMapDependent.java @@ -16,10 +16,6 @@ public class SSAConfigMapDependent public static final String DATA_KEY = "key1"; - public SSAConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentSSACustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/ConfigMapDependentResource.java index 57427a3537..348921cd93 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/ConfigMapDependentResource.java @@ -16,10 +16,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "key"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( InformerRelatedBehaviorTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java index 8686d6f33b..ec50e058b9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java @@ -368,35 +368,34 @@ Operator startOperator( } private void setNoConfigMapAccess() { - applyClusterRole("rback-test-no-configmap-access.yaml"); + applyClusterRole("rbac-test-no-configmap-access.yaml"); applyClusterRoleBinding(); } private void setNoCustomResourceAccess() { - applyClusterRole("rback-test-no-cr-access.yaml"); + applyClusterRole("rbac-test-no-cr-access.yaml"); applyClusterRoleBinding(); } private void setFullResourcesAccess() { - applyClusterRole("rback-test-full-access-role.yaml"); + applyClusterRole("rbac-test-full-access-role.yaml"); applyClusterRoleBinding(); } private void addRoleBindingsToTestNamespaces() { var role = - ReconcilerUtils.loadYaml( - Role.class, this.getClass(), "rback-test-only-main-ns-access.yaml"); + ReconcilerUtils.loadYaml(Role.class, this.getClass(), "rbac-test-only-main-ns-access.yaml"); adminClient.resource(role).inNamespace(actualNamespace).createOrReplace(); var roleBinding = ReconcilerUtils.loadYaml( - RoleBinding.class, this.getClass(), "rback-test-only-main-ns-access-binding.yaml"); + RoleBinding.class, this.getClass(), "rbac-test-only-main-ns-access-binding.yaml"); adminClient.resource(roleBinding).inNamespace(actualNamespace).createOrReplace(); } private void applyClusterRoleBinding() { var clusterRoleBinding = ReconcilerUtils.loadYaml( - ClusterRoleBinding.class, this.getClass(), "rback-test-role-binding.yaml"); + ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).createOrReplace(); } @@ -418,7 +417,7 @@ private Namespace namespace(String name) { private void removeClusterRoleBinding() { var clusterRoleBinding = ReconcilerUtils.loadYaml( - ClusterRoleBinding.class, this.getClass(), "rback-test-role-binding.yaml"); + ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).delete(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java index 36af4fadb4..880aaa6884 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java @@ -70,10 +70,6 @@ private static class ConfigMapDependentResource Updater, GarbageCollected { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( DependentGarbageCollectionTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap1.java index 2ea2b1daba..855944ef98 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap1.java @@ -17,10 +17,6 @@ public class MultipleManagedDependentResourceMultiInformerConfigMap1 public static final String NAME_SUFFIX = "-1"; - public MultipleManagedDependentResourceMultiInformerConfigMap1() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleManagedDependentResourceMultiInformerCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap2.java index dbc0934ada..7b28b322b7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleManagedDependentResourceMultiInformerConfigMap2.java @@ -18,10 +18,6 @@ public class MultipleManagedDependentResourceMultiInformerConfigMap2 public static final String NAME_SUFFIX = "-2"; - public MultipleManagedDependentResourceMultiInformerConfigMap2() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleManagedDependentResourceMultiInformerCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap1.java index 94584e8172..2fb65c6dde 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap1.java @@ -17,10 +17,6 @@ public class MultipleManagedDependentNoDiscriminatorConfigMap1 public static final String NAME_SUFFIX = "-1"; - public MultipleManagedDependentNoDiscriminatorConfigMap1() { - super(ConfigMap.class); - } - /* * Showcases optimization to avoid computing the whole desired state by providing the ResourceID * of the target resource. In this particular case this would not be necessary, since desired diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap2.java index 8836badb1f..4455ef5d9b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorConfigMap2.java @@ -18,10 +18,6 @@ public class MultipleManagedDependentNoDiscriminatorConfigMap2 public static final String NAME_SUFFIX = "-2"; - public MultipleManagedDependentNoDiscriminatorConfigMap2() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleManagedDependentNoDiscriminatorCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java index 38e2acc050..687bfcb5ac 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java @@ -16,10 +16,6 @@ public class MultipleManagedDependentResourceConfigMap1 public static final String NAME_SUFFIX = "-1"; - public MultipleManagedDependentResourceConfigMap1() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleManagedDependentResourceCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java index 95dcc66490..1a1d6b51c0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java @@ -16,10 +16,6 @@ public class MultipleManagedDependentResourceConfigMap2 public static final String NAME_SUFFIX = "-2"; - public MultipleManagedDependentResourceConfigMap2() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleManagedDependentResourceCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultipleOwnerDependentConfigMap.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultipleOwnerDependentConfigMap.java index 781451bba0..28ddfcc907 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultipleOwnerDependentConfigMap.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultipleOwnerDependentConfigMap.java @@ -19,10 +19,6 @@ public class MultipleOwnerDependentConfigMap public static final String RESOURCE_NAME = "test1"; - public MultipleOwnerDependentConfigMap() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleOwnerDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/DeploymentDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/DeploymentDependent.java new file mode 100644 index 0000000000..5cfb66f67e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/DeploymentDependent.java @@ -0,0 +1,95 @@ +package io.javaoperatorsdk.operator.dependent.prevblocklist; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.SSABasedGenericKubernetesResourceMatcher; + +@KubernetesDependent +public class DeploymentDependent + extends CRUDKubernetesDependentResource { + + public static final String RESOURCE_NAME = "test1"; + + public DeploymentDependent() { + super(Deployment.class); + } + + @Override + protected Deployment desired( + PrevAnnotationBlockCustomResource primary, + Context context) { + + return new DeploymentBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withSpec( + new DeploymentSpecBuilder() + .withReplicas(1) + .withSelector( + new LabelSelectorBuilder().withMatchLabels(Map.of("app", "nginx")).build()) + .withTemplate( + new PodTemplateSpecBuilder() + .withMetadata( + new ObjectMetaBuilder().withLabels(Map.of("app", "nginx")).build()) + .withSpec( + new PodSpecBuilder() + .withContainers( + new ContainerBuilder() + .withName("nginx") + .withImage("nginx:1.14.2") + .withResources( + new ResourceRequirementsBuilder() + .withLimits(Map.of("cpu", new Quantity("2000m"))) + .build()) + .build()) + .build()) + .build()) + .build()) + .build(); + } + + // for testing purposes replicating the matching logic but with the special matcher + @Override + public Result match( + Deployment actualResource, + Deployment desired, + PrevAnnotationBlockCustomResource primary, + Context context) { + final boolean matches; + addMetadata(true, actualResource, desired, primary, context); + if (useSSA(context)) { + matches = new SSAMatcherWithoutSanitization().matches(actualResource, desired, context); + } else { + matches = + GenericKubernetesResourceMatcher.match(desired, actualResource, false, false, context) + .matched(); + } + return Result.computed(matches, desired); + } + + // using this matcher, so we are able to reproduce issue with resource limits + static class SSAMatcherWithoutSanitization + extends SSABasedGenericKubernetesResourceMatcher { + + @Override + protected void sanitizeState(R actual, R desired, Map actualMap) {} + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockCustomResource.java new file mode 100644 index 0000000000..7aa3194672 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.dependent.prevblocklist; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("pabc") +public class PrevAnnotationBlockCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconciler.java new file mode 100644 index 0000000000..7f3dab61fe --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconciler.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.dependent.prevblocklist; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@Workflow(dependents = {@Dependent(type = DeploymentDependent.class)}) +@ControllerConfiguration() +public class PrevAnnotationBlockReconciler + implements Reconciler, TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + public PrevAnnotationBlockReconciler() {} + + @Override + public UpdateControl reconcile( + PrevAnnotationBlockCustomResource resource, + Context context) { + numberOfExecutions.getAndIncrement(); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java new file mode 100644 index 0000000000..137e2ba663 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.dependent.prevblocklist; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class PrevAnnotationBlockReconcilerIT { + + public static final String TEST_1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + // Removing resource from blocklist List would result in test failure + // .withConfigurationService( + // o -> o.previousAnnotationUsageBlocklist(Collections.emptyList())) + .withReconciler(PrevAnnotationBlockReconciler.class) + .build(); + + @Test + void doNotUsePrevAnnotationForDeploymentDependent() { + extension.create(testResource(TEST_1)); + + var reconciler = extension.getReconcilerOfType(PrevAnnotationBlockReconciler.class); + await() + .pollDelay(Duration.ofMillis(200)) + .untilAsserted( + () -> { + var deployment = extension.get(Deployment.class, TEST_1); + assertThat(deployment).isNotNull(); + assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(0).isLessThan(10); + }); + } + + PrevAnnotationBlockCustomResource testResource(String name) { + var res = new PrevAnnotationBlockCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(name).build()); + res.setSpec(new PrevAnnotationBlockSpec()); + res.getSpec().setValue("value"); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockSpec.java new file mode 100644 index 0000000000..9d80e14bc1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.dependent.prevblocklist; + +public class PrevAnnotationBlockSpec { + + private String value; + + public String getValue() { + return value; + } + + public PrevAnnotationBlockSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerTestReconciler.java index a7e72e592b..52094972da 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerTestReconciler.java @@ -61,10 +61,6 @@ public List> prepareEventSource public static class ReadOnlyConfigMapDependent extends KubernetesDependentResource { - public ReadOnlyConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( PrimaryIndexerTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/ConfigMapDependent.java index 806b87dfa1..2b63bbf6b1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/ConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/ConfigMapDependent.java @@ -11,10 +11,6 @@ public class ConfigMapDependent public static final String TEST_CONFIG_MAP_NAME = "testconfigmap"; - public ConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( PrimaryToSecondaryDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/SecretDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/SecretDependent.java index c3a4f5b77b..6371f453d7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/SecretDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/SecretDependent.java @@ -13,10 +13,6 @@ public class SecretDependent extends CRUDKubernetesDependentResource { - public SecretDependent() { - super(Secret.class); - } - @Override protected Secret desired( PrimaryToSecondaryDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/readonly/ReadOnlyDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/readonly/ReadOnlyDependent.java index 63e43fef95..a6f0662948 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/readonly/ReadOnlyDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/readonly/ReadOnlyDependent.java @@ -5,9 +5,4 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @KubernetesDependent -public class ReadOnlyDependent extends KubernetesDependentResource { - - public ReadOnlyDependent() { - super(ConfigMap.class); - } -} +public class ReadOnlyDependent extends KubernetesDependentResource {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/ConfigMapDependentResource.java index a7df64f117..358718b107 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/ConfigMapDependentResource.java @@ -16,10 +16,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "key"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( RestartTestCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java index df7a275122..56e34330e1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java @@ -19,10 +19,6 @@ public class ServiceDependentResource public static AtomicInteger updated = new AtomicInteger(0); - public ServiceDependentResource() { - super(Service.class); - } - @Override protected Service desired( ServiceStrictMatcherTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/ServiceAccountDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/ServiceAccountDependentResource.java index 9808b364a6..1a598992b5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/ServiceAccountDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/ServiceAccountDependentResource.java @@ -12,10 +12,6 @@ public class ServiceAccountDependentResource extends CRUDKubernetesDependentResource { - public ServiceAccountDependentResource() { - super(ServiceAccount.class); - } - @Override protected ServiceAccount desired( SpecialResourceCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java index 510aefddcf..f4007b6151 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java @@ -18,10 +18,6 @@ public class ServiceDependentResource public static AtomicInteger createUpdateCount = new AtomicInteger(0); - public ServiceDependentResource() { - super(Service.class); - } - @Override protected Service desired( SSALegacyMatcherCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java index 43125eef90..f5d9571711 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java @@ -70,10 +70,6 @@ public boolean isErrorOccurred() { private static class DeploymentDependentResource extends CRUDKubernetesDependentResource { - public DeploymentDependentResource() { - super(Deployment.class); - } - @Override protected Deployment desired( StandaloneDependentTestCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java index 72bcc8d8a3..fb8e4a6880 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java @@ -12,10 +12,6 @@ public class StatefulSetDesiredSanitizerDependentResource public static volatile Boolean nonMatchedAtLeastOnce; - public StatefulSetDesiredSanitizerDependentResource() { - super(StatefulSet.class); - } - @Override protected StatefulSet desired( StatefulSetDesiredSanitizerCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationDependent.java index 715caf5314..11923e274b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationDependent.java @@ -8,10 +8,6 @@ public class CRDPresentActivationDependent extends CRUDNoGCKubernetesDependentResource< CRDPresentActivationDependentCustomResource, CRDPresentActivationCustomResource> { - public CRDPresentActivationDependent() { - super(CRDPresentActivationDependentCustomResource.class); - } - @Override protected CRDPresentActivationDependentCustomResource desired( CRDPresentActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/ConfigMapDependentResource.java index feacf06cb5..c9078848b4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/ConfigMapDependentResource.java @@ -10,10 +10,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "data"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( GetNonActiveSecondaryCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/RouteDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/RouteDependentResource.java index 99e34df514..77ebef373a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/RouteDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/RouteDependentResource.java @@ -8,10 +8,6 @@ public class RouteDependentResource extends CRUDKubernetesDependentResource { - public RouteDependentResource() { - super(Route.class); - } - @Override protected Route desired( GetNonActiveSecondaryCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ConfigMapDependent.java index b4eebf36f0..adc633b877 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ConfigMapDependent.java @@ -11,10 +11,6 @@ public class ConfigMapDependent extends CRUDNoGCKubernetesDependentResource< ConfigMap, ManagedDependentDefaultDeleteConditionCustomResource> { - public ConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( ManagedDependentDefaultDeleteConditionCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/SecretDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/SecretDependent.java index 2ae036e7ee..a7d52511ea 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/SecretDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/SecretDependent.java @@ -13,10 +13,6 @@ public class SecretDependent extends CRUDNoGCKubernetesDependentResource< Secret, ManagedDependentDefaultDeleteConditionCustomResource> { - public SecretDependent() { - super(Secret.class); - } - @Override protected Secret desired( ManagedDependentDefaultDeleteConditionCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource1.java index cce3eb8ad4..ed83b870ab 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource1.java @@ -17,10 +17,6 @@ public class ConfigMapDependentResource1 public static final String DATA_KEY = "data"; public static final String SUFFIX = "1"; - public ConfigMapDependentResource1() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleDependentActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource2.java index 8b0a4d89bb..73ccb55cdb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/ConfigMapDependentResource2.java @@ -17,10 +17,6 @@ public class ConfigMapDependentResource2 public static final String DATA_KEY = "data"; public static final String SUFFIX = "2"; - public ConfigMapDependentResource2() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( MultipleDependentActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/SecretDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/SecretDependentResource.java index 9b629c5af4..330f0e3c0f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/SecretDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/SecretDependentResource.java @@ -11,10 +11,6 @@ public class SecretDependentResource extends CRUDKubernetesDependentResource { - public SecretDependentResource() { - super(Secret.class); - } - @Override protected Secret desired( MultipleDependentActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource1.java index f3cc3144c6..eec904d2c7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource1.java @@ -15,10 +15,6 @@ public class ConfigMapDependentResource1 extends CRUDKubernetesDependentResource { - public ConfigMapDependentResource1() { - super(ConfigMap.class); - } - @Override public ReconcileResult reconcile( OrderedManagedDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource2.java index aae0fe79b3..8e4a1467ec 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/ConfigMapDependentResource2.java @@ -15,10 +15,6 @@ public class ConfigMapDependentResource2 extends CRUDKubernetesDependentResource { - public ConfigMapDependentResource2() { - super(ConfigMap.class); - } - @Override public ReconcileResult reconcile( OrderedManagedDependentCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/ConfigMapDependentResource.java index 4edba945a8..cb2357bf8b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/ConfigMapDependentResource.java @@ -13,10 +13,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "data"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowActivationCleanupCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/ConfigMapDependentResource.java index 181e35eb2d..5f2e92ed55 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/ConfigMapDependentResource.java @@ -12,10 +12,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "data"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowActivationConditionCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/RouteDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/RouteDependentResource.java index 64a0c9e299..7d2d091c94 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/RouteDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/RouteDependentResource.java @@ -8,10 +8,6 @@ public class RouteDependentResource extends CRUDKubernetesDependentResource { - public RouteDependentResource() { - super(Route.class); - } - @Override protected Route desired( WorkflowActivationConditionCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/ConfigMapDependentResource.java index 29866202fc..fac6ecae88 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/ConfigMapDependentResource.java @@ -25,10 +25,6 @@ public class ConfigMapDependentResource private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowAllFeatureCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java index 36e3a95b92..92956e05f6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java @@ -8,10 +8,6 @@ public class DeploymentDependentResource extends CRUDNoGCKubernetesDependentResource { - public DeploymentDependentResource() { - super(Deployment.class); - } - @Override protected Deployment desired( WorkflowAllFeatureCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/ConfigMapDependent.java index b42f14cace..17190c5a92 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/ConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/ConfigMapDependent.java @@ -11,10 +11,6 @@ public class ConfigMapDependent extends CRUDNoGCKubernetesDependentResource { - public ConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowExplicitCleanupCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/ConfigMapDependent.java index cc404328a9..a2638b48b5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/ConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/ConfigMapDependent.java @@ -12,10 +12,6 @@ public class ConfigMapDependent extends CRUDNoGCKubernetesDependentResource< ConfigMap, WorkflowExplicitInvocationCustomResource> { - public ConfigMapDependent() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowExplicitInvocationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/ConfigMapDependentResource.java index 2bd666b64c..3366a61a1f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/ConfigMapDependentResource.java @@ -13,10 +13,6 @@ public class ConfigMapDependentResource public static final String DATA_KEY = "data"; - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired( WorkflowMultipleActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/SecretDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/SecretDependentResource.java index b392259ca9..cd1fbfcedc 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/SecretDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/SecretDependentResource.java @@ -11,10 +11,6 @@ public class SecretDependentResource extends CRUDKubernetesDependentResource { - public SecretDependentResource() { - super(Secret.class); - } - @Override protected Secret desired( WorkflowMultipleActivationCustomResource primary, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/ConfigMapDependent.java index bd8d4099ff..6dbac41f8c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/ConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/ConfigMapDependent.java @@ -9,10 +9,6 @@ public class ConfigMapDependent extends CRUDNoGCKubernetesDependentResource< ConfigMap, HandleWorkflowExceptionsInReconcilerCustomResource> { - public ConfigMapDependent() { - super(ConfigMap.class); - } - @Override public ReconcileResult reconcile( HandleWorkflowExceptionsInReconcilerCustomResource primary, diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-full-access-role.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-full-access-role.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-full-access-role.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-full-access-role.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-no-configmap-access.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-no-configmap-access.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-no-configmap-access.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-no-configmap-access.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-no-cr-access.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-no-cr-access.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-no-cr-access.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-no-cr-access.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-only-main-ns-access-binding.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-only-main-ns-access-binding.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-only-main-ns-access-binding.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-only-main-ns-access-binding.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-only-main-ns-access.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-only-main-ns-access.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-only-main-ns-access.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-only-main-ns-access.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-role-binding.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-role-binding.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rback-test-role-binding.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/rbac-test-role-binding.yaml diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/statefulset.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/statefulset.yaml index f40fbeb607..bb8a2df04b 100644 --- a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/statefulset.yaml +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/statefulset.yaml @@ -21,6 +21,13 @@ spec: ports: - containerPort: 80 name: web + env: + - name: APP1_HOST_NAME + value: "" + - name: APP2_HOST_NAME + value: "localhost" + - name: APP3_HOST_NAME + value: " " volumeMounts: - name: www mountPath: /usr/share/nginx/html diff --git a/pom.xml b/pom.xml index 66ebebdc07..12613ea0c5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators @@ -44,13 +44,6 @@ https://github.com/operator-framework/java-operator-sdk/tree/main - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - UTF-8 17 @@ -59,39 +52,39 @@ java-operator-sdk https://sonarcloud.io jdk - - 5.12.0 - 7.1.0 - 2.0.12 - 2.24.3 - 5.16.0 - 3.17.0 - 0.21.0 + 5.13.4 + 7.3.1 + 2.0.17 + 2.25.2 + 5.20.0 + 3.19.0 + 0.23.0 1.13.0 - 3.27.3 + 3.27.6 4.3.0 2.7.3 - 1.14.4 - 3.2.0 + 1.16.0 + 3.2.3 0.9.14 - 2.18.0 - 4.15 + 2.20.0 + 4.16 2.11 - 3.14.0 - 3.5.2 - 3.11.2 + 3.14.1 + 3.5.4 + 0.9.0 + 3.12.0 3.3.1 3.3.1 3.4.2 - 3.4.1 - 3.2.7 + 3.5.0 + 3.2.8 1.7.0 3.0.0 3.1.4 - 9.0.1 - 3.4.4 - 2.44.3 + 9.0.2 + 3.4.6 + 3.0.0 @@ -286,7 +279,7 @@ me.fabriciorby maven-surefire-junit5-tree-reporter - 1.4.0 + 1.5.1 @@ -294,6 +287,15 @@ org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} + + + attach-sources + + jar + + verify + + org.apache.maven.plugins @@ -342,6 +344,7 @@ + 1.28.0 true @@ -516,14 +519,6 @@ org.apache.maven.plugins maven-source-plugin - - - attach-sources - - jar - - - org.apache.maven.plugins @@ -545,14 +540,15 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} true - ossrh - https://oss.sonatype.org/ - true + central + true + true + published diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index 9608b44db1..bb2a8fe099 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 74aab104f1..23873c5d45 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index e726a2242a..8201c1148e 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index 5bc210f7d4..ec2b03325c 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -54,10 +54,6 @@ public class SchemaDependentResource private MySQLDbConfig dbConfig; - public SchemaDependentResource() { - super(Schema.class); - } - @Override public Optional configuration() { return Optional.of(new ResourcePollerConfig(getPollingPeriod(), dbConfig)); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 092ac22e24..cff28feadd 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -27,10 +27,6 @@ public class SecretDependentResource extends KubernetesDependentResource io.javaoperatorsdk java-operator-sdk - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 314e0ef96c..0c43071f16 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 6a18111b27..c7f25e996c 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -14,10 +14,6 @@ public class DeploymentDependentResource extends CRUDKubernetesDependentResource { - public DeploymentDependentResource() { - super(Deployment.class); - } - private static String tomcatImage(Tomcat tomcat) { return "tomcat:" + tomcat.getSpec().getVersion(); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index b42a42257d..bb0359458e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -13,10 +13,6 @@ informer = @Informer(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator")) public class ServiceDependentResource extends CRUDKubernetesDependentResource { - public ServiceDependentResource() { - super(Service.class); - } - @Override protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 2266303adc..55eafa8490 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.0.4-SNAPSHOT + 5.1.5-SNAPSHOT sample-webpage-operator diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java index 816db3688e..0cf8faad7c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java @@ -20,10 +20,6 @@ public class ConfigMapDependentResource extends CRUDKubernetesDependentResource { - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - @Override protected ConfigMap desired(WebPage webPage, Context context) { Map data = new HashMap<>(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java index 3476464f1f..4deef0f1c0 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java @@ -22,10 +22,6 @@ public class DeploymentDependentResource extends CRUDKubernetesDependentResource { - public DeploymentDependentResource() { - super(Deployment.class); - } - @Override protected Deployment desired(WebPage webPage, Context context) { Map labels = new HashMap<>(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/IngressDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/IngressDependentResource.java index 994a35c98c..3f3e64e8ed 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/IngressDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/IngressDependentResource.java @@ -15,10 +15,6 @@ informer = @Informer(labelSelector = WebPageManagedDependentsReconciler.SELECTOR)) public class IngressDependentResource extends CRUDKubernetesDependentResource { - public IngressDependentResource() { - super(Ingress.class); - } - @Override protected Ingress desired(WebPage webPage, Context context) { return makeDesiredIngress(webPage); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java index b1ab856f62..01e8953fa9 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java @@ -22,10 +22,6 @@ public class ServiceDependentResource .CRUDKubernetesDependentResource< Service, WebPage> { - public ServiceDependentResource() { - super(Service.class); - } - @Override protected Service desired(WebPage webPage, Context context) { Map serviceLabels = new HashMap<>();