diff --git a/.github/workflows/composites/build-controllers-project/action.yaml b/.github/workflows/composites/build-controllers-project/action.yaml index 307451c8f5..dff8da1648 100644 --- a/.github/workflows/composites/build-controllers-project/action.yaml +++ b/.github/workflows/composites/build-controllers-project/action.yaml @@ -5,7 +5,9 @@ runs: steps: - name: build controllers project shell: bash + env: + CURRENT_INDEX: ${{ matrix.current_index }} run: | cd spring-cloud-kubernetes-controllers - .././mvnw -T 1C -U clean install + .././mvnw -DCURRENT_INSTANCE=${CURRENT_INDEX} -T 1C -U clean install cd .. diff --git a/.github/workflows/composites/clean-space/action.yaml b/.github/workflows/composites/clean-space/action.yaml index 69d61642fd..f115de3a20 100644 --- a/.github/workflows/composites/clean-space/action.yaml +++ b/.github/workflows/composites/clean-space/action.yaml @@ -3,10 +3,6 @@ description: clean space runs: using: "composite" steps: - - name: apt-update - shell: bash - run: | - sudo apt-get update - name: Free Disk Space uses: jlumbroso/free-disk-space@main diff --git a/.github/workflows/composites/matrix-bounds-on-test-times-cache-hit/action.yaml b/.github/workflows/composites/matrix-bounds-on-test-times-cache-hit/action.yaml index 8be1192ca5..4badaf1b57 100644 --- a/.github/workflows/composites/matrix-bounds-on-test-times-cache-hit/action.yaml +++ b/.github/workflows/composites/matrix-bounds-on-test-times-cache-hit/action.yaml @@ -18,15 +18,56 @@ runs: steps: + - name: restore test times cache + uses: actions/cache/restore@v3 + with: + path: /tmp/sorted.txt + key: ${{ runner.os }}-spring-cloud-k8s-existing-test-times-cache-${{ github.run_id }} + restore-keys: ${{ runner.os }}-spring-cloud-k8s-existing-test-times-cache- + + - name: show cached test times + shell: bash + run: cat /tmp/sorted.txt + - name: compute matrix steps shell: bash run: | - sum_of_all_tests=$(awk -F' ' '{sum+=$2;} END{print sum;}' /tmp/sorted.txt) + PLAIN_TEST_CLASSNAMES=($(cat /tmp/tests.txt | grep -o 'spring.cloud.k8s.test.to.run -> org.*' | awk '{print $3}')) + + ####################################################################################################### + ### split into tests that we know the running times for and in tests that we do not know the times for. + ####################################################################################################### + for test in "${PLAIN_TEST_CLASSNAMES[@]}"; do + + find_test_in_sorted=$(grep "$test " /tmp/sorted.txt || true) + if [[ -z "$find_test_in_sorted" ]]; then + echo $test >> /tmp/tests-without-times.txt + else + echo $find_test_in_sorted >> /tmp/tests-with-times.txt + fi + + done + + sort -t' ' -nk2 /tmp/tests-with-times.txt >> /tmp/tests-with-times-sorted.txt + + # this is a work-around for the fact that 'actions/download-artifact@v3' does not support + # something like: 'ignore if downloaded file is missing'. + # so unless we create a dummy file for upload, download will fail + if [[ ! -f /tmp/tests-without-times.txt ]]; then touch /tmp/tests-without-times.txt; fi + + echo '--------------------------------------------------------------' + cat /tmp/tests-with-times-sorted.txt + echo '--------------------------------------------------------------' + cat /tmp/tests-without-times.txt + echo '--------------------------------------------------------------' + + + sum_of_all_tests=$(awk -F' ' '{sum+=$2;} END{print sum;}' /tmp/tests-with-times-sorted.txt) sum_of_all_tests_as_int=$(printf "%.0f\n" "$sum_of_all_tests") echo "sum of all tests : $sum_of_all_tests_as_int" - max_test_time=$(tail -1 /tmp/sorted.txt | awk '{print $2}') + max_test_time=$(tail -1 /tmp/tests-with-times-sorted.txt | awk '{print $2}') max_test_time_as_int=$(printf "%.0f\n" "$max_test_time") echo "max test time : $max_test_time_as_int" @@ -38,7 +79,10 @@ runs: number_of_instances_as_array+=']' average_time_per_instance=$(( sum_of_all_tests_as_int / $number_of_instances )) - echo "average time per instance $average_time_per_instance" + average_time_per_instance_array=() + average_time_per_instance_array+='[' + average_time_per_instance_array+=$average_time_per_instance + average_time_per_instance_array+=']' matrix_array=() matrix_array+='[' @@ -52,8 +96,9 @@ runs: matrix_array+=']' + echo "********************************************************************************************************" echo "number of instances : $number_of_instances_as_array" - echo "average time per instance : $average_time_per_instance" + echo "average_time_per_instance_as_array: $average_time_per_instance_as_array" echo "matrix_array : $matrix_array" echo "********************************************************************************************************" @@ -62,10 +107,23 @@ runs: matrix_array_json=$(jq -r -c . <<< $matrix_array) echo "matrix_array_json : $matrix_array_json" + + average_time_per_instance_json=$(jq -r -c . <<< $average_time_per_instance_array) + echo "average_time_per_instance_json : $average_time_per_instance_json" echo "TEST_TIMES_CACHE_PRESENT=true" >> $GITHUB_ENV echo "NUMBER_OF_MATRIX_INSTANCES=$(echo $number_of_instances_json)" >> $GITHUB_ENV echo "MATRIX_ARRAY=$(echo $matrix_array_json)" >> $GITHUB_ENV - echo "AVERAGE_TIME_PER_INSTANCE=$(echo $average_time_per_instance)" >> $GITHUB_ENV + echo "AVERAGE_TIME_PER_INSTANCE=$(echo $average_time_per_instance_json)" >> $GITHUB_ENV + - name: upload test with times + uses: actions/upload-artifact@v3 + with: + name: tests-with-times-sorted.txt + path: /tmp/tests-with-times-sorted.txt + - name: upload test without times + uses: actions/upload-artifact@v3 + with: + name: tests-without-times.txt + path: /tmp/tests-without-times.txt diff --git a/.github/workflows/composites/maven-build-with-dry-run-for-tests/action.yaml b/.github/workflows/composites/maven-build-with-dry-run-for-tests/action.yaml index f2b6696192..202705c0d4 100644 --- a/.github/workflows/composites/maven-build-with-dry-run-for-tests/action.yaml +++ b/.github/workflows/composites/maven-build-with-dry-run-for-tests/action.yaml @@ -4,13 +4,13 @@ runs: using: "composite" steps: - - name: run 'package' on the project + - name: run 'install' on the project shell: bash run: | ./mvnw install -B \ -Dskip.build.image=true \ -DskipTests -DskipITs \ - -T 1C -q + -T 1C -U -q - name: find all classpath entries shell: bash @@ -21,7 +21,7 @@ runs: shell: bash run: | cd spring-cloud-kubernetes-test-support - .././mvnw -q exec:java -Dexec.mainClass="org.springframework.cloud.kubernetes.tests.discovery.TestsDiscovery" > /tmp/tests.txt + .././mvnw -q exec:java -Prun-on-github-actions -Dexec.mainClass="org.springframework.cloud.kubernetes.tests.discovery.TestsDiscovery" > /tmp/tests.txt cd .. - name: show result diff --git a/.github/workflows/composites/pre-test-actions/action.yaml b/.github/workflows/composites/pre-test-actions/action.yaml index d2bb6b96b9..657c55e886 100644 --- a/.github/workflows/composites/pre-test-actions/action.yaml +++ b/.github/workflows/composites/pre-test-actions/action.yaml @@ -22,6 +22,11 @@ runs: - name: cache local maven repository uses: ./.github/workflows/composites/cache + - name: build project + shell: bash + run: | + ./mvnw clean install -Dskip.build.image=true -DskipITs -DskipTests -T1C -U -B -q + - name: build controllers project uses: ./.github/workflows/composites/build-controllers-project diff --git a/.github/workflows/composites/run-and-save-test-times-when-cache-missing/action.yaml b/.github/workflows/composites/run-and-save-test-times-when-cache-missing/action.yaml index 39e79ebdd7..d262d89bbd 100644 --- a/.github/workflows/composites/run-and-save-test-times-when-cache-missing/action.yaml +++ b/.github/workflows/composites/run-and-save-test-times-when-cache-missing/action.yaml @@ -60,8 +60,9 @@ runs: ./mvnw -s .settings.xml \ -DtestsToRun=${TEST_ARG[@]} \ + -DCURRENT_INSTANCE=${CURRENT_INDEX} \ -e clean install \ - -U -P sonar -nsu --batch-mode \ + -P sonar -nsu --batch-mode \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ -Dhttp.keepAlive=false \ -Dmaven.wagon.http.pool=false \ diff --git a/.github/workflows/composites/run-and-save-test-times-when-cache-present/action.yaml b/.github/workflows/composites/run-and-save-test-times-when-cache-present/action.yaml index 147475f458..c81655555f 100644 --- a/.github/workflows/composites/run-and-save-test-times-when-cache-present/action.yaml +++ b/.github/workflows/composites/run-and-save-test-times-when-cache-present/action.yaml @@ -4,53 +4,30 @@ runs: using: "composite" steps: - - name: restore test times cache - uses: actions/cache/restore@v3 + + - name: download tests with times + if: env.BASE_BRANCH != '2.1.x' + uses: actions/download-artifact@v3 with: - path: /tmp/sorted.txt - key: ${{ runner.os }}-spring-cloud-k8s-existing-test-times-cache-${{ github.run_id }} - restore-keys: ${{ runner.os }}-spring-cloud-k8s-existing-test-times-cache- + name: tests-with-times-sorted.txt + path: /tmp/ - - name: show cached test times - shell: bash - run: cat /tmp/sorted.txt + - name: download tests without times + if: env.BASE_BRANCH != '2.1.x' + uses: actions/download-artifact@v3 + with: + name: tests-without-times.txt + path: /tmp/ - name: split tests into known times and un-known times shell: bash run: | - ############################################################################################################ - ############################################################################################################ - ############################################################################################################ - - # 1. Get all existing tests and place them in PLAIN_TEST_CLASSNAMES - # 2. Get all test times from the existing cache : /tmp/sorted.txt - # 3. Split tests from PLAIN_TEST_CLASSNAMES into two files depending if we already know their running times - # or not : tests-without-times.txt and tests-with-times.txt - - ############################################################################################################ - ############################################################################################################ - ############################################################################################################ - PLAIN_TEST_CLASSNAMES=($(cat /tmp/tests.txt | grep -o 'spring.cloud.k8s.test.to.run -> org.*' | awk '{print $3}')) - - echo "${PLAIN_TEST_CLASSNAMES[@]}" - temp_dir=$(mktemp -d) - - for test in "${PLAIN_TEST_CLASSNAMES[@]}"; do - - find_test_in_sorted=$(grep "$test " /tmp/sorted.txt || true) - if [[ -z "$find_test_in_sorted" ]]; then - echo $test >> $temp_dir/tests-without-times.txt - else - echo $find_test_in_sorted >> $temp_dir/tests-with-times.txt - fi - - done - - echo "tests with times:" - cat $temp_dir/tests-with-times.txt - - sort -t' ' -nk2 $temp_dir/tests-with-times.txt >> $temp_dir/tests-with-times-sorted.txt + echo "------------------------------------------------------------------------------" + cat /tmp/tests-with-times-sorted.txt + echo "------------------------------------------------------------------------------" + if [[ -f /tmp/tests-without-times.txt ]]; then cat /tmp/tests-without-times.txt; fi + echo "------------------------------------------------------------------------------" ############################################################################################################ ############################################################################################################ @@ -70,7 +47,7 @@ runs: # this test needs to be taken in 'tests_to_take_in_current_iteration', thus: # tests_to_take_in_current_iteration=testC; also next_sum becomes 3sec # we then drop this line from /tmp/sorted.txt because we have already processed it - # ('sed -i "" "${j}d" $temp_dir/tests-with-times-sorted.txt') + # ('sed -i "" "${j}d" /tmp/tests-with-times-sorted.txt') # we also decrement j, since we removed one line from the file # we then take testB, add its time to next_sum, thus next_sum = 5 sec, but now the time is NOT < 3.1 sec @@ -81,22 +58,14 @@ runs: # we repeat the process again and now tests_to_take_in_current_iteration=testB,testA because their cumulative # sum will be 3 sec and it's < 3.1 sec. - - sum_of_all_tests=$(awk -F' ' '{sum+=$2;} END{print sum;}' $temp_dir/tests-with-times-sorted.txt) - sum_of_all_tests_as_int=$(printf "%.0f\n" "$sum_of_all_tests") - echo "sum of all tests : $sum_of_all_tests_as_int" - - max_test_time=$(tail -1 $temp_dir/tests-with-times-sorted.txt | awk '{print $2}') - max_test_time_as_int=$(printf "%.0f\n" "$max_test_time") - echo "max test time : $max_test_time_as_int" number_of_instances=${NUMBER_OF_JOBS} echo "number of instances $number_of_instances" - average_time_per_instance=$(( sum_of_all_tests_as_int / number_of_instances )) + average_time_per_instance=${AVERAGE_TIME_PER_INSTANCE} echo "average time per instance $average_time_per_instance" - number_of_lines_in_file=$(grep -c ^ $temp_dir/tests-with-times-sorted.txt) + number_of_lines_in_file=$(grep -c ^ /tmp/tests-with-times-sorted.txt) echo "number of lines in fine : $number_of_lines_in_file" tests_to_run_in_current_index='' @@ -106,7 +75,7 @@ runs: tests_to_take_in_current_iteration='' for ((j=$number_of_lines_in_file; j>0; j--)) ; do - current_line_in_file=$(awk "NR == ${j}" $temp_dir/tests-with-times-sorted.txt) + current_line_in_file=$(awk "NR == ${j}" /tmp/tests-with-times-sorted.txt) current_test_time=$(echo $current_line_in_file | awk '{print $2}') current_test_time=$(printf "%.0f\n" "$current_test_time") current_test_name=$(echo $current_line_in_file | awk '{print $1}') @@ -122,7 +91,7 @@ runs: tests_to_take_in_current_iteration="$tests_to_take_in_current_iteration,$current_test_name" fi - sed -i "${j}d" $temp_dir/tests-with-times-sorted.txt + sed -i "${j}d" /tmp/tests-with-times-sorted.txt number_of_lines_in_file=$(( $number_of_lines_in_file-1 )) continue fi @@ -154,7 +123,7 @@ runs: if [[ ${CURRENT_INDEX} = ${NUMBER_OF_JOBS} ]]; then echo "last index spotted" - if [ ! -f $temp_dir/tests-without-times.txt ]; then + if [ ! -f /tmp/tests-without-times.txt ]; then echo "no tests outside cache found" if [ -z "$tests_to_run_in_current_index" ]; then @@ -163,7 +132,7 @@ runs: fi else - TESTS_WITHOUT_TIMES=($(cat $temp_dir/tests-without-times.txt)) + TESTS_WITHOUT_TIMES=($(cat /tmp/tests-without-times.txt)) for test in "${TESTS_WITHOUT_TIMES[@]}"; do if [[ -z "$tests_to_run_in_current_index" ]]; then @@ -180,8 +149,10 @@ runs: ./mvnw -s .settings.xml \ -DtestsToRun=${tests_to_run_in_current_index} \ + -Dsurefire-reports-directory=surefire-reports/${CURRENT_INDEX} \ + -Dfailsafe-reports-directory=failsafe-reports/${CURRENT_INDEX} \ -e clean install \ - -U -P sonar -nsu --batch-mode \ + -P sonar -nsu --batch-mode \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ -Dhttp.keepAlive=false \ -Dmaven.wagon.http.pool=false \ @@ -199,7 +170,7 @@ runs: for i in "${sliced_array[@]}"; do # can be present in the last index as 'none,testA,testB' - if [[ "$i" -eq "none" ]]; then + if [[ "$i" == "none" ]]; then echo "skipping 'none'" else filename="${i}.txt" diff --git a/.github/workflows/composites/save-controller-images/action.yaml b/.github/workflows/composites/save-controller-images/action.yaml deleted file mode 100644 index 056b1b2adb..0000000000 --- a/.github/workflows/composites/save-controller-images/action.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: save controller docker images -description: save controller docker images -runs: - using: "composite" - steps: - - name: save controller docker images - shell: bash - run: | - mkdir -p /tmp/docker/images - TAG=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) - cd spring-cloud-kubernetes-controllers - while read controller_image; do - docker save -o /tmp/docker/images/${controller_image}.tar docker.io/springcloud/${controller_image}:$TAG - done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q | grep -v 'spring-cloud-kubernetes-controllers') - cd .. diff --git a/.github/workflows/composites/save-integration-tests-images/action.yaml b/.github/workflows/composites/save-integration-tests-images/action.yaml deleted file mode 100644 index cbc92e55f7..0000000000 --- a/.github/workflows/composites/save-integration-tests-images/action.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: save integration tests docker images -description: save integration tests docker images -runs: - using: "composite" - steps: - - name: save integration tests docker images - shell: bash - run: | - mkdir -p /tmp/docker/images - TAG=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) - cd spring-cloud-kubernetes-integration-tests - while read integ_test; do - docker save -o /tmp/docker/images/${integ_test}.tar docker.io/springcloud/${integ_test}:$TAG - done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q \ - | grep -v 'spring-cloud-kubernetes-integration-tests' \ - | grep -v 'spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps' \ - | grep -v 'spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app' \ - | grep -v 'spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps' \ - | grep -v 'spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app' ) - cd .. diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..be4b92dfc0 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,32 @@ +name: Deploy Docs +on: + push: + branches-ignore: [ gh-pages ] + tags: '**' + repository_dispatch: + types: request-build-reference # legacy + #schedule: + #- cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: +permissions: + actions: write +jobs: + build: + runs-on: ubuntu-latest + # if: github.repository_owner == 'spring-cloud' + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: docs-build + fetch-depth: 1 + - name: Dispatch (partial build) + if: github.ref_type == 'branch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} + - name: Dispatch (full build) + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index 84f4dc3590..fd2779105d 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -23,14 +23,12 @@ jobs: test_times_cache_present: ${{ steps.test_times_cache_present_init.outputs.test_times_cache_present }} number_of_matrix_instances: ${{ steps.test_times_cache_present_init.outputs.number_of_matrix_instances }} matrix_array: ${{ steps.test_times_cache_present_init.outputs.matrix_array }} + average_time_per_instance: ${{ steps.test_times_cache_present_init.outputs.average_time_per_instance }} steps: - name: checkout project uses: actions/checkout@v2 - - name: clean space - uses: ./.github/workflows/composites/clean-space - - name: set env variables uses: ./.github/workflows/composites/env-variables @@ -91,6 +89,7 @@ jobs: echo "test_times_cache_present=${{ env.TEST_TIMES_CACHE_PRESENT }}" >> $GITHUB_OUTPUT echo "number_of_matrix_instances=${{ env.NUMBER_OF_MATRIX_INSTANCES }}" >> $GITHUB_OUTPUT echo "matrix_array=${{ env.MATRIX_ARRAY }}" >> $GITHUB_OUTPUT + echo "average_time_per_instance=${{ env.AVERAGE_TIME_PER_INSTANCE }}" >> $GITHUB_OUTPUT test_when_cache_present: needs: [ build ] @@ -99,13 +98,14 @@ jobs: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 30 # only run this one if there is a previous cache of test times if: needs.build.outputs.test_times_cache_present == 'true' - timeout-minutes: 60 + timeout-minutes: 120 strategy: fail-fast: true matrix: current_index: [ "${{ fromJSON(needs.build.outputs.matrix_array) }}" ] number_of_jobs: [ "${{ fromJSON(needs.build.outputs.number_of_matrix_instances) }}" ] + average_time_per_instance: [ "${{ fromJSON(needs.build.outputs.average_time_per_instance) }}" ] steps: @@ -138,13 +138,14 @@ jobs: env: CURRENT_INDEX: ${{ matrix.current_index }} NUMBER_OF_JOBS: ${{ matrix.number_of_jobs }} + AVERAGE_TIME_PER_INSTANCE: ${{ matrix.average_time_per_instance }} test_when_cache_missing: needs: [ build ] runs-on: ubuntu-latest env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 30 - timeout-minutes: 60 + timeout-minutes: 120 # only run this one if there is no previous cache of test times if: needs.build.outputs.test_times_cache_present == 'false' diff --git a/.gitignore b/.gitignore index 74b4110323..170259d0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,9 @@ crashlytics-build.properties .vscode/ .java-version *Dockerfile + +node +node_modules +build +package.json +package-lock.json diff --git a/.mvn/maven.config b/.mvn/maven.config index 3b8cf46e1e..a682990566 100644 --- a/.mvn/maven.config +++ b/.mvn/maven.config @@ -1 +1 @@ --DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring +-P spring diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 00d32aab1d..ffa3a6bb04 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,2 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/README.adoc b/README.adoc index 5dfeafc2aa..66e1ebf2d4 100644 --- a/README.adoc +++ b/README.adoc @@ -5,2480 +5,3 @@ Edit the files in the src/main/asciidoc/ directory instead. //// -= Spring Cloud Kubernetes -:doctype: book -:idprefix: -:idseparator: - -:toc: left -:toclevels: 4 -:tabsize: 4 -:numbered: -:sectanchors: -:sectnums: -:icons: font -:hide-uri-scheme: -:docinfo: shared,private - -:sc-ext: java -:project-full-name: Spring Cloud Kubernetes -:all: {asterisk}{asterisk} - -This reference guide covers how to use Spring Cloud Kubernetes. - -== Why do you need Spring Cloud Kubernetes? - -Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes. If you are just getting started in your journey to running your Spring Boot app on Kubernetes you can accomplish a lot with nothing more than a basic Spring Boot app and Kubernetes itself. To learn more, you can get started by reading the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes[Spring Boot reference documentation for deploying to Kubernetes ] and also working through the workshop material https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop[Spring and Kubernetes]. - -== Starters - -Starters are convenient dependency descriptors you can include in your -application. Include a starter to get the dependencies and Spring Boot -auto-configuration for a feature set. Starters that begin with `spring-cloud-starter-kubernetes-fabric8` -provide implementations using the https://github.com/fabric8io/kubernetes-client[Fabric8 Kubernetes Java Client]. -Starters that begin with -`spring-cloud-starter-kubernetes-client` provide implementations using the https://github.com/kubernetes-client/java[Kubernetes Java Client]. - -[cols="a,d"] -|=== -| Starter | Features - -| [source,xml] -.Fabric8 Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8 - ----- - -[source,xml] -.Kubernetes Client Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client - ----- -| <> implementation that -resolves service names to Kubernetes Services. - -| [source,xml] -.Fabric8 Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8-config - ----- - -[source,xml] -.Kubernetes Client Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-config - ----- -| Load application properties from Kubernetes -<> and <>. -<> application properties when a ConfigMap or -Secret changes. - -| [source,xml] -.Fabric8 Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8-all - ----- - -[source,xml] -.Kubernetes Client Dependency ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-all - ----- -| All Spring Cloud Kubernetes features. -|=== - -== DiscoveryClient for Kubernetes - -This project provides an implementation of https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java[Discovery Client] -for https://kubernetes.io[Kubernetes]. -This client lets you query Kubernetes endpoints (see https://kubernetes.io/docs/user-guide/services/[services]) by name. -A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent `http` and `https` addresses and that a client can -access from a Spring Boot application running as a pod. - -DiscoveryClient can also find services of type `ExternalName` (see https://kubernetes.io/docs/concepts/services-networking/service/#externalname[ExternalName services]). At the moment, external name support type of services is only available if the following property `spring.cloud.kubernetes.discovery.include-external-name-services` is set to `true` and only in the `fabric8` implementation. In a later release, support will be added for the kubernetes native client also. - -This is something that you get for free by adding the following dependency inside your project: - -==== -HTTP Based `DiscoveryClient` -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-discoveryclient - ----- -==== - -NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the -<>. - -==== -Fabric8 Kubernetes Client -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8 - ----- -==== - -==== -Kubernetes Java Client -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client - ----- -==== - -To enable loading of the `DiscoveryClient`, add `@EnableDiscoveryClient` to the according configuration or application class, as the following example shows: - -==== -[source,java] ----- -@SpringBootApplication -@EnableDiscoveryClient -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} ----- -==== - -Then you can inject the client in your code simply by autowiring it, as the following example shows: - -==== -[source,java] ----- -@Autowired -private DiscoveryClient discoveryClient; ----- -==== - -You can choose to enable `DiscoveryClient` from all namespaces by setting the following property in `application.properties`: - -==== -[source] ----- -spring.cloud.kubernetes.discovery.all-namespaces=true ----- -==== - -To discover services and endpoints only from specified namespaces you should set property `all-namespaces` to `false` and set the following property in `application.properties` (in this example namespaces are: `ns1` and `ns2`). - -==== -[source] ----- -spring.cloud.kubernetes.discovery.namespaces[0]=ns1 -spring.cloud.kubernetes.discovery.namespaces[1]=ns2 ----- -==== - -To discover service endpoint addresses that are not marked as "ready" by the kubernetes api server, you can set the following property in `application.properties` (default: false): - -==== -[source] ----- -spring.cloud.kubernetes.discovery.include-not-ready-addresses=true ----- -NOTE: This might be useful when discovering services for monitoring purposes, and would enable inspecting the `/health` endpoint of not-ready service instances. -==== - -If your service exposes multiple ports, you will need to specify which port the `DiscoveryClient` should use. -The `DiscoveryClient` will choose the port using the following logic. - -1. If the service has a label `primary-port-name` it will use the port with the name specified in the label's value. -2. If no label is present, then the port name specified in `spring.cloud.kubernetes.discovery.primary-port-name` will be used. -3. If neither of the above are specified it will use the port named `https`. -4. If none of the above conditions are met it will use the port named `http`. -5. As a last resort it wil pick the first port in the list of ports. - -WARNING: The last option may result in non-deterministic behaviour. -Please make sure to configure your service and/or application accordingly. - -By default all of the ports and their names will be added to the metadata of the `ServiceInstance`. - -As said before, if you want to get the list of `ServiceInstance` to also include the `ExternalName` type services, you need to enable that support via: `spring.cloud.kubernetes.discovery.include-external-name-services=true`. As such, when calling `DiscoveryClient::getInstances` those will be returned also. You can distinguish between `ExternalName` and any other types by inspecting `ServiceInstance::getMetadata` and lookup for a field called `type`. This will be the type of the service returned : `ExternalName`/`ClusterIP`, etc. - -`ServiceInstance` can include the labels and annotations of specific pods from the underlying service instance. To obtain such information, you need to also enable: - -`spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true` and/or `spring.cloud.kubernetes.discovery.metadata.add-pod-annotations=true`. At the moment, such functionality is present only in the fabric8 client implementation, but will be added to the kubernetes native client in a later release. - -If, for any reason, you need to disable the `DiscoveryClient`, you can set the following property in `application.properties`: - -==== -[source] ----- -spring.cloud.kubernetes.discovery.enabled=false ----- -==== - -Some Spring Cloud components use the `DiscoveryClient` in order to obtain information about the local service instance. For -this to work, you need to align the Kubernetes service name with the `spring.application.name` property. - -NOTE: `spring.application.name` has no effect as far as the name registered for the application within Kubernetes - -Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the -`DiscoveryClient` implementation accordingly. By "watch" we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay` -milliseconds (by default it is `30000`). The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints -(for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event -should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract. -The endpoints will be queried in either : - - - all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`) - - - specific namespaces (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example: - -``` -spring: - cloud: - kubernetes: - discovery: - namespaces: - - namespace-a - - namespace-b -``` - -- we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken. - -In order to enable this functionality you need to add -`@EnableScheduling` on a configuration class in your application. - -By default, we use the `Endpoints`(see https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) API to find out the current state of services. There is another way though, via `EndpointSlices` (https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/). Such support can be enabled via a property: `spring.cloud.kubernetes.discovery.use-endpoint-slices=true` (by default it is `false`). Of course, your cluster has to support it also. As a matter of fact, if you enable this property, but your cluster does not support it, we will fail starting the application. If you decide to enable such support, you also need proper Role/ClusterRole set-up. For example: - -``` -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - namespace: default - name: namespace-reader -rules: - - apiGroups: ["discovery.k8s.io"] - resources: ["endpointslices"] - verbs: ["get", "list", "watch"] -``` - -== Kubernetes native service discovery - -Kubernetes itself is capable of (server side) service discovery (see: https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services). -Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio (https://istio.io), a service mesh that is capable of load balancing, circuit breaker, failover, and much more. - -The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring `RestTemplate` that refers to a fully qualified domain name (FQDN), such as `https://{service-name}.{namespace}.svc.{cluster}.local:{service-port}`. - -Additionally, you can use Hystrix for: - -* Circuit breaker implementation on the caller side, by annotating the spring boot application class with `@EnableCircuitBreaker` -* Fallback functionality, by annotating the respective method with `@HystrixCommand(fallbackMethod=` - -== Kubernetes PropertySource implementations - -The most common approach to configuring your Spring Boot application is to create an `application.properties` or `application.yaml` or -an `application-profile.properties` or `application-profile.yaml` file that contains key-value pairs that provide customization values to your -application or Spring Boot starters. You can override these properties by specifying system properties or environment -variables. - -To enable this functionality you need to set `spring.config.import=kubernetes:` in your application's configuration properties. -Currently you can not specify a ConfigMap or Secret to load using `spring.config.import`, by default Spring Cloud Kubernetes -will load a ConfigMap and/or Secret based on the `spring.application.name` property. If `spring.application.name` is not set it will -load a ConfigMap and/or Secret with the name `application`. - -If you would like to load Kubernetes ``PropertySource``s during the bootstrap phase like it worked prior to the 3.0.x release -you can either add `spring-cloud-starter-bootstrap` to your application's classpath or set `spring.cloud.bootstrap.enabled=true` -as an environment variable. - -[[configmap-propertysource]] -=== Using a `ConfigMap` `PropertySource` - -Kubernetes provides a resource named https://kubernetes.io/docs/user-guide/configmap/[`ConfigMap`] to externalize the -parameters to pass to your application in the form of key-value pairs or embedded `application.properties` or `application.yaml` files. -The link:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-fabric8-config[Spring Cloud Kubernetes Config] project makes Kubernetes `ConfigMap` instances available -during application startup and triggers hot reloading of beans or Spring context when changes are detected on -observed `ConfigMap` instances. - -Everything that follows is explained mainly referring to examples using ConfigMaps, but the same stands for -Secrets, i.e.: every feature is supported for both. - -The default behavior is to create a `Fabric8ConfigMapPropertySource` (or a `KubernetesClientConfigMapPropertySource`) based on a Kubernetes `ConfigMap` that has a `metadata.name` value of either the name of -your Spring application (as defined by its `spring.application.name` property) or a custom name defined within the -`application.properties` file under the following key: `spring.cloud.kubernetes.config.name`. - -However, more advanced configuration is possible where you can use multiple `ConfigMap` instances. -The `spring.cloud.kubernetes.config.sources` list makes this possible. -For example, you could define the following `ConfigMap` instances: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - config: - name: default-name - namespace: default-namespace - sources: - # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace - - name: c1 - # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2 - - namespace: n2 - # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3 - - namespace: n3 - name: c3 ----- -==== - -In the preceding example, if `spring.cloud.kubernetes.config.namespace` had not been set, -the `ConfigMap` named `c1` would be looked up in the namespace that the application runs. -See <> to get a better understanding of how the namespace -of the application is resolved. - - -Any matching `ConfigMap` that is found is processed as follows: - -* Apply individual configuration properties. -* Apply as `yaml` (or `properties`) the content of any property that is named by the value of `spring.application.name` - (if it's not present, by `application.yaml/properties`) -* Apply as a properties file the content of the above name + each active profile. - -An example should make a lot more sense. Let's suppose that `spring.application.name=my-app` and that -we have a single active profile called `k8s`. For a configuration as below: - - -==== -[source] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: my-app -data: - my-app.yaml: |- - ... - my-app-k8s.yaml: |- - .. - my-app-dev.yaml: |- - .. - someProp: someValue ----- -==== - -These is what we will end-up loading: - - - `my-app.yaml` treated as a file - - `my-app-k8s.yaml` treated as a file - - `my-app-dev.yaml` _ignored_, since `dev` is _not_ an active profile - - `someProp: someValue` plain property - -The single exception to the aforementioned flow is when the `ConfigMap` contains a *single* key that indicates -the file is a YAML or properties file. In that case, the name of the key does NOT have to be `application.yaml` or -`application.properties` (it can be anything) and the value of the property is treated correctly. -This features facilitates the use case where the `ConfigMap` was created by using something like the following: - -==== -[source] ----- -kubectl create configmap game-config --from-file=/path/to/app-config.yaml ----- -==== - -Assume that we have a Spring Boot application named `demo` that uses the following properties to read its thread pool -configuration. - -* `pool.size.core` -* `pool.size.maximum` - -This can be externalized to config map in `yaml` format as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - pool.size.core: 1 - pool.size.max: 16 ----- -==== - -Individual properties work fine for most cases. However, sometimes, embedded `yaml` is more convenient. In this case, we -use a single property named `application.yaml` to embed our `yaml`, as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yaml: |- - pool: - size: - core: 1 - max:16 ----- -==== - -The following example also works: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - custom-name.yaml: |- - pool: - size: - core: 1 - max:16 ----- -==== - -You can also define the search to happen based on labels, for example: - - -==== -[source,yaml] ----- -spring: - application: - name: labeled-configmap-with-prefix - cloud: - kubernetes: - config: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - letter: a ----- -==== - -This will search for every configmap in namespace `spring-k8s` that has labels `{letter : a}`. The important -thing to notice here is that unlike reading a configmap by name, this can result in _multiple_ config maps read. -As usual, the same feature is supported for secrets. - -You can also configure Spring Boot applications differently depending on active profiles that are merged together -when the `ConfigMap` is read. You can provide different property values for different profiles by using an -`application.properties` or `application.yaml` property, specifying profile-specific values, each in their own document -(indicated by the `---` sequence), as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yml: |- - greeting: - message: Say Hello to the World - farewell: - message: Say Goodbye - --- - spring: - profiles: development - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers - --- - spring: - profiles: production - greeting: - message: Say Hello to the Ops ----- -==== - -In the preceding case, the configuration loaded into your Spring Application with the `development` profile is as follows: - -==== -[source,yaml] ----- - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers ----- -==== - -However, if the `production` profile is active, the configuration becomes: - -==== -[source,yaml] ----- - greeting: - message: Say Hello to the Ops - farewell: - message: Say Goodbye ----- -==== - -If both profiles are active, the property that appears last within the `ConfigMap` overwrites any preceding values. - -Another option is to create a different config map per profile and spring boot will automatically fetch it based -on active profiles - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yml: |- - greeting: - message: Say Hello to the World - farewell: - message: Say Goodbye ----- -==== -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo-development -data: - application.yml: |- - spring: - profiles: development - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers ----- -==== -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo-production -data: - application.yml: |- - spring: - profiles: production - greeting: - message: Say Hello to the Ops - farewell: - message: Say Goodbye ----- -==== - - -To tell Spring Boot which `profile` should be enabled see the https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles[Spring Boot documentation]. -One option for activating a specific profile when deploying to Kubernetes is to launch your Spring Boot application with an environment variable that you can define in the PodSpec at the container specification. - Deployment resource file, as follows: - -==== -[source,yaml] ----- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: deployment-name - labels: - app: deployment-name -spec: - replicas: 1 - selector: - matchLabels: - app: deployment-name - template: - metadata: - labels: - app: deployment-name - spec: - containers: - - name: container-name - image: your-image - env: - - name: SPRING_PROFILES_ACTIVE - value: "development" ----- -==== - -You could run into a situation where there are multiple configs maps that have the same property names. For example: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-one -data: - application.yml: |- - greeting: - message: Say Hello from one ----- -==== - -and - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-two -data: - application.yml: |- - greeting: - message: Say Hello from two ----- -==== - -Depending on the order in which you place these in `bootstrap.yaml|properties`, you might end up with an un-expected result (the last config map wins). For example: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - config: - namespace: default-namespace - sources: - - name: config-map-two - - name: config-map-one ----- -==== - -will result in property `greetings.message` being `Say Hello from one`. - -There is a way to change this default configuration by specifying `useNameAsPrefix`. For example: - -==== -[source,yaml] ----- -spring: - application: - name: with-prefix - cloud: - kubernetes: - config: - useNameAsPrefix: true - namespace: default-namespace - sources: - - name: config-map-one - useNameAsPrefix: false - - name: config-map-two ----- -==== - -Such a configuration will result in two properties being generated: - - - `greetings.message` equal to `Say Hello from one`. - - - `config-map-two.greetings.message` equal to `Say Hello from two` - -Notice that `spring.cloud.kubernetes.config.useNameAsPrefix` has a _lower_ priority than `spring.cloud.kubernetes.config.sources.useNameAsPrefix`. -This allows you to set a "default" strategy for all sources, at the same time allowing to override only a few. - -If using the config map name is not an option, you can specify a different strategy, called : `explicitPrefix`. Since this is an _explicit_ prefix that -you select, it can only be supplied to the `sources` level. At the same time it has a higher priority than `useNameAsPrefix`. Let's suppose we have a third config map with these entries: - - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-three -data: - application.yml: |- - greeting: - message: Say Hello from three ----- -==== - -A configuration like the one below: - -==== -[source,yaml] ----- -spring: - application: - name: with-prefix - cloud: - kubernetes: - config: - useNameAsPrefix: true - namespace: default-namespace - sources: - - name: config-map-one - useNameAsPrefix: false - - name: config-map-two - explicitPrefix: two - - name: config-map-three ----- -==== - -will result in three properties being generated: - - - `greetings.message` equal to `Say Hello from one`. - - - `two.greetings.message` equal to `Say Hello from two`. - - - `config-map-three.greetings.message` equal to `Say Hello from three`. - -The same way you configure a prefix for configmaps, you can do it for secrets also; both for secrets that are based on name -and the ones based on labels. For example: - -==== -[source.yaml] ----- -spring: - application: - name: prefix-based-secrets - cloud: - kubernetes: - secrets: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - letter: a - useNameAsPrefix: false - - labels: - letter: b - explicitPrefix: two - - labels: - letter: c - - labels: - letter: d - useNameAsPrefix: true - - name: my-secret ----- -==== - -The same processing rules apply when generating property source as for config maps. The only difference is that -potentially, looking up secrets by labels can mean that we find more than one source. In such a case, prefix (if specified via `useNameAsPrefix`) -will be the names of all secrets found for those particular labels. - -One more thing to bear in mind is that we support `prefix` per _source_, not per secret. The easiest way to explain this is via an example: - -==== -[source.yaml] ----- -spring: - application: - name: prefix-based-secrets - cloud: - kubernetes: - secrets: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - color: blue - useNameAsPrefix: true ----- -==== - -Suppose that a query matching such a label will provide two secrets as a result: `secret-a` and `secret-b`. -Both of these secrets have the same property name: `color=sea-blue` and `color=ocean-blue`. It is undefined which -`color` will end-up as part of property sources, but the prefix for it will be `secret-a.secret-b` -(concatenated sorted naturally, names of the secrets). - -If you need more fine-grained results, adding more labels to identify the secret uniquely would be an option. - - - -By default, besides reading the config map that is specified in the `sources` configuration, Spring will also try to read -all properties from "profile aware" sources. The easiest way to explain this is via an example. Let's suppose your application -enables a profile called "dev" and you have a configuration like the one below: - -==== -[source,yaml] ----- -spring: - application: - name: spring-k8s - cloud: - kubernetes: - config: - namespace: default-namespace - sources: - - name: config-map-one ----- -==== - -Besides reading the `config-map-one`, Spring will also try to read `config-map-one-dev`; in this particular order. Each active profile -generates such a profile aware config map. - -Though your application should not be impacted by such a config map, it can be disabled if needed: - -==== -[source,yaml] ----- -spring: - application: - name: spring-k8s - cloud: - kubernetes: - config: - includeProfileSpecificSources: false - namespace: default-namespace - sources: - - name: config-map-one - includeProfileSpecificSources: false ----- -==== - -Notice that just like before, there are two levels where you can specify this property: for all config maps or -for individual ones; the latter having a higher priority. - -NOTE: You should check the security configuration section. To access config maps from inside a pod you need to have the correct -Kubernetes service accounts, roles and role bindings. - -Another option for using `ConfigMap` instances is to mount them into the Pod by running the Spring Cloud Kubernetes application -and having Spring Cloud Kubernetes read them from the file system. - -NOTE: This feature is deprecated and will be removed in a future release (Use `spring.config.import` instead). -This behavior is controlled by the `spring.cloud.kubernetes.config.paths` property. You can use it in -addition to or instead of the mechanism described earlier. -`spring.cloud.kubernetes.config.paths` expects a List of full paths to each property file, because directories are not being recursively parsed. For example: - -``` -spring: - cloud: - kubernetes: - config: - paths: - - /tmp/application.properties - - /var/application.yaml -``` - -NOTE: If you use `spring.cloud.kubernetes.config.paths` or `spring.cloud.kubernetes.secrets.path` the automatic reload -functionality will not work. You will need to make a `POST` request to the `/actuator/refresh` endpoint or -restart/redeploy the application. - -[#config-map-fail-fast] -In some cases, your application may be unable to load some of your `ConfigMaps` using the Kubernetes API. -If you want your application to fail the start-up process in such cases, you can set -`spring.cloud.kubernetes.config.fail-fast=true` to make the application start-up fail with an Exception. - -[#config-map-retry] -You can also make your application retry loading `ConfigMap` property sources on a failure. First, you need to -set `spring.cloud.kubernetes.config.fail-fast=true`. Then you need to add `spring-retry` -and `spring-boot-starter-aop` to your classpath. You can configure retry properties such as -the maximum number of attempts, backoff options like initial interval, multiplier, max interval by setting the -`spring.cloud.kubernetes.config.retry.*` properties. - -NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason -and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `ConfigMap` `PropertySources` -by setting `spring.cloud.kubernetes.config.retry.enabled=false`. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.config.enabled` | `Boolean` | `true` | Enable ConfigMaps `PropertySource` -| `spring.cloud.kubernetes.config.name` | `String` | `${spring.application.name}` | Sets the name of `ConfigMap` to look up -| `spring.cloud.kubernetes.config.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to lookup -| `spring.cloud.kubernetes.config.paths` | `List` | `null` | Sets the paths where `ConfigMap` instances are mounted -| `spring.cloud.kubernetes.config.enableApi` | `Boolean` | `true` | Enable or disable consuming `ConfigMap` instances through APIs -| `spring.cloud.kubernetes.config.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `ConfigMap` -| `spring.cloud.kubernetes.config.retry.enabled` | `Boolean` | `true` | Enable or disable config retry. -| `spring.cloud.kubernetes.config.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. -| `spring.cloud.kubernetes.config.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. -| `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. -| `spring.cloud.kubernetes.config.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. -|=== - -=== Secrets PropertySource - -Kubernetes has the notion of https://kubernetes.io/docs/concepts/configuration/secret/[Secrets] for storing -sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with `Secrets` to make secrets -accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the `spring.cloud.kubernetes.secrets.enabled` property. - -When enabled, the `Fabric8SecretsPropertySource` looks up Kubernetes for `Secrets` from the following sources: - -. Reading recursively from secrets mounts -. Named after the application (as defined by `spring.application.name`) -. Matching some labels - -*Note:* - -By default, consuming Secrets through the API (points 2 and 3 above) *is not enabled* for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. -Further, we recommend that containers share secrets through mounted volumes. - -If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. -For more information about risks and best practices when consuming Secrets through the API refer to https://kubernetes.io/docs/concepts/configuration/secret/#best-practices[this doc]. - -If the secrets are found, their data is made available to the application. - -Assume that we have a spring boot application named `demo` that uses properties to read its database -configuration. We can create a Kubernetes secret by using the following command: - -==== -[source] ----- -kubectl create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd ----- -==== - -The preceding command would create the following secret (which you can see by using `kubectl get secrets db-secret -o yaml`): - -==== -[source,yaml] ----- -apiVersion: v1 -data: - password: cDQ1NXcwcmQ= - username: dXNlcg== -kind: Secret -metadata: - creationTimestamp: 2017-07-04T09:15:57Z - name: db-secret - namespace: default - resourceVersion: "357496" - selfLink: /api/v1/namespaces/default/secrets/db-secret - uid: 63c89263-6099-11e7-b3da-76d6186905a8 -type: Opaque ----- -==== - -Note that the data contains Base64-encoded versions of the literal provided by the `create` command. - -Your application can then use this secret -- for example, by exporting the secret's value as environment variables: - -==== -[source,yaml] ----- -apiVersion: v1 -kind: Deployment -metadata: - name: ${project.artifactId} -spec: - template: - spec: - containers: - - env: - - name: DB_USERNAME - valueFrom: - secretKeyRef: - name: db-secret - key: username - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: db-secret - key: password ----- -==== - -You can select the Secrets to consume in a number of ways: - -. By listing the directories where secrets are mapped: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql ----- -==== -+ -If you have all the secrets mapped to a common root, you can set them like: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.paths=/etc/secrets ----- -==== - -. By setting a named secret: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.name=db-secret ----- -==== - -. By defining a list of labels: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.labels.broker=activemq --Dspring.cloud.kubernetes.secrets.labels.db=postgresql ----- -==== - -As the case with `ConfigMap`, more advanced configuration is also possible where you can use multiple `Secret` -instances. The `spring.cloud.kubernetes.secrets.sources` list makes this possible. -For example, you could define the following `Secret` instances: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - secrets: - name: default-name - namespace: default-namespace - sources: - # Spring Cloud Kubernetes looks up a Secret named s1 in namespace default-namespace - - name: s1 - # Spring Cloud Kubernetes looks up a Secret named default-name in namespace n2 - - namespace: n2 - # Spring Cloud Kubernetes looks up a Secret named s3 in namespace n3 - - namespace: n3 - name: s3 ----- -==== - -In the preceding example, if `spring.cloud.kubernetes.secrets.namespace` had not been set, -the `Secret` named `s1` would be looked up in the namespace that the application runs. -See <> to get a better understanding of how the namespace -of the application is resolved. - -<>; if you want your application to fail to start -when it is unable to load `Secrets` property sources, you can set `spring.cloud.kubernetes.secrets.fail-fast=true`. - -It is also possible to enable retry for `Secret` property sources <>. -As with the `ConfigMap` property sources, first you need to set `spring.cloud.kubernetes.secrets.fail-fast=true`. -Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath. -Retry behavior of the `Secret` property sources can be configured by setting the `spring.cloud.kubernetes.secrets.retry.*` -properties. - -NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason -and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `Secrets` `PropertySources` -by setting `spring.cloud.kubernetes.secrets.retry.enabled=false`. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.secrets.enabled` | `Boolean` | `true` | Enable Secrets `PropertySource` -| `spring.cloud.kubernetes.secrets.name` | `String` | `${spring.application.name}` | Sets the name of the secret to look up -| `spring.cloud.kubernetes.secrets.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to look up -| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` | Sets the labels used to lookup secrets -| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` | Sets the paths where secrets are mounted (example 1) -| `spring.cloud.kubernetes.secrets.enableApi` | `Boolean` | `false` | Enables or disables consuming secrets through APIs (examples 2 and 3) -| `spring.cloud.kubernetes.secrets.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `Secret` -| `spring.cloud.kubernetes.secrets.retry.enabled` | `Boolean` | `true` | Enable or disable secrets retry. -| `spring.cloud.kubernetes.secrets.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. -| `spring.cloud.kubernetes.secrets.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. -| `spring.cloud.kubernetes.secrets.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. -| `spring.cloud.kubernetes.secrets.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. -|=== - -Notes: - -* The `spring.cloud.kubernetes.secrets.labels` property behaves as defined by -https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#map-based-binding[Map-based binding]. -* The `spring.cloud.kubernetes.secrets.paths` property behaves as defined by -https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#collection-based-binding[Collection-based binding]. -* Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod. - -You can find an example of an application that uses secrets (though it has not been updated to use the new `spring-cloud-kubernetes` project) at -https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-camel-config] - -[[namespace-resolution]] -=== Namespace resolution -Finding an application namespace happens on a best-effort basis. There are some steps that we iterate in order -to find it. The easiest and most common one, is to specify it in the proper configuration, for example: - -==== -[source,yaml] ----- -spring: - application: - name: app - cloud: - kubernetes: - secrets: - name: secret - namespace: default - sources: - # Spring Cloud Kubernetes looks up a Secret named 'a' in namespace 'default' - - name: a - # Spring Cloud Kubernetes looks up a Secret named 'secret' in namespace 'b' - - namespace: b - # Spring Cloud Kubernetes looks up a Secret named 'd' in namespace 'c' - - namespace: c - name: d ----- -==== - -Remember that the same can be done for config maps. If such a namespace is not specified, it will be read (in this order): - -1. from property `spring.cloud.kubernetes.client.namespace` -2. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath` property -3. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file -(kubernetes default namespace path) -4. from a designated client method call (for example fabric8's : `KubernetesClient::getNamespace`), if the client provides -such a method. This, in turn, could be configured via environment properties. For example fabric8 client can be configured via -"KUBERNETES_NAMESPACE" property; consult the client documentation for exact details. - -Failure to find a namespace from the above steps will result in an Exception being raised. - -[[order_of_configMaps_and_secrets]] -=== Order of ConfigMaps and Secrets - -If, for whatever reason, you enabled both configmaps and secrets, and there is a common property between them, the value from the ConfigMap will have a higher precedence. That is: it will override whatever values are found in secrets. - -=== `PropertySource` Reload - -WARNING: This functionality has been deprecated in the 2020.0 release. Please see -the <> controller for an alternative way -to achieve the same functionality. - -Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. -The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or -`Secret` changes. - -By default, this feature is disabled. You can enable it by using the `spring.cloud.kubernetes.reload.enabled=true` configuration property (for example, in the `application.properties` file). -Please notice that this will enable monitoring of configmaps only (i.e.: `spring.cloud.kubernetes.reload.monitoring-config-maps` will be set to `true`). -If you want to enable monitoring of secrets, this must be done explicitly via : `spring.cloud.kubernetes.reload.monitoring-secrets=true`. - -The following levels of reload are supported (by setting the `spring.cloud.kubernetes.reload.strategy` property): - -* `refresh` (default): Only configuration beans annotated with `@ConfigurationProperties` or `@RefreshScope` are reloaded. -This reload level leverages the refresh feature of Spring Cloud Context. - -* `restart_context`: the whole Spring `ApplicationContext` is gracefully restarted. Beans are recreated with the new configuration. -In order for the restart context functionality to work properly you must enable and expose the restart actuator endpoint -[source,yaml] -==== ----- -management: - endpoint: - restart: - enabled: true - endpoints: - web: - exposure: - include: restart ----- -==== - -* `shutdown`: the Spring `ApplicationContext` is shut down to activate a restart of the container. - When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the `ApplicationContext` -and that a replication controller or replica set is configured to restart the pod. - -Assuming that the reload feature is enabled with default settings (`refresh` mode), the following bean is refreshed when the config map changes: - -==== -[java, source] ----- -@Configuration -@ConfigurationProperties(prefix = "bean") -public class MyConfig { - - private String message = "a message that can be changed live"; - - // getter and setters - -} ----- -==== - -To see that changes effectively happen, you can create another bean that prints the message periodically, as follows - -==== -[source,java] ----- -@Component -public class MyBean { - - @Autowired - private MyConfig config; - - @Scheduled(fixedDelay = 5000) - public void hello() { - System.out.println("The message is: " + config.getMessage()); - } -} ----- -==== - -You can change the message printed by the application by using a `ConfigMap`, as follows: - -==== -[source,yaml] ----- -apiVersion: v1 -kind: ConfigMap -metadata: - name: reload-example -data: - application.properties: |- - bean.message=Hello World! ----- -==== - -Any change to the property named `bean.message` in the `ConfigMap` associated with the pod is reflected in the -output. More generally speaking, changes associated to properties prefixed with the value defined by the `prefix` -field of the `@ConfigurationProperties` annotation are detected and reflected in the application. -<> is explained earlier in this chapter. - -The reload feature supports two operating modes: - -* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). -Any event produces a re-check on the configuration and, in case of changes, a reload. -The `view` role on the service account is required in order to listen for config map changes. A higher level role (such as `edit`) is required for secrets -(by default, secrets are not monitored). -* Polling: Periodically re-creates the configuration from config maps and secrets to see if it has changed. -You can configure the polling period by using the `spring.cloud.kubernetes.reload.period` property and defaults to 15 seconds. -It requires the same role as the monitored property source. -This means, for example, that using polling on file-mounted secret sources does not require particular privileges. - -[[namespace-label-filtering]] -=== Reload namespace and label filtering -By default, a namespace chosen using the steps outlined in <> will be used to listen to changes -in configmaps and secrets. i.e.: if you do not tell reload what namespaces and configmaps/secrets to watch for, -it will watch all configmaps/secrets from the namespace that will be computed using the above algorithm. - -On the other hand, you can define a more fine-grained approach. For example, you can specify the namespaces where -changes will be monitored: - -==== -[source,yaml] ----- -spring: - application: - name: event-reload - cloud: - kubernetes: - reload: - enabled: true - strategy: shutdown - mode: event - namespaces: - - my-namespace ----- -==== - -Such a configuration will make the app watch changes only in the `my-namespace` namespace. Mind that this will -watch _all_ configmaps/secrets (depending on which one you enable). If you want an even more fine-grained approach, -you can enable "label-filtering". First we need to enable such support via : `enable-reload-filtering: true` - -==== -[source,yaml] ----- -spring: - application: - name: event-reload - cloud: - kubernetes: - reload: - enabled: true - strategy: shutdown - mode: event - namespaces: - - my-namespaces - monitoring-config-maps: true - enable-reload-filtering: true ----- -==== - -What this will do, is watch configmaps/secrets that only have the `spring.cloud.kubernetes.config.informer.enabled: true` label. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.reload.enabled` | `Boolean` | `false` | Enables monitoring of property sources and configuration reload -| `spring.cloud.kubernetes.reload.monitoring-config-maps` | `Boolean` | `true` | Allow monitoring changes in config maps -| `spring.cloud.kubernetes.reload.monitoring-secrets` | `Boolean` | `false` | Allow monitoring changes in secrets -| `spring.cloud.kubernetes.reload.strategy` | `Enum` | `refresh` | The strategy to use when firing a reload (`refresh`, `restart_context`, or `shutdown`) -| `spring.cloud.kubernetes.reload.mode` | `Enum` | `event` | Specifies how to listen for changes in property sources (`event` or `polling`) -| `spring.cloud.kubernetes.reload.period` | `Duration`| `15s` | The period for verifying changes when using the `polling` strategy -| `spring.cloud.kubernetes.reload.namespaces` | `String[]`| | namespaces where we should watch for changes -| `spring.cloud.kubernetes.reload.enable-reload-filtering` | `String` | | enabled labeled filtering for reload functionality -|=== - -Notes: - -* You should not use properties under `spring.cloud.kubernetes.reload` in config maps or secrets. Changing such properties at runtime may lead to unexpected results. -* Deleting a property or the whole config map does not restore the original state of the beans when you use the `refresh` level. - -== Kubernetes Ecosystem Awareness - -All features described earlier in this guide work equally well, regardless of whether your application is running inside -Kubernetes. This is really helpful for development and troubleshooting. -From a development point of view, this lets you start your Spring Boot application and debug one -of the modules that is part of this project. You need not deploy it in Kubernetes, -as the code of the project relies on the -https://github.com/fabric8io/kubernetes-client[Fabric8 Kubernetes Java client], which is a fluent DSL that can -communicate by using `http` protocol to the REST API of the Kubernetes Server. - -Kubernetes awareness is based on Spring Boot API, specifically on https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnCloudPlatform.html[ConditionalOnCloudPlatform]. -That property will auto-detect if your application is currently deployed in kubernetes or not. It is possible to override -that setting via `spring.main.cloud-platform`. - -For example, if you need to test some features, but do not want to deploy to a cluster, it is enough to set the: -`spring.main.cloud-platform=KUBERNETES`. This will make `spring-cloud-kubernetes` act as-if it is deployed in a real cluster. - -NOTE: If you have `spring-cloud-starter-bootstrap` on your classpath or are setting `spring.cloud.bootstrap.enabled=true` then -you will have to set `spring.main.cloud-platform` should be set in `bootstrap.{properties|yml}` -(or the profile specific one). Also note that these properties: `spring.cloud.kubernetes.config.enabled` and `spring.cloud.kubernetes.secrets.enabled` -will only take effect when set in `bootstrap.{properties|yml}` when you have `spring-cloud-starter-bootstrap` on your classpath or are setting `spring.cloud.bootstrap.enabled=true`. - -=== Breaking Changes In 3.0.x - -In versions of Spring Cloud Kubernetes prior to `3.0.x`, Kubernetes awareness was implemented using `spring.cloud.kubernetes.enabled` property. This -property was removed and is un-supported. Instead, we use Spring Boot API: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnCloudPlatform.html[ConditionalOnCloudPlatform]. -If it is needed to explicitly enable or disable this awareness, use `spring.main.cloud-platform=NONE/KUBERNETES`. - -=== Kubernetes Profile Autoconfiguration - -When the application runs as a pod inside Kubernetes, a Spring profile named `kubernetes` automatically gets activated. -This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed -within the Kubernetes platform (for example, different development and production configuration). - -=== Istio Awareness - -When you include the `spring-cloud-kubernetes-fabric8-istio` module in the application classpath, a new profile is added to the application, -provided the application is running inside a Kubernetes Cluster with https://istio.io[Istio] installed. You can then use -spring `@Profile("istio")` annotations in your Beans and `@Configuration` classes. - -The Istio awareness module uses `me.snowdrop:istio-client` to interact with Istio APIs, letting us discover traffic rules, circuit breakers, and so on, -making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according to the environment. - -== Pod Health Indicator - -Spring Boot uses https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java[`HealthIndicator`] to expose info about the health of an application. -That makes it really useful for exposing health-related information to the user and makes it a good fit for use as https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/[readiness probes]. - -The Kubernetes health indicator (which is part of the core module) exposes the following info: - -* Pod name, IP address, namespace, service account, node name, and its IP address -* A flag that indicates whether the Spring Boot application is internal or external to Kubernetes - -You can disable this `HealthContributor` by setting `management.health.kubernetes.enabled` -to `false` in `application.[properties | yaml]`. - -== Info Contributor - -Spring Cloud Kubernetes includes an `InfoContributor` which adds Pod information to -Spring Boot's `/info` Acturator endpoint. - -You can disable this `InfoContributor` by setting `management.info.kubernetes.enabled` -to `false` in `application.[properties | yaml]`. - -== Leader Election -The Spring Cloud Kubernetes leader election mechanism implements the leader election API of Spring Integration using a Kubernetes ConfigMap. - -Multiple application instances compete for leadership, but leadership will only be granted to one. -When granted leadership, a leader application receives an `OnGrantedEvent` application event with leadership `Context`. -Applications periodically attempt to gain leadership, with leadership granted to the first caller. -A leader will remain a leader until either it is removed from the cluster, or it yields its leadership. -When leadership removal occurs, the previous leader receives `OnRevokedEvent` application event. -After removal, any instances in the cluster may become the new leader, including the old leader. - -To include it in your project, add the following dependency. -==== -Fabric8 Leader Implementation -[source,xml] ----- - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-leader - ----- -==== - -To specify the name of the configmap used for leader election use the following property. -==== -[source,properties] ----- -spring.cloud.kubernetes.leader.config-map-name=leader ----- -==== - -== LoadBalancer for Kubernetes -This project includes Spring Cloud Load Balancer for load balancing based on Kubernetes Endpoints and provides implementation of load balancer based on Kubernetes Service. -To include it to your project add the following dependency. -==== -Fabric8 Implementation -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8-loadbalancer - ----- -==== - -==== -Kubernetes Java Client Implementation -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-loadbalancer - ----- -==== - -To enable load balancing based on Kubernetes Service name use the following property. Then load balancer would try to call application using address, for example `service-a.default.svc.cluster.local` -==== -[source] ----- -spring.cloud.kubernetes.loadbalancer.mode=SERVICE ----- -==== - -To enabled load balancing across all namespaces use the following property. Property from `spring-cloud-kubernetes-discovery` module is respected. -==== -[source] ----- -spring.cloud.kubernetes.discovery.all-namespaces=true ----- -==== - -If a service needs to be accessed over HTTPS you need to add a label or annotation to your service definition with the name `secured` and the value `true` and the load balancer will then use HTTPS to make requests to the service. - -== Security Configurations Inside Kubernetes - - -=== Namespace - -Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. -For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows: - -==== -[source] ----- - env: - - name: "KUBERNETES_NAMESPACE" - valueFrom: - fieldRef: - fieldPath: "metadata.namespace" ----- -==== - -=== Service Account - -For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with `spring-cloud-kubernetes` has access to the Kubernetes API. -For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. - -Depending on the requirements, you'll need `get`, `list` and `watch` permission on the following resources: - -.Kubernetes Resource Permissions -|=== -|Dependency | Resources - - -|spring-cloud-starter-kubernetes-fabric8 -|pods, services, endpoints - -|spring-cloud-starter-kubernetes-fabric8-config -|configmaps, secrets - -|spring-cloud-starter-kubernetes-client -|pods, services, endpoints - -|spring-cloud-starter-kubernetes-client-config -|configmaps, secrets -|=== - -For development purposes, you can add `cluster-reader` permissions to your `default` service account. On a production system you'll likely want to provide more granular permissions. - -The following Role and RoleBinding are an example for namespaced permissions for the `default` account: - -==== -[source,yaml] ----- -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - namespace: YOUR-NAME-SPACE - name: namespace-reader -rules: - - apiGroups: [""] - resources: ["configmaps", "pods", "services", "endpoints", "secrets"] - verbs: ["get", "list", "watch"] - ---- - -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: namespace-reader-binding - namespace: YOUR-NAME-SPACE -subjects: -- kind: ServiceAccount - name: default - apiGroup: "" -roleRef: - kind: Role - name: namespace-reader - apiGroup: "" ----- -==== - -== Service Registry Implementation - -In Kubernetes service registration is controlled by the platform, the application itself does not control -registration as it may do in other platforms. For this reason using `spring.cloud.service-registry.auto-registration.enabled` -or setting `@EnableDiscoveryClient(autoRegister=false)` will have no effect in Spring Cloud Kubernetes. - -[#spring-cloud-kubernetes-configuration-watcher] -## Spring Cloud Kubernetes Configuration Watcher - -Kubernetes provides the ability to https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume[mount a ConfigMap or Secret as a volume] -in the container of your application. When the contents of the ConfigMap or Secret changes, the https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically[mounted volume will be updated with those changes]. - -However, Spring Boot will not automatically update those changes unless you restart the application. Spring Cloud -provides the ability refresh the application context without restarting the application by either hitting the -actuator endpoint `/refresh` or via publishing a `RefreshRemoteApplicationEvent` using Spring Cloud Bus. - -To achieve this configuration refresh of a Spring Cloud app running on Kubernetes, you can deploy the Spring Cloud -Kubernetes Configuration Watcher controller into your Kubernetes cluster. - -The application is published as a container and is available on https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-configuration-watcher[Docker Hub]. - However, if you need to customize the config watcher behavior or prefer to build the image yourself you can easily build your own -image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher[source code on GitHub] and use that. - -Spring Cloud Kubernetes Configuration Watcher can send refresh notifications to applications in two ways. - -1. Over HTTP in which case the application being notified must of the `/refresh` actuator endpoint exposed and accessible from within the cluster -2. Using Spring Cloud Bus, in which case you will need a message broker deployed to your custer for the application to use. - -### Deployment YAML - -Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes. - -==== -[source,yaml] ----- ---- -apiVersion: v1 -kind: List -items: - - apiVersion: v1 - kind: Service - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - name: spring-cloud-kubernetes-configuration-watcher - spec: - ports: - - name: http - port: 8888 - targetPort: 8888 - selector: - app: spring-cloud-kubernetes-configuration-watcher - type: ClusterIP - - apiVersion: v1 - kind: ServiceAccount - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - name: spring-cloud-kubernetes-configuration-watcher - - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - name: spring-cloud-kubernetes-configuration-watcher:view - roleRef: - kind: Role - apiGroup: rbac.authorization.k8s.io - name: namespace-reader - subjects: - - kind: ServiceAccount - name: spring-cloud-kubernetes-configuration-watcher - - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - namespace: default - name: namespace-reader - rules: - - apiGroups: ["", "extensions", "apps"] - resources: ["configmaps", "pods", "services", "endpoints", "secrets"] - verbs: ["get", "list", "watch"] - - apiVersion: apps/v1 - kind: Deployment - metadata: - name: spring-cloud-kubernetes-configuration-watcher-deployment - spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configuration-watcher - template: - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - spec: - serviceAccount: spring-cloud-kubernetes-configuration-watcher - containers: - - name: spring-cloud-kubernetes-configuration-watcher - image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.1-SNAPSHOT - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8888 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8888 - path: /actuator/health/liveness - ports: - - containerPort: 8888 - ----- -==== - -The Service Account and associated Role Binding is important for Spring Cloud Kubernetes Configuration to work properly. -The controller needs access to read data about ConfigMaps, Pods, Services, Endpoints and Secrets in the Kubernetes cluster. - -### Monitoring ConfigMaps and Secrets - -Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMaps with a label of `spring.cloud.kubernetes.config` with the value `true` -or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels -or the values of those labels is not `true` then any changes will be ignored. - -If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret -and send a notification to the application with that name. This might not be enough for your use-case though, you could for example what to: - -- bind a config-map to multiple applications, so that a change inside a single configmap triggers a refresh for many services -- have profile based sources trigger events for your application - -For that reasons there is an addition annotation you could specify: - -`spring.cloud.kubernetes.configmap.apps` or `spring.cloud.kubernetes.secret.apps`. It takes a String of apps separated by comma, -that specifies the names of applications that will receive a notification when changes happen in this secret/configmap. - -For example: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: example-configmap - labels: - spring.cloud.kubernetes.config: "true" - annotations: - spring.cloud.kubernetes.configmap.apps: "app-a, app-b" ----- -==== - -### HTTP Implementation - -The HTTP implementation is what is used by default. When this implementation is used Spring Cloud Kubernetes Configuration Watcher and a -change to a ConfigMap or Secret occurs then the HTTP implementation will use the Spring Cloud Kubernetes Discovery Client to fetch all -instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator -`/refresh` endpoint. By default it will send the post request to `/actuator/refresh` using the port registered in the discovery client. - -#### Non-Default Management Port and Actuator Path - -If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application -can add an annotation called `boot.spring.io/actuator` and set its value to the path and port used by the application. For example - -==== -[source,yaml] ----- -apiVersion: v1 -kind: Service -metadata: - labels: - app: config-map-demo - name: config-map-demo - annotations: - boot.spring.io/actuator: http://:9090/myactuator/home -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: config-map-demo ----- -==== - - -Another way you can choose to configure the actuator path and/or management port is by setting -`spring.cloud.kubernetes.configuration.watcher.actuatorPath` and `spring.cloud.kubernetes.configuration.watcher.actuatorPort`. - -### Messaging Implementation - -The messaging implementation can be enabled by setting profile to either `bus-amqp` (RabbitMQ) or `bus-kafka` (Kafka) when the Spring Cloud Kubernetes Configuration Watcher -application is deployed to Kubernetes. - -### Configuring RabbitMQ - -When the `bus-amqp` profile is enabled you will need to configure Spring RabbitMQ to point it to the location of the RabbitMQ -instance you would like to use as well as any credentials necessary to authenticate. This can be done -by setting the standard Spring RabbitMQ properties, for example - -==== -[source,yaml] ----- -spring: - rabbitmq: - username: user - password: password - host: rabbitmq ----- -==== - -### Configuring Kafka - -When the `bus-kafka` profile is enabled you will need to configure Spring Kafka to point it to the location of the Kafka Broker -instance you would like to use. This can be done by setting the standard Spring Kafka properties, for example - -==== -[source,yaml] ----- -spring: - kafka: - producer: - bootstrap-servers: localhost:9092 ----- -==== - -[#spring-cloud-kubernetes-configserver] -## Spring Cloud Kubernetes Config Server - -The Spring Cloud Kubernetes Config Server, is based on https://spring.io/projects/spring-cloud-config[Spring Cloud Config Server] and adds an https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository[environment repository] for Kubernetes -https://kubernetes.io/docs/concepts/configuration/configmap/[Config Maps] and https://kubernetes.io/docs/concepts/configuration/secret/[Secrets]. - -This is component is completely optional. However, it allows you to continue to leverage configuration -you may have stored in existing environment repositories (Git, SVN, Vault, etc) with applications that you are running on Kubernetes. - -A default image is located on https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-configserver[Docker Hub] which will allow you to easily get a Config Server deployed on Kubernetes without building -the code and image yourself. However, if you need to customize the config server behavior or prefer to build the image yourself you can easily build your own -image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver[source code on GitHub] and use that. - -### Configuration - -#### Enabling The Kubernetes Environment Repository -To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. -You may activate other profiles as well to use other environment repository implementations. - -#### Config Map and Secret PropertySources -By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. -You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. - -#### Fetching Config Map and Secret Data From Additional Namespaces -By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. -If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated -list of namespace values. - -NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` -you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. - -#### Kubernetes Access Controls -The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that -it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). - -### Deployment Yaml - -Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. - -==== -[source,yaml] ----- ---- -apiVersion: v1 -kind: List -items: - - apiVersion: v1 - kind: Service - metadata: - labels: - app: spring-cloud-kubernetes-configserver - name: spring-cloud-kubernetes-configserver - spec: - ports: - - name: http - port: 8888 - targetPort: 8888 - selector: - app: spring-cloud-kubernetes-configserver - type: ClusterIP - - apiVersion: v1 - kind: ServiceAccount - metadata: - labels: - app: spring-cloud-kubernetes-configserver - name: spring-cloud-kubernetes-configserver - - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - labels: - app: spring-cloud-kubernetes-configserver - name: spring-cloud-kubernetes-configserver:view - roleRef: - kind: Role - apiGroup: rbac.authorization.k8s.io - name: namespace-reader - subjects: - - kind: ServiceAccount - name: spring-cloud-kubernetes-configserver - - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - namespace: default - name: namespace-reader - rules: - - apiGroups: ["", "extensions", "apps"] - resources: ["configmaps", "secrets"] - verbs: ["get", "list"] - - apiVersion: apps/v1 - kind: Deployment - metadata: - name: spring-cloud-kubernetes-configserver-deployment - spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configserver - template: - metadata: - labels: - app: spring-cloud-kubernetes-configserver - spec: - serviceAccount: spring-cloud-kubernetes-configserver - containers: - - name: spring-cloud-kubernetes-configserver - image: springcloud/spring-cloud-kubernetes-configserver - imagePullPolicy: IfNotPresent - env: - - name: SPRING_PROFILES_INCLUDE - value: "kubernetes" - readinessProbe: - httpGet: - port: 8888 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8888 - path: /actuator/health/liveness - ports: - - containerPort: 8888 - ----- -==== - -[#spring-cloud-kubernetes-discoveryserver] -## Spring Cloud Kubernetes Discovery Server - -The Spring Cloud Kubernetes Discovery Server provides HTTP endpoints apps can use to gather information -about services available within a Kubernetes cluster. The Spring Cloud Kubernetes Discovery Server -can be used by apps using the `spring-cloud-starter-kubernetes-discoveryclient` to provide data to -the `DiscoveryClient` implementation provided by that starter. - -### Permissions -The Spring Cloud Discovery server uses -the Kubernetes API server to get data about Service and Endpoint resrouces so it needs list, watch, and -get permissions to use those endpoints. See the below sample Kubernetes deployment YAML for an -examlpe of how to configure the Service Account on Kubernetes. - - -### Endpoints -There are three endpoints exposed by the server. - -#### `/apps` - -A `GET` request sent to `/apps` will return a JSON array of available services. Each item contains -the name of the Kubernetes service and service instance information. Below is a sample response. - -==== -[source,json] ----- -[ - { - "name":"spring-cloud-kubernetes-discoveryserver", - "serviceInstances":[ - { - "instanceId":"836a2f25-daee-4af2-a1be-aab9ce2b938f", - "serviceId":"spring-cloud-kubernetes-discoveryserver", - "host":"10.244.1.6", - "port":8761, - "uri":"http://10.244.1.6:8761", - "secure":false, - "metadata":{ - "app":"spring-cloud-kubernetes-discoveryserver", - "kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"name\":\"spring-cloud-kubernetes-discoveryserver\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":8761}],\"selector\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"type\":\"ClusterIP\"}}\n", - "http":"8761" - }, - "namespace":"default", - "scheme":"http" - } - ] - }, - { - "name":"kubernetes", - "serviceInstances":[ - { - "instanceId":"1234", - "serviceId":"kubernetes", - "host":"172.18.0.3", - "port":6443, - "uri":"http://172.18.0.3:6443", - "secure":false, - "metadata":{ - "provider":"kubernetes", - "component":"apiserver", - "https":"6443" - }, - "namespace":"default", - "scheme":"http" - } - ] - } -] ----- -==== - -#### `/apps/{name}` - -A `GET` request to `/apps/{name}` can be used to get instance data for all instances of a given -service. Below is a sample response when a `GET` request is made to `/apps/kubernetes`. - -==== -[source,json] ----- -[ - { - "instanceId":"1234", - "serviceId":"kubernetes", - "host":"172.18.0.3", - "port":6443, - "uri":"http://172.18.0.3:6443", - "secure":false, - "metadata":{ - "provider":"kubernetes", - "component":"apiserver", - "https":"6443" - }, - "namespace":"default", - "scheme":"http" - } -] ----- -==== - -#### `/app/{name}/{instanceid}` - -A `GET` request made to `/app/{name}/{instanceid}` will return the instance data for a specific -instance of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes/1234`. - -==== -[source,json] ----- - { - "instanceId":"1234", - "serviceId":"kubernetes", - "host":"172.18.0.3", - "port":6443, - "uri":"http://172.18.0.3:6443", - "secure":false, - "metadata":{ - "provider":"kubernetes", - "component":"apiserver", - "https":"6443" - }, - "namespace":"default", - "scheme":"http" - } ----- -==== - -### Deployment YAML - -An image of the Spring Cloud Discovery Server is hosted on https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-discoveryserver[Docker Hub]. -However, if you need to customize the discovery server behavior or prefer to build the image yourself you can easily build your own -image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver[source code on GitHub] and use that. - -Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes. - -==== -[source,yaml] ----- ---- -apiVersion: v1 -kind: List -items: - - apiVersion: v1 - kind: Service - metadata: - labels: - app: spring-cloud-kubernetes-discoveryserver - name: spring-cloud-kubernetes-discoveryserver - spec: - ports: - - name: http - port: 80 - targetPort: 8761 - selector: - app: spring-cloud-kubernetes-discoveryserver - type: ClusterIP - - apiVersion: v1 - kind: ServiceAccount - metadata: - labels: - app: spring-cloud-kubernetes-discoveryserver - name: spring-cloud-kubernetes-discoveryserver - - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - labels: - app: spring-cloud-kubernetes-discoveryserver - name: spring-cloud-kubernetes-discoveryserver:view - roleRef: - kind: Role - apiGroup: rbac.authorization.k8s.io - name: namespace-reader - subjects: - - kind: ServiceAccount - name: spring-cloud-kubernetes-discoveryserver - - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - namespace: default - name: namespace-reader - rules: - - apiGroups: ["", "extensions", "apps"] - resources: ["services", "endpoints"] - verbs: ["get", "list", "watch"] - - apiVersion: apps/v1 - kind: Deployment - metadata: - name: spring-cloud-kubernetes-discoveryserver-deployment - spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-discoveryserver - template: - metadata: - labels: - app: spring-cloud-kubernetes-discoveryserver - spec: - serviceAccount: spring-cloud-kubernetes-discoveryserver - containers: - - name: spring-cloud-kubernetes-discoveryserver - image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.0-SNAPSHOT - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8761 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8761 - path: /actuator/health/liveness - ports: - - containerPort: 8761 - - ----- -==== - -== Examples - -Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by -following the Spring Cloud interfaces. - -In your applications, you need to add the `spring-cloud-kubernetes-discovery` dependency to your classpath and remove any other dependency that contains a `DiscoveryClient` implementation (that is, a Eureka discovery client). -The same applies for `PropertySourceLocator`, where you need to add to the classpath the `spring-cloud-kubernetes-config` and remove any other dependency that contains a `PropertySourceLocator` implementation (that is, a configuration server client). - -The following projects highlight the usage of these dependencies and demonstrate how you can use these libraries from any Spring Boot application: - -* https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-examples[Spring Cloud Kubernetes Examples]: the ones located inside this repository. -* Spring Cloud Kubernetes Full Example: Minions and Boss - ** https://github.com/salaboy/spring-cloud-k8s-minion[Minion] - ** https://github.com/salaboy/spring-cloud-k8s-boss[Boss] -* Spring Cloud Kubernetes Full Example: https://github.com/salaboy/s1p_docs[SpringOne Platform Tickets Service] -* https://github.com/salaboy/s1p_gateway[Spring Cloud Gateway with Spring Cloud Kubernetes Discovery and Config] -* https://github.com/salaboy/showcase-admin-tool[Spring Boot Admin with Spring Cloud Kubernetes Discovery and Config] - -== Other Resources - -This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes. - -* https://salaboy.com/2018/09/27/the-s1p-experience/[S1P Spring Cloud on PKS] -* https://salaboy.com/2018/07/18/ljc-july-18-spring-cloud-docker-k8s/[Spring Cloud, Docker, Kubernetes -> London Java Community July 2018] - - -Please feel free to submit other resources through pull requests to https://github.com/spring-cloud/spring-cloud-kubernetes[this repository]. - -== Configuration properties - -To see the list of all Kubernetes related configuration properties please check link:appendix.html[the Appendix page]. - -== Building - -:jdkversion: 17 - -=== Basic Compile and Test - -To build the source you will need to install JDK {jdkversion}. - -Spring Cloud uses Maven for most build-related activities, and you -should be able to get off the ground quite quickly by cloning the -project you are interested in and typing - ----- -$ ./mvnw install ----- - -NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command -in place of `./mvnw` in the examples below. If you do that you also -might need to add `-P spring` if your local Maven settings do not -contain repository declarations for spring pre-release artifacts. - -NOTE: Be aware that you might need to increase the amount of memory -available to Maven by setting a `MAVEN_OPTS` environment variable with -a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in -the `.mvn` configuration, so if you find you have to do it to make a -build succeed, please raise a ticket to get the settings added to -source control. - -The projects that require middleware (i.e. Redis) for testing generally -require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running. - - -=== Documentation - -The spring-cloud-build module has a "docs" profile, and if you switch -that on it will try to build asciidoc sources from -`src/main/asciidoc`. As part of that process it will look for a -`README.adoc` and process it by loading all the includes, but not -parsing or rendering it, just copying it to `${main.basedir}` -(defaults to `${basedir}`, i.e. the root of the project). If there are -any changes in the README it will then show up after a Maven build as -a modified file in the correct place. Just commit it and push the change. - -=== Working with the code -If you don't have an IDE preference we would recommend that you use -https://www.springsource.com/developer/sts[Spring Tools Suite] or -https://eclipse.org[Eclipse] when working with the code. We use the -https://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools -should also work without issue as long as they use Maven 3.3.3 or better. - -==== Activate the Spring Maven profile -Spring Cloud projects require the 'spring' Maven profile to be activated to resolve -the spring milestone and snapshot repositories. Use your preferred IDE to set this -profile to be active, or you may experience build errors. - -==== Importing into eclipse with m2eclipse -We recommend the https://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with -eclipse. If you don't already have m2eclipse installed it is available from the "eclipse -marketplace". - -NOTE: Older versions of m2e do not support Maven 3.3, so once the -projects are imported into Eclipse you will also need to tell -m2eclipse to use the right profile for the projects. If you -see many different errors related to the POMs in the projects, check -that you have an up to date installation. If you can't upgrade m2e, -add the "spring" profile to your `settings.xml`. Alternatively you can -copy the repository settings from the "spring" profile of the parent -pom into your `settings.xml`. - -==== Importing into eclipse without m2eclipse -If you prefer not to use m2eclipse you can generate eclipse project metadata using the -following command: - -[indent=0] ----- - $ ./mvnw eclipse:eclipse ----- - -The generated eclipse projects can be imported by selecting `import existing projects` -from the `file` menu. - - -=== Building Docker Images On ARM64 - -If you run the Spring Cloud Kuberentes build on an ARM64 machine the docker images -used for the integration tests will fail to run due to using the wrong architecture. -This is because the Paketo build pack does not yet support ARM64. To work around this you -can run the build by passing `-Dspring-boot.build-image.builder=dashaun/builder:tiny` to Maven. - -For example: -``` -./mvnw clean install -Dspring-boot.build-image.builder=dashaun/builder:tiny -``` - - -== Contributing - -:spring-cloud-build-branch: master - -Spring Cloud is released under the non-restrictive Apache 2.0 license, -and follows a very standard Github development process, using Github -tracker for issues and merging pull requests into master. If you want -to contribute even something trivial please do not hesitate, but -follow the guidelines below. - -=== Sign the Contributor License Agreement -Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -Signing the contributor's agreement does not grant anyone commit rights to the main -repository, but it does mean that we can accept your contributions, and you will get an -author credit if we do. Active contributors might be asked to join the core team, and -given the ability to merge pull requests. - -=== Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of -conduct]. By participating, you are expected to uphold this code. Please report -unacceptable behavior to spring-code-of-conduct@pivotal.io. - -=== Code Conventions and Housekeeping -None of these is essential for a pull request, but they will all help. They can also be -added after the original pull request but before a merge. - -* Use the Spring Framework code format conventions. If you use Eclipse - you can import formatter settings using the - `eclipse-code-formatter.xml` file from the - https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring - Cloud Build] project. If using IntelliJ, you can use the - https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter - Plugin] to import the same file. -* Make sure all new `.java` files to have a simple Javadoc class comment with at least an - `@author` tag identifying you, and preferably at least a paragraph on what the class is - for. -* Add the ASF license header comment to all new `.java` files (copy from existing files - in the project) -* Add yourself as an `@author` to the .java files that you modify substantially (more - than cosmetic changes). -* Add some Javadocs and, if you change the namespace, some XSD doc elements. -* A few unit tests would help a lot as well -- someone has to do it. -* If no-one else is using your branch, please rebase it against the current master (or - other target branch in the main project). -* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], - if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit - message (where XXXX is the issue number). - -=== Checkstyle - -Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: - -.spring-cloud-build-tools/ ----- -└── src -    ├── checkstyle -    │   └── checkstyle-suppressions.xml <3> -    └── main -    └── resources -    ├── checkstyle-header.txt <2> -    └── checkstyle.xml <1> ----- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules - -==== Checkstyle configuration - -Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins. - -.pom.xml ----- - -true <1> - true - <2> - true - <3> - - - - - <4> - io.spring.javaformat - spring-javaformat-maven-plugin - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - ----- -<1> Fails the build upon Checkstyle errors -<2> Fails the build upon Checkstyle violations -<3> Checkstyle analyzes also the test sources -<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules -<5> Add checkstyle plugin to your build and reporting phases - -If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example: - -.projectRoot/src/checkstyle/checkstyle-suppresions.xml ----- - - - - - - ----- - -It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script: - -```bash -$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig -$ touch .springformat -``` - -=== IDE setup - -==== Intellij IDEA - -In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. -The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools[Spring Cloud Build] project. - -.spring-cloud-build-tools/ ----- -└── src -    ├── checkstyle -    │   └── checkstyle-suppressions.xml <3> -    └── main -    └── resources -    ├── checkstyle-header.txt <2> -    ├── checkstyle.xml <1> -    └── intellij -       ├── Intellij_Project_Defaults.xml <4> -       └── Intellij_Spring_Boot_Java_Conventions.xml <5> ----- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules -<4> Project defaults for Intellij that apply most of Checkstyle rules -<5> Project style conventions for Intellij that apply most of Checkstyle rules - -.Code style - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-code-style.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file. - -.Inspection profiles - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-inspections.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file. - -.Checkstyle - -To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-checkstyle.png[Checkstyle] - -Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables: - -- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL. -- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL. -- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`. - -IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources. - -=== Duplicate Finder - -Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. - -==== Duplicate Finder configuration - -Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`. - -.pom.xml -[source,xml] ----- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - ----- - -For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation]. - -You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build. - -If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project: - -[source,xml] ----- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - org.joda.time.base.BaseDateTime - .*module-info - - - changelog.txt - - - - - - - ----- - - -== AOT and native image support - -At this point, Spring Cloud Kubernetes does not support Spring Boot AOT transformations or native images. Partial support might be added in future releases. diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml new file mode 100644 index 0000000000..4841f7cf69 --- /dev/null +++ b/docs/antora-playbook.yml @@ -0,0 +1,39 @@ +antora: + extensions: + - '@springio/antora-extensions/partial-build-extension' + - require: '@springio/antora-extensions/latest-version-extension' + - require: '@springio/antora-extensions/inject-collector-cache-config-extension' + - '@antora/collector-extension' + - '@antora/atlas-extension' + - require: '@springio/antora-extensions/root-component-extension' + root_component_name: 'cloud-kubernetes' + - '@springio/antora-extensions/static-page-extension' +site: + title: Spring Cloud Kubernetes + url: https://docs.spring.io/spring-cloud-kubernetes/reference/ +content: + sources: + - url: ./.. + branches: HEAD + start_path: docs + worktrees: true +asciidoc: + attributes: + page-stackoverflow-url: https://stackoverflow.com/tags/spring-cloud + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + chomp: 'all' + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + sourcemap: true +urls: + latest_version_segment: '' +runtime: + log: + failure_level: warn + format: pretty +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.11/ui-bundle.zip diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 0000000000..8ac9d3a032 --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,12 @@ +name: cloud-kubernetes +version: true +title: Spring Cloud Kubernetes +nav: + - modules/ROOT/nav.adoc +ext: + collector: + run: + command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests + local: true + scan: + dir: ./target/classes/antora-resources/ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 0000000000..3c7b11303e --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1,24 @@ +* xref:index.adoc[Introduction] +* xref:getting-started.adoc[] +* xref:discovery-client.adoc[] +* xref:discovery-kubernetes-native.adoc[] +* xref:property-source-config.adoc[] +** xref:property-source-config/configmap-propertysource.adoc[] +** xref:property-source-config/secrets-propertysource.adoc[] +** xref:property-source-config/namespace-resolution.adoc[] +** xref:property-source-config/order_of_configMaps_and_secrets.adoc[] +** xref:property-source-config/propertysource-reload.adoc[] +** xref:property-source-config/namespace-label-filtering.adoc[] +* xref:kubernetes-awareness.adoc[] +* xref:pod-health-indicator.adoc[] +* xref:info-contributor.adoc[] +* xref:leader-election.adoc[] +* xref:load-balancer.adoc[] +* xref:security-service-accounts.adoc[] +* xref:service-registry.adoc[] +* xref:spring-cloud-kubernetes-configuration-watcher.adoc[] +* xref:spring-cloud-kubernetes-configserver.adoc[] +* xref:spring-cloud-kubernetes-discoveryserver.adoc[] +* xref:examples.adoc[] +* xref:other-resources.adoc[] +* xref:appendix.adoc[] diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/modules/ROOT/pages/_attributes.adoc similarity index 90% rename from docs/src/main/asciidoc/_attributes.adoc rename to docs/modules/ROOT/pages/_attributes.adoc index 31424366ba..bed0a077d8 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/modules/ROOT/pages/_attributes.adoc @@ -1,8 +1,6 @@ :doctype: book :idprefix: :idseparator: - -:toc: left -:toclevels: 4 :tabsize: 4 :numbered: :sectanchors: diff --git a/docs/src/main/asciidoc/appendix.adoc b/docs/modules/ROOT/pages/appendix.adoc similarity index 84% rename from docs/src/main/asciidoc/appendix.adoc rename to docs/modules/ROOT/pages/appendix.adoc index 39eb1593ef..c15db1d526 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/modules/ROOT/pages/appendix.adoc @@ -1,7 +1,8 @@ :numbered!: [appendix] [[common-application-properties]] -== Common application properties += Common application properties +:page-section-summary-toc: 1 include::_attributes.adoc[] @@ -11,4 +12,4 @@ This appendix provides a list of common {project-full-name} properties and refer NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. Also, you can define your own properties. -include::_configprops.adoc[] +include::partial$_configprops.adoc[] \ No newline at end of file diff --git a/docs/modules/ROOT/pages/configprops.adoc b/docs/modules/ROOT/pages/configprops.adoc new file mode 100644 index 0000000000..32cbb8e589 --- /dev/null +++ b/docs/modules/ROOT/pages/configprops.adoc @@ -0,0 +1,6 @@ +[[configuration-properties]] += Configuration Properties + +Below you can find a list of configuration properties. + +include::partial$_configprops.adoc[] diff --git a/docs/modules/ROOT/pages/discovery-client.adoc b/docs/modules/ROOT/pages/discovery-client.adoc new file mode 100644 index 0000000000..8383ff59cd --- /dev/null +++ b/docs/modules/ROOT/pages/discovery-client.adoc @@ -0,0 +1,295 @@ +[[discoveryclient-for-kubernetes]] += DiscoveryClient for Kubernetes + +This project provides an implementation of https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java[Discovery Client] +for https://kubernetes.io[Kubernetes]. +This client lets you query Kubernetes endpoints (see https://kubernetes.io/docs/user-guide/services/[services]) by name. +A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent `http` and `https` addresses and that a client can +access from a Spring Boot application running as a pod. + +DiscoveryClient can also find services of type `ExternalName` (see https://kubernetes.io/docs/concepts/services-networking/service/#externalname[ExternalName services]). At the moment, external name support type of services is only available if the following property `spring.cloud.kubernetes.discovery.include-external-name-services` is set to `true` (it is `false` by default). + +There are 3 types of discovery clients that we support: + +1. +==== +Fabric8 Kubernetes Client +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-fabric8 + +---- +==== + +2. +==== +Kubernetes Java Client +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-client + +---- +==== + +3. +==== +HTTP Based `DiscoveryClient` +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + +---- +==== + +NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the +<>. + +To enable loading of the `DiscoveryClient`, add `@EnableDiscoveryClient` to the according configuration or application class, as the following example shows: + +==== +[source,java] +---- +@SpringBootApplication +@EnableDiscoveryClient +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +---- +==== +Then you can inject the client in your code simply by autowiring it, as the following example shows: +==== +[source,java] +---- +@Autowired +private DiscoveryClient discoveryClient; +---- +==== + +The first question you should ask yourself is _where_ a `DiscoveryClient` supposed to discover services. In the kubernetes world, this means what namespace(s). There are 3 options here: + +- `selective namespaces`. For example: + +[source] +---- +spring.cloud.kubernetes.discovery.namespaces[0]=ns1 +spring.cloud.kubernetes.discovery.namespaces[1]=ns2 +---- + +Such a configuration makes discovery client only search for services in two namespaces `ns1` and `ns2`. + +- `all-namespaces`. + +==== +[source] +---- +spring.cloud.kubernetes.discovery.all-namespaces=true +---- +==== + +While such an option exists, this can be a burden on both kube-api and your application. It is rare to need such a setting. + +- `one namespace`. This is the default setting, if you do not specify any of the above. It works on the rules outlined in xref:property-source-config.adoc#namespace-resolution[Namespace Resolution]. + + +==== +NOTE: The above options work exactly as written for fabric8 and k8s clients. For the HTTP based client, you need to enable those options on the _server_. That can be achieved by setting them in `deployment.yaml` used to deploy the image in the cluster, using env variable(s). +==== + +For example: + +[source] +---- + containers: + - name: discovery-server + image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT + env: + - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0 + value: "namespace-a" +---- + +Once namespaces have been configured, the next question to answer is what services to discover. Think about it as what filter to apply. By default, no filtering is applied at all and all services are discovered. If you need to narrow what discovery client can find, you have two options: + +- Only take services that match certain service labels. This property is specified with: `spring.cloud.kubernetes.discovery.service-labels`. It accepts a `Map` and only those services that have such labels (as seen in `metadata.labels` in the service definition) will be taken into account. + +- The other option is to use https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression]. This is denoted by the `spring.cloud.kubernetes.discovery.filter` property, and its value depends on the client that you chose. If you use the fabric8 client, this SpEL expression must be created against `io.fabric8.kubernetes.api.model.Service` class. One such example could be: + +[source] +---- +spring.cloud.kubernetes.discovery.filter='#root.metadata.namespace matches "^.+A$"' +---- + +which tells discovery client to only get services that have the `metadata.namespace` that ends in upper case `A`. + +If your discovery client is based on k8s-native client, then the SpEL expression must be based on `io.kubernetes.client.openapi.models.V1Service` class. The same filter showed above would work here. + +If your discovery client is the http based one, then the SeEL expression has to be based on the same `io.kubernetes.client.openapi.models.V1Service` class, with the only distinction that this needs to be set as an env variable in the deployment yaml: + + +---- + containers: + - name: discovery-server + image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT + env: + - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_FILTER + value: '#root.metadata.namespace matches "^.+A$"' +---- + +It's now time to think what discovery client is supposed to return back. In general, there are two methods that `DiscoveryClient` has: `getServices` and `getInstances`. + +`getServices` will return the service _names_ as seen in the `metadata.name`. + + +NOTE: This method will return unique service names, even if there are duplicates across different namespaces (that you chose for the search). + + +`getInstances` returns a `List`. Besides the usual fields that a `ServiceInstance` has, we also add some data, like namespace or pod metadata (more explanation about these will follow in the document). Here is the data that we return at the moment: + +. `instanceId` - unique id of the service instance +. `serviceId` - the name of the service (it is the same as the one reported by calling `getServices`) +. `host` - IP of the instance (or name in case of the `ExternalName` type of service) +. `port` - port number of the instance. This requires a bit more explanation, as choosing the port number has its rules: + +.. service has no port defined, 0 (zero) will be returned. +.. service has a single port defined, that one will be returned. +.. If the service has a label `primary-port-name`, we will use the port number that has the name specified in the label's value. +.. If the above label is not present, then we will use the port name specified in `spring.cloud.kubernetes.discovery.primary-port-name` to find the port number. +.. If neither of the above are specified, we will use the port named `https` or `http` to compute the port number. +.. As a last resort we wil pick the first port in the list of ports. This last option may result in non-deterministic behaviour. + +. `uri` of the service instance + +. `scheme` either `http` or `https` (depending on the `secure` result) + +. `metadata` of the service: + +.. `labels` (if requested via `spring.cloud.kubernetes.discovery.metadata.add-labels=true`). Label keys can be "prefixed" with the value of `spring.cloud.kubernetes.discovery.metadata.labels-prefix` if it is set. + +.. `annotations` (if requested via `spring.cloud.kubernetes.discovery.metadata.add-annotations=true`). Annotations keys can be "prefixed" with the value of `spring.cloud.kubernetes.discovery.metadata.annotations-prefix` if it is set. + +.. `ports` (if requested via `spring.cloud.kubernetes.discovery.metadata.add-ports=true`). Port keys can be "prefixed" with the value of `spring.cloud.kubernetes.discovery.metadata.ports-prefix` if it is set. + +.. `k8s_namespace` with the value of the namespace where instance resides. + +.. `type` that holds the service type, for example `ClusterIP` or `ExternalName` + +. `secure` if the port that was discovered should be treated as secure. We will use the same rules outlined above to find the port name and number, and then: + +.. If this service has a label called `secured` with any of the values : `["true", "on", "yes", "1"]`, then treat the port that was found as secure. + +.. If such a label is not found, search for an annotation called `secured` and apply the same above rules. + +.. If this port number is part of `spring.cloud.kubernetes.discovery.known-secure-ports` (by default this value holds `[443, 8443]`), treat port number as secured. + +.. Last resort is to see if port name matches `https`; if it does treat this port as secured. + +. `namespace` - the namespace of the found instance. + +. `pod-metadata` labels and annotations of the service instance (pod), in the form of `Map>`. This support needs to be enabled via `spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true` and/or `spring.cloud.kubernetes.discovery.metadata.add-pod-annotaations=true` + +''' + + +To discover service endpoint addresses that are not marked as "ready" by the kubernetes api server, you can set the following property in `application.properties` (default: false): + +==== +[source] +---- +spring.cloud.kubernetes.discovery.include-not-ready-addresses=true +---- +NOTE: This might be useful when discovering services for monitoring purposes, and would enable inspecting the `/health` endpoint of not-ready service instances. +If you want to get the list of `ServiceInstance` to also include the `ExternalName` type services, you need to enable that support via: `spring.cloud.kubernetes.discovery.include-external-name-services=true`. As such, when calling `DiscoveryClient::getInstances` those will be returned also. You can distinguish between `ExternalName` and any other types by inspecting `ServiceInstance::getMetadata` and lookup for a field called `type`. This will be the type of the service returned : `ExternalName`/`ClusterIP`, etc. +If, for any reason, you need to disable the `DiscoveryClient`, you can set the following property in `application.properties`: +==== +[source] +---- +spring.main.cloud-platform=NONE +---- + +Note that the support of discovery client is _automatic_, depending on where you run the application. So the above setting might not be needed. + +Some Spring Cloud components use the `DiscoveryClient` in order to obtain information about the local service instance. For +this to work, you need to align the Kubernetes service name with the `spring.application.name` property. + +NOTE: `spring.application.name` has no effect as far as the name registered for the application within Kubernetes + +''' + +Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the `DiscoveryClient` implementation accordingly. In order to enable this functionality you need to add +`@EnableScheduling` on a configuration class in your application. By "watch", we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay` +milliseconds (by default it is `30000`). For the http discovery server this must be an environment variable set in deployment yaml: + +---- + containers: + - name: discovery-server + image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT + env: + - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY + value: 3000 +---- + +The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints +(for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event +should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract. +The endpoints will be queried in either : +- `all-namespaces` (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`) + +- `selective namespaces` (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example: + +- `one namespace` via xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken. + +NOTE: If, for any reasons, you want to disable catalog watcher, you need to set `spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false`. For the http discovery server, this needs to be an environment variable set in deployment for example: + +[source] +---- +SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCH_ENABLED=FALSE +---- + +The functionality of catalog watch works for all 3 discovery clients that we support, with some caveats that you need to be aware of in case of the http client. + +- The first is that this functionality is disabled by default, and it needs to be enabled in two places: + +* in discovery server via an environment variable in the deployment manifest, for example: ++ +---- +containers: + - name: discovery-server + image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT + env: + - name: SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED + value: "TRUE" +---- ++ + +* in discovery client, via a property in your `application.properties` for example: ++ +---- +spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true +---- ++ + +- The second point is that this is only supported since version `3.0.6` and upwards. +- Since http discovery has _two_ components : server and client, we strongly recommend to align versions between them, otherwise things might not work. +- If you decide to disable catalog watcher, you need to disable it in both server and client. + +By default, we use the `Endpoints`(see https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) API to find out the current state of services. There is another way though, via `EndpointSlices` (https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/). Such support can be enabled via a property: `spring.cloud.kubernetes.discovery.use-endpoint-slices=true` (by default it is `false`). Of course, your cluster has to support it also. As a matter of fact, if you enable this property, but your cluster does not support it, we will fail starting the application. If you decide to enable such support, you also need proper Role/ClusterRole set-up. For example: + +[source] +---- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: default + name: namespace-reader +rules: + - apiGroups: ["discovery.k8s.io"] + resources: ["endpointslices"] + verbs: ["get", "list", "watch"] +---- diff --git a/docs/src/main/asciidoc/discovery-kubernetes-native.adoc b/docs/modules/ROOT/pages/discovery-kubernetes-native.adoc similarity index 80% rename from docs/src/main/asciidoc/discovery-kubernetes-native.adoc rename to docs/modules/ROOT/pages/discovery-kubernetes-native.adoc index aa5e2f8dbe..53b54b4402 100644 --- a/docs/src/main/asciidoc/discovery-kubernetes-native.adoc +++ b/docs/modules/ROOT/pages/discovery-kubernetes-native.adoc @@ -1,9 +1,11 @@ -== Kubernetes native service discovery +[[kubernetes-native-service-discovery]] += Kubernetes native service discovery +:page-section-summary-toc: 1 Kubernetes itself is capable of (server side) service discovery (see: https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services). Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio (https://istio.io), a service mesh that is capable of load balancing, circuit breaker, failover, and much more. -The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring `RestTemplate` that refers to a fully qualified domain name (FQDN), such as `https://{service-name}.{namespace}.svc.{cluster}.local:{service-port}`. +The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring `RestTemplate` that refers to a fully qualified domain name (FQDN), such as `https://\{service-name}.\{namespace}.svc.\{cluster}.local:\{service-port}`. Additionally, you can use Hystrix for: diff --git a/docs/src/main/asciidoc/examples.adoc b/docs/modules/ROOT/pages/examples.adoc similarity index 96% rename from docs/src/main/asciidoc/examples.adoc rename to docs/modules/ROOT/pages/examples.adoc index 6754e0f049..ca0b11ec72 100644 --- a/docs/src/main/asciidoc/examples.adoc +++ b/docs/modules/ROOT/pages/examples.adoc @@ -1,4 +1,6 @@ -== Examples +[[examples]] += Examples +:page-section-summary-toc: 1 Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by following the Spring Cloud interfaces. diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/modules/ROOT/pages/getting-started.adoc similarity index 83% rename from docs/src/main/asciidoc/getting-started.adoc rename to docs/modules/ROOT/pages/getting-started.adoc index e62c0f5fbb..07ac817194 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/modules/ROOT/pages/getting-started.adoc @@ -1,4 +1,5 @@ -== Starters +[[starters]] += Starters Starters are convenient dependency descriptors you can include in your application. Include a starter to get the dependencies and Spring Boot @@ -7,6 +8,9 @@ provide implementations using the https://github.com/fabric8io/kubernetes-client Starters that begin with `spring-cloud-starter-kubernetes-client` provide implementations using the https://github.com/kubernetes-client/java[Kubernetes Java Client]. +NOTE: You CANNOT combine starters from Fabric8 and Kubernetes Java Clients. You must pick one library to +use and use the starters for that library only. + [cols="a,d"] |=== | Starter | Features @@ -49,8 +53,8 @@ resolves service names to Kubernetes Services. ---- | Load application properties from Kubernetes -<> and <>. -<> application properties when a ConfigMap or +xref:property-source-config/configmap-propertysource.adoc[ConfigMaps] and <>. +xref:property-source-config/propertysource-reload.adoc[Reload] application properties when a ConfigMap or Secret changes. | [source,xml] diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000000..815ded0e0f --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1 @@ +include::spring-cloud-kubernetes.adoc[Introduction] \ No newline at end of file diff --git a/docs/src/main/asciidoc/info-contributor.adoc b/docs/modules/ROOT/pages/info-contributor.adoc similarity index 66% rename from docs/src/main/asciidoc/info-contributor.adoc rename to docs/modules/ROOT/pages/info-contributor.adoc index 7f2057e1dc..abef6b1ea5 100644 --- a/docs/src/main/asciidoc/info-contributor.adoc +++ b/docs/modules/ROOT/pages/info-contributor.adoc @@ -1,7 +1,9 @@ -== Info Contributor +[[info-contributor]] += Info Contributor +:page-section-summary-toc: 1 Spring Cloud Kubernetes includes an `InfoContributor` which adds Pod information to -Spring Boot's `/info` Acturator endpoint. +Spring Boot's `/info` Actuator endpoint. You can disable this `InfoContributor` by setting `management.info.kubernetes.enabled` to `false` in `application.[properties | yaml]`. diff --git a/docs/src/main/asciidoc/kubernetes-awareness.adoc b/docs/modules/ROOT/pages/kubernetes-awareness.adoc similarity index 83% rename from docs/src/main/asciidoc/kubernetes-awareness.adoc rename to docs/modules/ROOT/pages/kubernetes-awareness.adoc index d2687e77bf..41793a5a82 100644 --- a/docs/src/main/asciidoc/kubernetes-awareness.adoc +++ b/docs/modules/ROOT/pages/kubernetes-awareness.adoc @@ -1,4 +1,5 @@ -== Kubernetes Ecosystem Awareness +[[kubernetes-ecosystem-awareness]] += Kubernetes Ecosystem Awareness All features described earlier in this guide work equally well, regardless of whether your application is running inside Kubernetes. This is really helpful for development and troubleshooting. @@ -20,19 +21,35 @@ you will have to set `spring.main.cloud-platform` should be set in `bootstrap.{p (or the profile specific one). Also note that these properties: `spring.cloud.kubernetes.config.enabled` and `spring.cloud.kubernetes.secrets.enabled` will only take effect when set in `bootstrap.{properties|yml}` when you have `spring-cloud-starter-bootstrap` on your classpath or are setting `spring.cloud.bootstrap.enabled=true`. -=== Breaking Changes In 3.0.x +[[breaking-changes-in-3-0-x]] +== Breaking Changes In 3.0.x In versions of Spring Cloud Kubernetes prior to `3.0.x`, Kubernetes awareness was implemented using `spring.cloud.kubernetes.enabled` property. This property was removed and is un-supported. Instead, we use Spring Boot API: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnCloudPlatform.html[ConditionalOnCloudPlatform]. If it is needed to explicitly enable or disable this awareness, use `spring.main.cloud-platform=NONE/KUBERNETES`. -=== Kubernetes Profile Autoconfiguration +- Another breaking change is the additional `list` verb needed for loading configmaps/secrets. For example: + +``` +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-role +rules: + - apiGroups: ["", "extensions", "apps", "discovery.k8s.io"] + resources: ["configmaps", "pods", "services", "endpoints", "secrets", "endpointslices"] + verbs: ["get", "list", "watch"] +``` + +[[kubernetes-profile-autoconfiguration]] +== Kubernetes Profile Autoconfiguration When the application runs as a pod inside Kubernetes, a Spring profile named `kubernetes` automatically gets activated. This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed within the Kubernetes platform (for example, different development and production configuration). -=== Istio Awareness +[[istio-awareness]] +== Istio Awareness When you include the `spring-cloud-kubernetes-fabric8-istio` module in the application classpath, a new profile is added to the application, provided the application is running inside a Kubernetes Cluster with https://istio.io[Istio] installed. You can then use diff --git a/docs/src/main/asciidoc/leader-election.adoc b/docs/modules/ROOT/pages/leader-election.adoc similarity index 96% rename from docs/src/main/asciidoc/leader-election.adoc rename to docs/modules/ROOT/pages/leader-election.adoc index 0a03a76429..bc0d920a77 100644 --- a/docs/src/main/asciidoc/leader-election.adoc +++ b/docs/modules/ROOT/pages/leader-election.adoc @@ -1,4 +1,6 @@ -== Leader Election +[[leader-election]] += Leader Election + The Spring Cloud Kubernetes leader election mechanism implements the leader election API of Spring Integration using a Kubernetes ConfigMap. Multiple application instances compete for leadership, but leadership will only be granted to one. @@ -9,7 +11,6 @@ When leadership removal occurs, the previous leader receives `OnRevokedEvent` ap After removal, any instances in the cluster may become the new leader, including the old leader. To include it in your project, add the following dependency. -==== Fabric8 Leader Implementation [source,xml] ---- @@ -18,12 +19,9 @@ Fabric8 Leader Implementation spring-cloud-kubernetes-fabric8-leader ---- -==== To specify the name of the configmap used for leader election use the following property. -==== [source,properties] ---- spring.cloud.kubernetes.leader.config-map-name=leader ---- -==== diff --git a/docs/src/main/asciidoc/load-balancer.adoc b/docs/modules/ROOT/pages/load-balancer.adoc similarity index 95% rename from docs/src/main/asciidoc/load-balancer.adoc rename to docs/modules/ROOT/pages/load-balancer.adoc index 037b45b3bc..ea1e2ea55a 100644 --- a/docs/src/main/asciidoc/load-balancer.adoc +++ b/docs/modules/ROOT/pages/load-balancer.adoc @@ -1,7 +1,8 @@ -== LoadBalancer for Kubernetes +[[loadbalancer-for-kubernetes]] += LoadBalancer for Kubernetes + This project includes Spring Cloud Load Balancer for load balancing based on Kubernetes Endpoints and provides implementation of load balancer based on Kubernetes Service. To include it to your project add the following dependency. -==== Fabric8 Implementation [source,xml] ---- @@ -10,9 +11,7 @@ Fabric8 Implementation spring-cloud-starter-kubernetes-fabric8-loadbalancer ---- -==== -==== Kubernetes Java Client Implementation [source,xml] ---- @@ -21,22 +20,17 @@ Kubernetes Java Client Implementation spring-cloud-starter-kubernetes-client-loadbalancer ---- -==== To enable load balancing based on Kubernetes Service name use the following property. Then load balancer would try to call application using address, for example `service-a.default.svc.cluster.local` -==== [source] ---- spring.cloud.kubernetes.loadbalancer.mode=SERVICE ---- -==== To enabled load balancing across all namespaces use the following property. Property from `spring-cloud-kubernetes-discovery` module is respected. -==== [source] ---- spring.cloud.kubernetes.discovery.all-namespaces=true ---- -==== If a service needs to be accessed over HTTPS you need to add a label or annotation to your service definition with the name `secured` and the value `true` and the load balancer will then use HTTPS to make requests to the service. diff --git a/docs/src/main/asciidoc/other-resources.adoc b/docs/modules/ROOT/pages/other-resources.adoc similarity index 87% rename from docs/src/main/asciidoc/other-resources.adoc rename to docs/modules/ROOT/pages/other-resources.adoc index 542cdaf07d..e81bcc02e7 100644 --- a/docs/src/main/asciidoc/other-resources.adoc +++ b/docs/modules/ROOT/pages/other-resources.adoc @@ -1,4 +1,6 @@ -== Other Resources +[[other-resources]] += Other Resources +:page-section-summary-toc: 1 This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes. diff --git a/docs/src/main/asciidoc/pod-health-indicator.adoc b/docs/modules/ROOT/pages/pod-health-indicator.adoc similarity index 92% rename from docs/src/main/asciidoc/pod-health-indicator.adoc rename to docs/modules/ROOT/pages/pod-health-indicator.adoc index 3c59f2e34e..80b812842d 100644 --- a/docs/src/main/asciidoc/pod-health-indicator.adoc +++ b/docs/modules/ROOT/pages/pod-health-indicator.adoc @@ -1,4 +1,6 @@ -== Pod Health Indicator +[[pod-health-indicator]] += Pod Health Indicator +:page-section-summary-toc: 1 Spring Boot uses https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java[`HealthIndicator`] to expose info about the health of an application. That makes it really useful for exposing health-related information to the user and makes it a good fit for use as https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/[readiness probes]. diff --git a/docs/modules/ROOT/pages/property-source-config.adoc b/docs/modules/ROOT/pages/property-source-config.adoc new file mode 100644 index 0000000000..bd35b685f5 --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config.adoc @@ -0,0 +1,18 @@ +[[kubernetes-propertysource-implementations]] += Kubernetes PropertySource implementations +:page-section-summary-toc: 1 + +The most common approach to configuring your Spring Boot application is to create an `application.properties` or `application.yaml` or +an `application-profile.properties` or `application-profile.yaml` file that contains key-value pairs that provide customization values to your +application or Spring Boot starters. You can override these properties by specifying system properties or environment +variables. + +To enable this functionality you need to set the `spring.config.import` application configuration property to `kubernetes:` (escape with quotes when using yaml eg. `"kubernetes:"`). +Currently you can not specify a ConfigMap or Secret to load using `spring.config.import`, by default Spring Cloud Kubernetes +will load a ConfigMap and/or Secret based on the `spring.application.name` property. If `spring.application.name` is not set it will +load a ConfigMap and/or Secret with the name `application`. + +If you would like to load Kubernetes ``PropertySource``s during the bootstrap phase like it worked prior to the 3.0.x release +you can either add `spring-cloud-starter-bootstrap` to your application's classpath or set `spring.cloud.bootstrap.enabled=true` +as an environment variable. + diff --git a/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc b/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc new file mode 100644 index 0000000000..64727ccd26 --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc @@ -0,0 +1,605 @@ +[[configmap-propertysource]] += Using a `ConfigMap` `PropertySource` + +Kubernetes provides a resource named https://kubernetes.io/docs/user-guide/configmap/[`ConfigMap`] to externalize the +parameters to pass to your application in the form of key-value pairs or embedded `application.properties` or `application.yaml` files. +The link:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-fabric8-config[Spring Cloud Kubernetes Config] project makes Kubernetes `ConfigMap` instances available +during application startup and triggers hot reloading of beans or Spring context when changes are detected on +observed `ConfigMap` instances. + +Everything that follows is explained mainly referring to examples using ConfigMaps, but the same stands for +Secrets, i.e.: every feature is supported for both. + +The default behavior is to create a `Fabric8ConfigMapPropertySource` (or a `KubernetesClientConfigMapPropertySource`) based on a Kubernetes `ConfigMap` that has `metadata.name` of either: + +- value of `spring.cloud.kubernetes.config.name` +- value of your Spring application (as defined by `spring.application.name` property) +- the String literal `"application"` + +However, more advanced configuration is possible where you can use multiple `ConfigMap` instances. +The `spring.cloud.kubernetes.config.sources` list makes this possible. +For example, you could define the following `ConfigMap` instances: + +[source,yaml] +---- +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + config: + name: default-name + namespace: default-namespace + sources: + # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace + - name: c1 + # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2 + - namespace: n2 + # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3 + - namespace: n3 + name: c3 +---- + +In the preceding example, if `spring.cloud.kubernetes.config.namespace` had not been set, +the `ConfigMap` named `c1` would be looked up in the namespace that the application runs. +See xref:property-source-config/namespace-resolution.adoc[Namespace resolution] to get a better understanding of how the namespace +of the application is resolved. + + +Any matching `ConfigMap` that is found is processed as follows: + +* Apply individual configuration properties. +* Apply as `yaml` (or `properties`) the content of any property that is named by the value of `spring.application.name` + (if it's not present, by `application.yaml/properties`) +* Apply as a properties file the content of the above name + each active profile. + +An example should make a lot more sense. Let's suppose that `spring.application.name=my-app` and that +we have a single active profile called `k8s`. For a configuration as below: + + +[source] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: my-app +data: + my-app.yaml: |- + ... + my-app-k8s.yaml: |- + .. + my-app-dev.yaml: |- + .. + not-my-app.yaml: |- + .. + someProp: someValue +---- + +This is what we will end-up loading: + + - `my-app.yaml` treated as a file + - `my-app-k8s.yaml` treated as a file + - `my-app-dev.yaml` _ignored_, since `dev` is _not_ an active profile + - `not-my-app.yaml` _ignored_, since it does not match `spring.application.name` + - `someProp: someValue` plain property + +The order of loading properties is a as follows: + +- first load all properties from `my-app.yaml` +- then all from profile-based sources: `my-app-k8s.yaml` +- then all plain properties `someProp: someValue` + +This means that profile based sources take precedence over non-profile based sources (just like in a vanilla Spring app); and plain properties take precedence over both profile and non-profile based sources. Here is an example: + +==== +[source] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: my-app +data: + my-app-k8s.yaml: |- + key1=valueA + key2=valueB + my-app.yaml: |- + key1=valueC + key2=valueA + key1: valueD +---- +==== + +After processing such a ConfigMap, this is what you will get in the properties: `key1=valueD`, `key2=valueB`. + +The single exception to the aforementioned flow is when the `ConfigMap` contains a *single* key that indicates +the file is a YAML or properties file. In that case, the name of the key does NOT have to be `application.yaml` or +`application.properties` (it can be anything) and the value of the property is treated correctly. +This features facilitates the use case where the `ConfigMap` was created by using something like the following: + +[source] +---- +kubectl create configmap game-config --from-file=/path/to/app-config.yaml +---- + +Assume that we have a Spring Boot application named `demo` that uses the following properties to read its thread pool +configuration. + +* `pool.size.core` +* `pool.size.maximum` + +This can be externalized to config map in `yaml` format as follows: + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + pool.size.core: 1 + pool.size.max: 16 +---- + +Individual properties work fine for most cases. However, sometimes, embedded `yaml` is more convenient. In this case, we +use a single property named `application.yaml` to embed our `yaml`, as follows: + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yaml: |- + pool: + size: + core: 1 + max:16 +---- + +The following example also works: + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + custom-name.yaml: |- + pool: + size: + core: 1 + max:16 +---- + +You can also define the search to happen based on labels, for example: + + +[source,yaml] +---- +spring: + application: + name: labeled-configmap-with-prefix + cloud: + kubernetes: + config: + enableApi: true + useNameAsPrefix: true + namespace: spring-k8s + sources: + - labels: + letter: a +---- + +This will search for every configmap in namespace `spring-k8s` that has labels `{letter : a}`. The important +thing to notice here is that unlike reading a configmap by name, this can result in _multiple_ config maps read. +As usual, the same feature is supported for secrets. + +You can also configure Spring Boot applications differently depending on active profiles that are merged together +when the `ConfigMap` is read. You can provide different property values for different profiles by using an +`application.properties` or `application.yaml` property, specifying profile-specific values, each in their own document +(indicated by the `---` sequence), as follows: + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye + --- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers + --- + spring: + profiles: production + greeting: + message: Say Hello to the Ops +---- + +In the preceding case, the configuration loaded into your Spring Application with the `development` profile is as follows: + +[source,yaml] +---- + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers +---- + +However, if the `production` profile is active, the configuration becomes: + +[source,yaml] +---- + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye +---- + +If both profiles are active, the property that appears last within the `ConfigMap` overwrites any preceding values. + +Another option is to create a different config map per profile and spring boot will automatically fetch it based +on active profiles + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye +---- +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-development +data: + application.yml: |- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers +---- +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-production +data: + application.yml: |- + spring: + profiles: production + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye +---- + + +To tell Spring Boot which `profile` should be enabled see the https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles[Spring Boot documentation]. +One option for activating a specific profile when deploying to Kubernetes is to launch your Spring Boot application with an environment variable that you can define in the PodSpec at the container specification. + Deployment resource file, as follows: + +[source,yaml] +---- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-name + labels: + app: deployment-name +spec: + replicas: 1 + selector: + matchLabels: + app: deployment-name + template: + metadata: + labels: + app: deployment-name + spec: + containers: + - name: container-name + image: your-image + env: + - name: SPRING_PROFILES_ACTIVE + value: "development" +---- + +You could run into a situation where there are multiple configs maps that have the same property names. For example: + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-one +data: + application.yml: |- + greeting: + message: Say Hello from one +---- + +and + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-two +data: + application.yml: |- + greeting: + message: Say Hello from two +---- + +Depending on the order in which you place these in `bootstrap.yaml|properties`, you might end up with an un-expected result (the last config map wins). For example: + +[source,yaml] +---- +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + config: + namespace: default-namespace + sources: + - name: config-map-two + - name: config-map-one +---- + +will result in property `greetings.message` being `Say Hello from one`. + +There is a way to change this default configuration by specifying `useNameAsPrefix`. For example: + +[source,yaml] +---- +spring: + application: + name: with-prefix + cloud: + kubernetes: + config: + useNameAsPrefix: true + namespace: default-namespace + sources: + - name: config-map-one + useNameAsPrefix: false + - name: config-map-two +---- + +Such a configuration will result in two properties being generated: + + - `greetings.message` equal to `Say Hello from one`. + + - `config-map-two.greetings.message` equal to `Say Hello from two` + +Notice that `spring.cloud.kubernetes.config.useNameAsPrefix` has a _lower_ priority than `spring.cloud.kubernetes.config.sources.useNameAsPrefix`. +This allows you to set a "default" strategy for all sources, at the same time allowing to override only a few. + +If using the config map name is not an option, you can specify a different strategy, called : `explicitPrefix`. Since this is an _explicit_ prefix that +you select, it can only be supplied to the `sources` level. At the same time it has a higher priority than `useNameAsPrefix`. Let's suppose we have a third config map with these entries: + + +[source,yaml] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-three +data: + application.yml: |- + greeting: + message: Say Hello from three +---- + +A configuration like the one below: + +[source,yaml] +---- +spring: + application: + name: with-prefix + cloud: + kubernetes: + config: + useNameAsPrefix: true + namespace: default-namespace + sources: + - name: config-map-one + useNameAsPrefix: false + - name: config-map-two + explicitPrefix: two + - name: config-map-three +---- + +will result in three properties being generated: + + - `greetings.message` equal to `Say Hello from one`. + + - `two.greetings.message` equal to `Say Hello from two`. + + - `config-map-three.greetings.message` equal to `Say Hello from three`. + +The same way you configure a prefix for configmaps, you can do it for secrets also; both for secrets that are based on name +and the ones based on labels. For example: + +[source.yaml] +---- +spring: + application: + name: prefix-based-secrets + cloud: + kubernetes: + secrets: + enableApi: true + useNameAsPrefix: true + namespace: spring-k8s + sources: + - labels: + letter: a + useNameAsPrefix: false + - labels: + letter: b + explicitPrefix: two + - labels: + letter: c + - labels: + letter: d + useNameAsPrefix: true + - name: my-secret +---- + +The same processing rules apply when generating property source as for config maps. The only difference is that +potentially, looking up secrets by labels can mean that we find more than one source. In such a case, prefix (if specified via `useNameAsPrefix`) +will be the names of all secrets found for those particular labels. + +One more thing to bear in mind is that we support `prefix` per _source_, not per secret. The easiest way to explain this is via an example: + +[source.yaml] +---- +spring: + application: + name: prefix-based-secrets + cloud: + kubernetes: + secrets: + enableApi: true + useNameAsPrefix: true + namespace: spring-k8s + sources: + - labels: + color: blue + useNameAsPrefix: true +---- + +Suppose that a query matching such a label will provide two secrets as a result: `secret-a` and `secret-b`. +Both of these secrets have the same property name: `color=sea-blue` and `color=ocean-blue`. It is undefined which +`color` will end-up as part of property sources, but the prefix for it will be `secret-a.secret-b` +(concatenated sorted naturally, names of the secrets). + +If you need more fine-grained results, adding more labels to identify the secret uniquely would be an option. + + + +By default, besides reading the config map that is specified in the `sources` configuration, Spring will also try to read +all properties from "profile aware" sources. The easiest way to explain this is via an example. Let's suppose your application +enables a profile called "dev" and you have a configuration like the one below: + +[source,yaml] +---- +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + namespace: default-namespace + sources: + - name: config-map-one +---- + +Besides reading the `config-map-one`, Spring will also try to read `config-map-one-dev`; in this particular order. Each active profile +generates such a profile aware config map. + +Though your application should not be impacted by such a config map, it can be disabled if needed: + +[source,yaml] +---- +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + includeProfileSpecificSources: false + namespace: default-namespace + sources: + - name: config-map-one + includeProfileSpecificSources: false +---- + +Notice that just like before, there are two levels where you can specify this property: for all config maps or +for individual ones; the latter having a higher priority. + +NOTE: You should check the security configuration section. To access config maps from inside a pod you need to have the correct +Kubernetes service accounts, roles and role bindings. + +Another option for using `ConfigMap` instances is to mount them into the Pod by running the Spring Cloud Kubernetes application +and having Spring Cloud Kubernetes read them from the file system. + +NOTE: This feature is deprecated and will be removed in a future release (Use `spring.config.import` instead). +This behavior is controlled by the `spring.cloud.kubernetes.config.paths` property. You can use it in +addition to or instead of the mechanism described earlier. +`spring.cloud.kubernetes.config.paths` expects a List of full paths to each property file, because directories are not being recursively parsed. For example: + +``` +spring: + cloud: + kubernetes: + config: + paths: + - /tmp/application.properties + - /var/application.yaml +``` + +NOTE: If you use `spring.cloud.kubernetes.config.paths` or `spring.cloud.kubernetes.secrets.path` the automatic reload +functionality will not work. You will need to make a `POST` request to the `/actuator/refresh` endpoint or +restart/redeploy the application. + +[#config-map-fail-fast] +In some cases, your application may be unable to load some of your `ConfigMaps` using the Kubernetes API. +If you want your application to fail the start-up process in such cases, you can set +`spring.cloud.kubernetes.config.fail-fast=true` to make the application start-up fail with an Exception. + +[#config-map-retry] +You can also make your application retry loading `ConfigMap` property sources on a failure. First, you need to +set `spring.cloud.kubernetes.config.fail-fast=true`. Then you need to add `spring-retry` +and `spring-boot-starter-aop` to your classpath. You can configure retry properties such as +the maximum number of attempts, backoff options like initial interval, multiplier, max interval by setting the +`spring.cloud.kubernetes.config.retry.*` properties. + +NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason +and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `ConfigMap` `PropertySources` +by setting `spring.cloud.kubernetes.config.retry.enabled=false`. + +.Properties: +[options="header,footer"] +|=== +| Name | Type | Default | Description +| `spring.cloud.kubernetes.config.enabled` | `Boolean` | `true` | Enable ConfigMaps `PropertySource` +| `spring.cloud.kubernetes.config.name` | `String` | `${spring.application.name}` | Sets the name of `ConfigMap` to look up +| `spring.cloud.kubernetes.config.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to lookup +| `spring.cloud.kubernetes.config.paths` | `List` | `null` | Sets the paths where `ConfigMap` instances are mounted +| `spring.cloud.kubernetes.config.enableApi` | `Boolean` | `true` | Enable or disable consuming `ConfigMap` instances through APIs +| `spring.cloud.kubernetes.config.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `ConfigMap` +| `spring.cloud.kubernetes.config.retry.enabled` | `Boolean` | `true` | Enable or disable config retry. +| `spring.cloud.kubernetes.config.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. +| `spring.cloud.kubernetes.config.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. +| `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. +| `spring.cloud.kubernetes.config.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. +|=== + diff --git a/docs/modules/ROOT/pages/property-source-config/namespace-label-filtering.adoc b/docs/modules/ROOT/pages/property-source-config/namespace-label-filtering.adoc new file mode 100644 index 0000000000..f79f2ed1e5 --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/namespace-label-filtering.adoc @@ -0,0 +1,66 @@ +[[namespace-label-filtering]] += Reload namespace and label filtering + +By default, a namespace chosen using the steps outlined in xref:property-source-config/namespace-resolution.adoc[Namespace resolution] will be used to listen to changes +in configmaps and secrets. i.e.: if you do not tell reload what namespaces and configmaps/secrets to watch for, +it will watch all configmaps/secrets from the namespace that will be computed using the above algorithm. + +On the other hand, you can define a more fine-grained approach. For example, you can specify the namespaces where +changes will be monitored: + +[source,yaml] +---- +spring: + application: + name: event-reload + cloud: + kubernetes: + reload: + enabled: true + strategy: shutdown + mode: event + namespaces: + - my-namespace +---- + +Such a configuration will make the app watch changes only in the `my-namespace` namespace. Mind that this will +watch _all_ configmaps/secrets (depending on which one you enable). If you want an even more fine-grained approach, +you can enable "label-filtering". First we need to enable such support via : `enable-reload-filtering: true` + +[source,yaml] +---- +spring: + application: + name: event-reload + cloud: + kubernetes: + reload: + enabled: true + strategy: shutdown + mode: event + namespaces: + - my-namespaces + monitoring-config-maps: true + enable-reload-filtering: true +---- + +What this will do, is watch configmaps/secrets that only have the `spring.cloud.kubernetes.config.informer.enabled: true` label. + +.Properties: +[options="header,footer"] +|=== +| Name | Type | Default | Description +| `spring.cloud.kubernetes.reload.enabled` | `Boolean` | `false` | Enables monitoring of property sources and configuration reload +| `spring.cloud.kubernetes.reload.monitoring-config-maps` | `Boolean` | `true` | Allow monitoring changes in config maps +| `spring.cloud.kubernetes.reload.monitoring-secrets` | `Boolean` | `false` | Allow monitoring changes in secrets +| `spring.cloud.kubernetes.reload.strategy` | `Enum` | `refresh` | The strategy to use when firing a reload (`refresh`, `restart_context`, or `shutdown`) +| `spring.cloud.kubernetes.reload.mode` | `Enum` | `event` | Specifies how to listen for changes in property sources (`event` or `polling`) +| `spring.cloud.kubernetes.reload.period` | `Duration`| `15s` | The period for verifying changes when using the `polling` strategy +| `spring.cloud.kubernetes.reload.namespaces` | `String[]`| | namespaces where we should watch for changes +| `spring.cloud.kubernetes.reload.enable-reload-filtering` | `String` | | enabled labeled filtering for reload functionality +|=== + +Notes: + +* You should not use properties under `spring.cloud.kubernetes.reload` in config maps or secrets. Changing such properties at runtime may lead to unexpected results. +* Deleting a property or the whole config map does not restore the original state of the beans when you use the `refresh` level. diff --git a/docs/modules/ROOT/pages/property-source-config/namespace-resolution.adoc b/docs/modules/ROOT/pages/property-source-config/namespace-resolution.adoc new file mode 100644 index 0000000000..c594a6372a --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/namespace-resolution.adoc @@ -0,0 +1,38 @@ +[[namespace-resolution]] += Namespace resolution + +Finding an application namespace happens on a best-effort basis. There are some steps that we iterate in order +to find it. The easiest and most common one, is to specify it in the proper configuration, for example: + +[source,yaml] +---- +spring: + application: + name: app + cloud: + kubernetes: + secrets: + name: secret + namespace: default + sources: + # Spring Cloud Kubernetes looks up a Secret named 'a' in namespace 'default' + - name: a + # Spring Cloud Kubernetes looks up a Secret named 'secret' in namespace 'b' + - namespace: b + # Spring Cloud Kubernetes looks up a Secret named 'd' in namespace 'c' + - namespace: c + name: d +---- + +Remember that the same can be done for config maps. If such a namespace is not specified, it will be read (in this order): + +1. from property `spring.cloud.kubernetes.client.namespace` +2. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath` property +3. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file +(kubernetes default namespace path) +4. from a designated client method call (for example fabric8's : `KubernetesClient::getNamespace`), if the client provides +such a method. This, in turn, could be configured via environment properties. For example fabric8 client can be configured via +"KUBERNETES_NAMESPACE" property; consult the client documentation for exact details. + +Failure to find a namespace from the above steps will result in an Exception being raised. + diff --git a/docs/modules/ROOT/pages/property-source-config/order_of_configMaps_and_secrets.adoc b/docs/modules/ROOT/pages/property-source-config/order_of_configMaps_and_secrets.adoc new file mode 100644 index 0000000000..6ce8b392f9 --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/order_of_configMaps_and_secrets.adoc @@ -0,0 +1,6 @@ +[[order_of_configMaps_and_secrets]] += Order of ConfigMaps and Secrets +:page-section-summary-toc: 1 + +If, for whatever reason, you enabled both configmaps and secrets, and there is a common property between them, the value from the ConfigMap will have a higher precedence. That is: it will override whatever values are found in secrets. + diff --git a/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc b/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc new file mode 100644 index 0000000000..d656135def --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc @@ -0,0 +1,101 @@ +[[propertysource-reload]] += `PropertySource` Reload + +WARNING: This functionality has been deprecated in the 2020.0 release. Please see +the xref:spring-cloud-kubernetes-configuration-watcher.adoc#spring-cloud-kubernetes-configuration-watcher[null] controller for an alternative way +to achieve the same functionality. + +Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. +The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or +`Secret` changes. + +By default, this feature is disabled. You can enable it by using the `spring.cloud.kubernetes.reload.enabled=true` configuration property (for example, in the `application.properties` file). +Please notice that this will enable monitoring of configmaps only (i.e.: `spring.cloud.kubernetes.reload.monitoring-config-maps` will be set to `true`). +If you want to enable monitoring of secrets, this must be done explicitly via : `spring.cloud.kubernetes.reload.monitoring-secrets=true`. + +The following levels of reload are supported (by setting the `spring.cloud.kubernetes.reload.strategy` property): + +* `refresh` (default): Only configuration beans annotated with `@ConfigurationProperties` or `@RefreshScope` are reloaded. +This reload level leverages the refresh feature of Spring Cloud Context. + +* `restart_context`: the whole Spring `ApplicationContext` is gracefully restarted. Beans are recreated with the new configuration. +In order for the restart context functionality to work properly you must enable and expose the restart actuator endpoint +[source,yaml] +==== +---- +management: + endpoint: + restart: + enabled: true + endpoints: + web: + exposure: + include: restart +---- +==== + +* `shutdown`: the Spring `ApplicationContext` is shut down to activate a restart of the container. + When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the `ApplicationContext` +and that a replication controller or replica set is configured to restart the pod. + +Assuming that the reload feature is enabled with default settings (`refresh` mode), the following bean is refreshed when the config map changes: + +[java, source] +---- +@Configuration +@ConfigurationProperties(prefix = "bean") +public class MyConfig { + + private String message = "a message that can be changed live"; + + // getter and setters + +} +---- + +To see that changes effectively happen, you can create another bean that prints the message periodically, as follows + +[source,java] +---- +@Component +public class MyBean { + + @Autowired + private MyConfig config; + + @Scheduled(fixedDelay = 5000) + public void hello() { + System.out.println("The message is: " + config.getMessage()); + } +} +---- + +You can change the message printed by the application by using a `ConfigMap`, as follows: + +[source,yaml] +---- +apiVersion: v1 +kind: ConfigMap +metadata: + name: reload-example +data: + application.properties: |- + bean.message=Hello World! +---- + +Any change to the property named `bean.message` in the `ConfigMap` associated with the pod is reflected in the +output. More generally speaking, changes associated to properties prefixed with the value defined by the `prefix` +field of the `@ConfigurationProperties` annotation are detected and reflected in the application. +xref:property-source-config/configmap-propertysource.adoc[Associating a `ConfigMap` with a pod] is explained earlier in this chapter. + +The reload feature supports two operating modes: + +* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). +Any event produces a re-check on the configuration and, in case of changes, a reload. +The `view` role on the service account is required in order to listen for config map changes. A higher level role (such as `edit`) is required for secrets +(by default, secrets are not monitored). +* Polling: Periodically re-creates the configuration from config maps and secrets to see if it has changed. +You can configure the polling period by using the `spring.cloud.kubernetes.reload.period` property and defaults to 15 seconds. +It requires the same role as the monitored property source. +This means, for example, that using polling on file-mounted secret sources does not require particular privileges. + diff --git a/docs/modules/ROOT/pages/property-source-config/secrets-propertysource.adoc b/docs/modules/ROOT/pages/property-source-config/secrets-propertysource.adoc new file mode 100644 index 0000000000..610e6cc849 --- /dev/null +++ b/docs/modules/ROOT/pages/property-source-config/secrets-propertysource.adoc @@ -0,0 +1,188 @@ +[[secrets-propertysource]] += Secrets PropertySource + +Kubernetes has the notion of https://kubernetes.io/docs/concepts/configuration/secret/[Secrets] for storing +sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with `Secrets` to make secrets +accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the `spring.cloud.kubernetes.secrets.enabled` property. + +When enabled, the `Fabric8SecretsPropertySource` looks up Kubernetes for `Secrets` from the following sources: + +. Reading recursively from secrets mounts +. Named after the application (as defined by `spring.application.name`) +. Matching some labels + +*Note:* + +By default, consuming Secrets through the API (points 2 and 3 above) *is not enabled* for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. +Further, we recommend that containers share secrets through mounted volumes. + +If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. +For more information about risks and best practices when consuming Secrets through the API refer to https://kubernetes.io/docs/concepts/configuration/secret/#best-practices[this doc]. + +If the secrets are found, their data is made available to the application. + +Assume that we have a spring boot application named `demo` that uses properties to read its database +configuration. We can create a Kubernetes secret by using the following command: + +[source] +---- +kubectl create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd +---- + +The preceding command would create the following secret (which you can see by using `kubectl get secrets db-secret -o yaml`): + +[source,yaml] +---- +apiVersion: v1 +data: + password: cDQ1NXcwcmQ= + username: dXNlcg== +kind: Secret +metadata: + creationTimestamp: 2017-07-04T09:15:57Z + name: db-secret + namespace: default + resourceVersion: "357496" + selfLink: /api/v1/namespaces/default/secrets/db-secret + uid: 63c89263-6099-11e7-b3da-76d6186905a8 +type: Opaque +---- + +Note that the data contains Base64-encoded versions of the literal provided by the `create` command. + +Your application can then use this secret -- for example, by exporting the secret's value as environment variables: + +[source,yaml] +---- +apiVersion: v1 +kind: Deployment +metadata: + name: ${project.artifactId} +spec: + template: + spec: + containers: + - env: + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: db-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-secret + key: password +---- + +You can select the Secrets to consume in a number of ways: + +. By listing the directories where secrets are mapped: ++ +[source,bash] +---- +-Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql +---- ++ +If you have all the secrets mapped to a common root, you can set them like: ++ +[source,bash] +---- +-Dspring.cloud.kubernetes.secrets.paths=/etc/secrets +---- + +. By setting a named secret: ++ +[source,bash] +---- +-Dspring.cloud.kubernetes.secrets.name=db-secret +---- + +. By defining a list of labels: ++ +[source,bash] +---- +-Dspring.cloud.kubernetes.secrets.labels.broker=activemq +-Dspring.cloud.kubernetes.secrets.labels.db=postgresql +---- + +As the case with `ConfigMap`, more advanced configuration is also possible where you can use multiple `Secret` +instances. The `spring.cloud.kubernetes.secrets.sources` list makes this possible. +For example, you could define the following `Secret` instances: + +[source,yaml] +---- +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + secrets: + name: default-name + namespace: default-namespace + sources: + # Spring Cloud Kubernetes looks up a Secret named s1 in namespace default-namespace + - name: s1 + # Spring Cloud Kubernetes looks up a Secret named default-name in namespace n2 + - namespace: n2 + # Spring Cloud Kubernetes looks up a Secret named s3 in namespace n3 + - namespace: n3 + name: s3 +---- + +In the preceding example, if `spring.cloud.kubernetes.secrets.namespace` had not been set, +the `Secret` named `s1` would be looked up in the namespace that the application runs. +See xref:property-source-config/namespace-resolution.adoc[namespace-resolution] to get a better understanding of how the namespace +of the application is resolved. + +xref:property-source-config/configmap-propertysource.adoc#config-map-fail-fast[Similar to the `ConfigMaps`]; if you want your application to fail to start +when it is unable to load `Secrets` property sources, you can set `spring.cloud.kubernetes.secrets.fail-fast=true`. + +It is also possible to enable retry for `Secret` property sources xref:property-source-config/configmap-propertysource.adoc#config-map-retry[like the `ConfigMaps`]. +As with the `ConfigMap` property sources, first you need to set `spring.cloud.kubernetes.secrets.fail-fast=true`. +Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath. +Retry behavior of the `Secret` property sources can be configured by setting the `spring.cloud.kubernetes.secrets.retry.*` +properties. + +NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason +and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `Secrets` `PropertySources` +by setting `spring.cloud.kubernetes.secrets.retry.enabled=false`. + +Since data coming from Secrets is usually treated as sensitive, endpoints of the actuator `/env` and `/configprops` can be made to sanitize data, so that it is not displayed in plain text. In order to do that, you need to set: + +[source] +---- +spring.cloud.kubernetes.sanitize.secrets=true +---- + +This setting is supported since `3.0.6` and upwards. + +.Properties: +[options="header,footer"] +|=== +| Name | Type | Default | Description +| `spring.cloud.kubernetes.secrets.enabled` | `Boolean` | `true` | Enable Secrets `PropertySource` +| `spring.cloud.kubernetes.secrets.name` | `String` | `${spring.application.name}` | Sets the name of the secret to look up +| `spring.cloud.kubernetes.secrets.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to look up +| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` | Sets the labels used to lookup secrets +| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` | Sets the paths where secrets are mounted (example 1) +| `spring.cloud.kubernetes.secrets.enableApi` | `Boolean` | `false` | Enables or disables consuming secrets through APIs (examples 2 and 3) +| `spring.cloud.kubernetes.secrets.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `Secret` +| `spring.cloud.kubernetes.secrets.retry.enabled` | `Boolean` | `true` | Enable or disable secrets retry. +| `spring.cloud.kubernetes.secrets.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. +| `spring.cloud.kubernetes.secrets.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. +| `spring.cloud.kubernetes.secrets.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. +| `spring.cloud.kubernetes.secrets.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. +|=== + +Notes: + +* The `spring.cloud.kubernetes.secrets.labels` property behaves as defined by +https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#map-based-binding[Map-based binding]. +* The `spring.cloud.kubernetes.secrets.paths` property behaves as defined by +https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#collection-based-binding[Collection-based binding]. +* Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod. + +You can find an example of an application that uses secrets (though it has not been updated to use the new `spring-cloud-kubernetes` project) at +https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-camel-config] + diff --git a/docs/src/main/asciidoc/security-service-accounts.adoc b/docs/modules/ROOT/pages/security-service-accounts.adoc similarity index 93% rename from docs/src/main/asciidoc/security-service-accounts.adoc rename to docs/modules/ROOT/pages/security-service-accounts.adoc index ddd5da50dc..7e08c3e943 100644 --- a/docs/src/main/asciidoc/security-service-accounts.adoc +++ b/docs/modules/ROOT/pages/security-service-accounts.adoc @@ -1,12 +1,13 @@ -== Security Configurations Inside Kubernetes +[[security-configurations-inside-kubernetes]] += Security Configurations Inside Kubernetes -=== Namespace +[[namespace]] +== Namespace Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows: -==== [source] ---- env: @@ -15,9 +16,9 @@ For earlier versions, it needs to be specified as an environment variable to the fieldRef: fieldPath: "metadata.namespace" ---- -==== -=== Service Account +[[service-account]] +== Service Account For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with `spring-cloud-kubernetes` has access to the Kubernetes API. For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. @@ -46,7 +47,6 @@ For development purposes, you can add `cluster-reader` permissions to your `defa The following Role and RoleBinding are an example for namespaced permissions for the `default` account: -==== [source,yaml] ---- kind: Role @@ -75,4 +75,3 @@ roleRef: name: namespace-reader apiGroup: "" ---- -==== diff --git a/docs/src/main/asciidoc/service-registry.adoc b/docs/modules/ROOT/pages/service-registry.adoc similarity index 77% rename from docs/src/main/asciidoc/service-registry.adoc rename to docs/modules/ROOT/pages/service-registry.adoc index 723e928a63..a6102d3e25 100644 --- a/docs/src/main/asciidoc/service-registry.adoc +++ b/docs/modules/ROOT/pages/service-registry.adoc @@ -1,4 +1,6 @@ -== Service Registry Implementation +[[service-registry-implementation]] += Service Registry Implementation +:page-section-summary-toc: 1 In Kubernetes service registration is controlled by the platform, the application itself does not control registration as it may do in other platforms. For this reason using `spring.cloud.service-registry.auto-registration.enabled` diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc similarity index 93% rename from docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc rename to docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc index 23cb8c12df..037557e1fa 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc @@ -1,5 +1,5 @@ -[#spring-cloud-kubernetes-configserver] -## Spring Cloud Kubernetes Config Server +[spring-cloud-kubernetes-configserver] += Spring Cloud Kubernetes Config Server The Spring Cloud Kubernetes Config Server, is based on https://spring.io/projects/spring-cloud-config[Spring Cloud Config Server] and adds an https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository[environment repository] for Kubernetes https://kubernetes.io/docs/concepts/configuration/configmap/[Config Maps] and https://kubernetes.io/docs/concepts/configuration/secret/[Secrets]. @@ -11,17 +11,17 @@ A default image is located on https://hub.docker.com/r/springcloud/spring-cloud- the code and image yourself. However, if you need to customize the config server behavior or prefer to build the image yourself you can easily build your own image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver[source code on GitHub] and use that. -### Configuration +## Configuration -#### Enabling The Kubernetes Environment Repository +### Enabling The Kubernetes Environment Repository To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. You may activate other profiles as well to use other environment repository implementations. -#### Config Map and Secret PropertySources +### Config Map and Secret PropertySources By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. -#### Fetching Config Map and Secret Data From Additional Namespaces +### Fetching Config Map and Secret Data From Additional Namespaces By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated list of namespace values. @@ -29,15 +29,14 @@ list of namespace values. NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. -#### Kubernetes Access Controls +### Kubernetes Access Controls The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). -### Deployment Yaml +## Deployment Yaml Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. -==== [source,yaml] ---- --- @@ -119,4 +118,3 @@ items: - containerPort: 8888 ---- -==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configuration-watcher.adoc similarity index 67% rename from docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc rename to docs/modules/ROOT/pages/spring-cloud-kubernetes-configuration-watcher.adoc index 7acb43a50b..c93649c095 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configuration-watcher.adoc @@ -1,5 +1,5 @@ -[#spring-cloud-kubernetes-configuration-watcher] -## Spring Cloud Kubernetes Configuration Watcher +[spring-cloud-kubernetes-configuration-watcher] += Spring Cloud Kubernetes Configuration Watcher Kubernetes provides the ability to https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume[mount a ConfigMap or Secret as a volume] in the container of your application. When the contents of the ConfigMap or Secret changes, the https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically[mounted volume will be updated with those changes]. @@ -15,17 +15,69 @@ The application is published as a container and is available on https://hub.dock However, if you need to customize the config watcher behavior or prefer to build the image yourself you can easily build your own image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher[source code on GitHub] and use that. +Another option to configure it is to provide some environment variables in the deployment.yaml used to deploy configuration watcher. Here are some important ones: + +[source] +---- + +env: + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER + value: DEBUG + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD + value: DEBUG + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD + value: DEBUG +---- + +These enable debug logging on the configuration watcher and are particular useful on the initial set-up, to be able to diagnose potential miss-configurations. + +[source] +---- +env: + - name: SPRING_CLOUD_KUBERNETES_RELOAD_NAMESPACES_0 + value: "namespace-a" +---- + +This one lets watcher know where to search for secrets and configmaps. You have two options here: selective namespaces (the setting above) and a namespace chosen by xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] (this is the default option). +Keep in mind that all these options require proper RBAC rules. + +Changes from configmaps/secrets will only trigger an event being fired from configuration watcher if that particular change came from a source that has a label: `spring.cloud.kubernetes.config=true` or `spring.cloud.kubernetes.secret=true`. + +To put it simpler, if you change a configmap (or secret), that does _not_ have the label above, configuration watcher will skip firing an event for it (if you enabled debug logging, this will be visible in logs). + +By default, configuration watcher will monitor all configmaps/secrets in the configured namespace(s). If you want to filter to watch only particular sources, you can do that by setting: + +[source] +---- +SPRING_CLOUD_KUBERNETES_CONFIG_INFORMER_ENABLED=TRUE +---- + +This will tell watcher to only monitor sources that have a label: `spring.cloud.kubernetes.config.informer.enabled=true`. + +One more important configuration, especially for configmaps and secrets that are mounted as volumes (via `spring.cloud.kubernetes.config.paths`/`spring.cloud.kubernetes.secrets.paths` or using `spring.config.import`) is: + +[source] +---- +- name: SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_REFRESHDELAY + value: "10000" +---- + +This tells how many milliseconds should we wait before firing the event from configuration watcher. This is important because kubernetes documentation says: + +> When a ConfigMap currently consumed in a volume is updated, projected keys are eventually updated as well. + +You need to "match" this _eventually_ part to that value in milliseconds on your cluster. + Spring Cloud Kubernetes Configuration Watcher can send refresh notifications to applications in two ways. -1. Over HTTP in which case the application being notified must of the `/refresh` actuator endpoint exposed and accessible from within the cluster +1. Over HTTP, in which case the application being notified, must have the `/refresh` actuator endpoint exposed and accessible from within the cluster 2. Using Spring Cloud Bus, in which case you will need a message broker deployed to your custer for the application to use. -### Deployment YAML +## Deployment YAML Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes. -==== -[source,yaml] +[source,yaml,subs="attributes+"] ---- --- apiVersion: v1 @@ -89,7 +141,7 @@ items: serviceAccount: spring-cloud-kubernetes-configuration-watcher containers: - name: spring-cloud-kubernetes-configuration-watcher - image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.1-SNAPSHOT + image: springcloud/spring-cloud-kubernetes-configuration-watcher:{spring-cloud-version} imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -103,19 +155,14 @@ items: - containerPort: 8888 ---- -==== The Service Account and associated Role Binding is important for Spring Cloud Kubernetes Configuration to work properly. The controller needs access to read data about ConfigMaps, Pods, Services, Endpoints and Secrets in the Kubernetes cluster. -### Monitoring ConfigMaps and Secrets - -Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMaps with a label of `spring.cloud.kubernetes.config` with the value `true` -or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels -or the values of those labels is not `true` then any changes will be ignored. +## Monitoring ConfigMaps and Secrets -If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret -and send a notification to the application with that name. This might not be enough for your use-case though, you could for example what to: +If a change is made to a ConfigMap or Secret with valid labels (as detailed above), then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret +and send a notification to the application with that name. This might not be enough for your use-case though, you could for example want to: - bind a config-map to multiple applications, so that a change inside a single configmap triggers a refresh for many services - have profile based sources trigger events for your application @@ -127,7 +174,6 @@ that specifies the names of applications that will receive a notification when c For example: -==== [source,yaml] ---- kind: ConfigMap @@ -139,21 +185,19 @@ metadata: annotations: spring.cloud.kubernetes.configmap.apps: "app-a, app-b" ---- -==== -### HTTP Implementation +## HTTP Implementation -The HTTP implementation is what is used by default. When this implementation is used Spring Cloud Kubernetes Configuration Watcher and a +The HTTP implementation is what is used by default. When this implementation is used, Spring Cloud Kubernetes Configuration Watcher and a change to a ConfigMap or Secret occurs then the HTTP implementation will use the Spring Cloud Kubernetes Discovery Client to fetch all instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator -`/refresh` endpoint. By default it will send the post request to `/actuator/refresh` using the port registered in the discovery client. +`/refresh` endpoint. By default, it will send the post request to `/actuator/refresh` using the port registered in the discovery client. -#### Non-Default Management Port and Actuator Path +### Non-Default Management Port and Actuator Path If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application can add an annotation called `boot.spring.io/actuator` and set its value to the path and port used by the application. For example -==== [source,yaml] ---- apiVersion: v1 @@ -172,24 +216,22 @@ spec: selector: app: config-map-demo ---- -==== Another way you can choose to configure the actuator path and/or management port is by setting `spring.cloud.kubernetes.configuration.watcher.actuatorPath` and `spring.cloud.kubernetes.configuration.watcher.actuatorPort`. -### Messaging Implementation +## Messaging Implementation The messaging implementation can be enabled by setting profile to either `bus-amqp` (RabbitMQ) or `bus-kafka` (Kafka) when the Spring Cloud Kubernetes Configuration Watcher application is deployed to Kubernetes. -### Configuring RabbitMQ +## Configuring RabbitMQ When the `bus-amqp` profile is enabled you will need to configure Spring RabbitMQ to point it to the location of the RabbitMQ instance you would like to use as well as any credentials necessary to authenticate. This can be done by setting the standard Spring RabbitMQ properties, for example -==== [source,yaml] ---- spring: @@ -198,14 +240,12 @@ spring: password: password host: rabbitmq ---- -==== -### Configuring Kafka +## Configuring Kafka When the `bus-kafka` profile is enabled you will need to configure Spring Kafka to point it to the location of the Kafka Broker instance you would like to use. This can be done by setting the standard Spring Kafka properties, for example -==== [source,yaml] ---- spring: @@ -213,4 +253,3 @@ spring: producer: bootstrap-servers: localhost:9092 ---- -==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes-discoveryserver.adoc similarity index 86% rename from docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc rename to docs/modules/ROOT/pages/spring-cloud-kubernetes-discoveryserver.adoc index ebe0e2b99b..c7635ab311 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes-discoveryserver.adoc @@ -1,27 +1,26 @@ -[#spring-cloud-kubernetes-discoveryserver] -## Spring Cloud Kubernetes Discovery Server +[spring-cloud-kubernetes-discoveryserver] += Spring Cloud Kubernetes Discovery Server The Spring Cloud Kubernetes Discovery Server provides HTTP endpoints apps can use to gather information about services available within a Kubernetes cluster. The Spring Cloud Kubernetes Discovery Server can be used by apps using the `spring-cloud-starter-kubernetes-discoveryclient` to provide data to the `DiscoveryClient` implementation provided by that starter. -### Permissions +## Permissions The Spring Cloud Discovery server uses -the Kubernetes API server to get data about Service and Endpoint resrouces so it needs list, watch, and -get permissions to use those endpoints. See the below sample Kubernetes deployment YAML for an -examlpe of how to configure the Service Account on Kubernetes. +the Kubernetes API server to get data about Pod, Service and Endpoint resources, so it needs list, +watch, and get permissions to use those endpoints. See the below sample Kubernetes deployment YAML +for an example of how to configure the Service Account on Kubernetes. -### Endpoints +## Endpoints There are three endpoints exposed by the server. -#### `/apps` +### `/apps` A `GET` request sent to `/apps` will return a JSON array of available services. Each item contains the name of the Kubernetes service and service instance information. Below is a sample response. -==== [source,json] ---- [ @@ -67,14 +66,12 @@ the name of the Kubernetes service and service instance information. Below is a } ] ---- -==== -#### `/apps/{name}` +### `/apps/\{name}` -A `GET` request to `/apps/{name}` can be used to get instance data for all instances of a given +A `GET` request to `/apps/\{name}` can be used to get instance data for all instances of a given service. Below is a sample response when a `GET` request is made to `/apps/kubernetes`. -==== [source,json] ---- [ @@ -95,14 +92,12 @@ service. Below is a sample response when a `GET` request is made to `/apps/kube } ] ---- -==== -#### `/app/{name}/{instanceid}` +### `/app/\{name}/\{instanceid}` -A `GET` request made to `/app/{name}/{instanceid}` will return the instance data for a specific +A `GET` request made to `/app/\{name}/\{instanceid}` will return the instance data for a specific instance of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes/1234`. -==== [source,json] ---- { @@ -121,17 +116,15 @@ instance of a given service. Below is a sample response when a `GET` request is "scheme":"http" } ---- -==== -### Deployment YAML +## Deployment YAML An image of the Spring Cloud Discovery Server is hosted on https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-discoveryserver[Docker Hub]. However, if you need to customize the discovery server behavior or prefer to build the image yourself you can easily build your own image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver[source code on GitHub] and use that. -Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes. +Below is a sample deployment YAML you can use to deploy the Kubernetes Discovery Server to Kubernetes. -==== [source,yaml] ---- --- @@ -178,7 +171,7 @@ items: name: namespace-reader rules: - apiGroups: ["", "extensions", "apps"] - resources: ["services", "endpoints"] + resources: ["pods", "services", "endpoints"] verbs: ["get", "list", "watch"] - apiVersion: apps/v1 kind: Deployment @@ -193,10 +186,10 @@ items: labels: app: spring-cloud-kubernetes-discoveryserver spec: - serviceAccount: spring-cloud-kubernetes-discoveryserver + serviceAccountName: spring-cloud-kubernetes-discoveryserver containers: - name: spring-cloud-kubernetes-discoveryserver - image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.0-SNAPSHOT + image: springcloud/spring-cloud-kubernetes-discoveryserver:3.1.0 imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -211,4 +204,3 @@ items: ---- -==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc similarity index 67% rename from docs/src/main/asciidoc/spring-cloud-kubernetes.adoc rename to docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc index 435208ec29..90188d2453 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc @@ -1,52 +1,41 @@ +[[spring-cloud-kubernetes]] = Spring Cloud Kubernetes -include::_attributes.adoc[] This reference guide covers how to use Spring Cloud Kubernetes. +[[why-do-you-need-spring-cloud-kubernetes]] == Why do you need Spring Cloud Kubernetes? Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes. If you are just getting started in your journey to running your Spring Boot app on Kubernetes you can accomplish a lot with nothing more than a basic Spring Boot app and Kubernetes itself. To learn more, you can get started by reading the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes[Spring Boot reference documentation for deploying to Kubernetes ] and also working through the workshop material https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop[Spring and Kubernetes]. -include::getting-started.adoc[] -include::discovery-client.adoc[] -include::discovery-kubernetes-native.adoc[] -include::property-source-config.adoc[] -include::kubernetes-awareness.adoc[] -include::pod-health-indicator.adoc[] -include::info-contributor.adoc[] -include::leader-election.adoc[] -include::load-balancer.adoc[] -include::security-service-accounts.adoc[] -include::service-registry.adoc[] -include::spring-cloud-kubernetes-configuration-watcher.adoc[] -include::spring-cloud-kubernetes-configserver.adoc[] -include::spring-cloud-kubernetes-discoveryserver.adoc[] -include::examples.adoc[] -include::other-resources.adoc[] +[[configuration-properties]] == Configuration properties To see the list of all Kubernetes related configuration properties please check link:appendix.html[the Appendix page]. +[[building]] == Building -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building.adoc[] +Click https://docs.spring.io/spring-cloud-build/reference/building.html[here] for basic building instructions. + +[[building-docker-images-on-arm64]] === Building Docker Images On ARM64 If you run the Spring Cloud Kuberentes build on an ARM64 machine the docker images @@ -60,10 +49,13 @@ For example: ``` +[[contributing]] == Contributing -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] +Click https://docs.spring.io/spring-cloud-build/reference/contributing.html[here] for instructions on contributing to this project. + +[[aot-and-native-image-support]] == AOT and native image support At this point, Spring Cloud Kubernetes does not support Spring Boot AOT transformations or native images. Partial support might be added in future releases. diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc similarity index 99% rename from docs/src/main/asciidoc/_configprops.adoc rename to docs/modules/ROOT/partials/_configprops.adoc index d6e8d2d5fb..1564003f50 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -60,6 +60,7 @@ |spring.cloud.kubernetes.config.use-name-as-prefix | `+++false+++` | |spring.cloud.kubernetes.discovery.all-namespaces | `+++false+++` | |spring.cloud.kubernetes.discovery.cache-loading-timeout-seconds | `+++60+++` | +|spring.cloud.kubernetes.discovery.discovery-server-url | | |spring.cloud.kubernetes.discovery.enabled | `+++true+++` | |spring.cloud.kubernetes.discovery.filter | | |spring.cloud.kubernetes.discovery.include-external-name-services | `+++false+++` | diff --git a/docs/pom.xml b/docs/pom.xml index 1da38f1d8a..11a73181be 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,8 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT + .. jar Spring Cloud Kubernetes Docs @@ -17,7 +18,6 @@ spring-cloud-kubernetes ${basedir}/.. spring.cloud.kubernetes.* - deploy none @@ -42,26 +42,32 @@ docs + + + src/main/antora/resources/antora-resources + true + + pl.project13.maven git-commit-id-plugin - - org.codehaus.mojo - exec-maven-plugin - org.apache.maven.plugins maven-dependency-plugin - org.apache.maven.plugins - maven-resources-plugin + org.codehaus.mojo + exec-maven-plugin + + + io.spring.maven.antora + antora-component-version-maven-plugin - org.asciidoctor - asciidoctor-maven-plugin + io.spring.maven.antora + antora-maven-plugin org.apache.maven.plugins diff --git a/docs/src/main/antora/resources/antora-resources/antora.yml b/docs/src/main/antora/resources/antora-resources/antora.yml new file mode 100644 index 0000000000..9148923fa3 --- /dev/null +++ b/docs/src/main/antora/resources/antora-resources/antora.yml @@ -0,0 +1,20 @@ +version: @antora-component.version@ +prerelease: @antora-component.prerelease@ + +asciidoc: + attributes: + attribute-missing: 'warn' + chomp: 'all' + project-root: @maven.multiModuleProjectDirectory@ + github-repo: @docs.main@ + github-raw: https://raw.githubusercontent.com/spring-cloud/@docs.main@/@github-tag@ + github-code: https://github.com/spring-cloud/@docs.main@/tree/@github-tag@ + github-issues: https://github.com/spring-cloud/@docs.main@/issues/ + github-wiki: https://github.com/spring-cloud/@docs.main@/wiki + spring-cloud-version: @project.version@ + github-tag: @github-tag@ + version-type: @version-type@ + docs-url: https://docs.spring.io/@docs.main@/docs/@project.version@ + raw-docs-url: https://raw.githubusercontent.com/spring-cloud/@docs.main@/@github-tag@ + project-version: @project.version@ + project-name: @docs.main@ diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index cf518c7c51..e69de29bb2 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -1 +0,0 @@ -include::spring-cloud-kubernetes.adoc[] diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc deleted file mode 100644 index 99d6a18204..0000000000 --- a/docs/src/main/asciidoc/discovery-client.adoc +++ /dev/null @@ -1,175 +0,0 @@ -== DiscoveryClient for Kubernetes - -This project provides an implementation of https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java[Discovery Client] -for https://kubernetes.io[Kubernetes]. -This client lets you query Kubernetes endpoints (see https://kubernetes.io/docs/user-guide/services/[services]) by name. -A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent `http` and `https` addresses and that a client can -access from a Spring Boot application running as a pod. - -DiscoveryClient can also find services of type `ExternalName` (see https://kubernetes.io/docs/concepts/services-networking/service/#externalname[ExternalName services]). At the moment, external name support type of services is only available if the following property `spring.cloud.kubernetes.discovery.include-external-name-services` is set to `true` and only in the `fabric8` implementation. In a later release, support will be added for the kubernetes native client also. - -This is something that you get for free by adding the following dependency inside your project: - -==== -HTTP Based `DiscoveryClient` -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-discoveryclient - ----- -==== - -NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the -<>. - -==== -Fabric8 Kubernetes Client -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8 - ----- -==== - -==== -Kubernetes Java Client -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-kubernetes-client - ----- -==== - -To enable loading of the `DiscoveryClient`, add `@EnableDiscoveryClient` to the according configuration or application class, as the following example shows: - -==== -[source,java] ----- -@SpringBootApplication -@EnableDiscoveryClient -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} ----- -==== - -Then you can inject the client in your code simply by autowiring it, as the following example shows: - -==== -[source,java] ----- -@Autowired -private DiscoveryClient discoveryClient; ----- -==== - -You can choose to enable `DiscoveryClient` from all namespaces by setting the following property in `application.properties`: - -==== -[source] ----- -spring.cloud.kubernetes.discovery.all-namespaces=true ----- -==== - -To discover services and endpoints only from specified namespaces you should set property `all-namespaces` to `false` and set the following property in `application.properties` (in this example namespaces are: `ns1` and `ns2`). - -==== -[source] ----- -spring.cloud.kubernetes.discovery.namespaces[0]=ns1 -spring.cloud.kubernetes.discovery.namespaces[1]=ns2 ----- -==== - -To discover service endpoint addresses that are not marked as "ready" by the kubernetes api server, you can set the following property in `application.properties` (default: false): - -==== -[source] ----- -spring.cloud.kubernetes.discovery.include-not-ready-addresses=true ----- -NOTE: This might be useful when discovering services for monitoring purposes, and would enable inspecting the `/health` endpoint of not-ready service instances. -==== - -If your service exposes multiple ports, you will need to specify which port the `DiscoveryClient` should use. -The `DiscoveryClient` will choose the port using the following logic. - -1. If the service has a label `primary-port-name` it will use the port with the name specified in the label's value. -2. If no label is present, then the port name specified in `spring.cloud.kubernetes.discovery.primary-port-name` will be used. -3. If neither of the above are specified it will use the port named `https`. -4. If none of the above conditions are met it will use the port named `http`. -5. As a last resort it wil pick the first port in the list of ports. - -WARNING: The last option may result in non-deterministic behaviour. -Please make sure to configure your service and/or application accordingly. - -By default all of the ports and their names will be added to the metadata of the `ServiceInstance`. - -As said before, if you want to get the list of `ServiceInstance` to also include the `ExternalName` type services, you need to enable that support via: `spring.cloud.kubernetes.discovery.include-external-name-services=true`. As such, when calling `DiscoveryClient::getInstances` those will be returned also. You can distinguish between `ExternalName` and any other types by inspecting `ServiceInstance::getMetadata` and lookup for a field called `type`. This will be the type of the service returned : `ExternalName`/`ClusterIP`, etc. - -`ServiceInstance` can include the labels and annotations of specific pods from the underlying service instance. To obtain such information, you need to also enable: - -`spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true` and/or `spring.cloud.kubernetes.discovery.metadata.add-pod-annotations=true`. At the moment, such functionality is present only in the fabric8 client implementation, but will be added to the kubernetes native client in a later release. - -If, for any reason, you need to disable the `DiscoveryClient`, you can set the following property in `application.properties`: - -==== -[source] ----- -spring.cloud.kubernetes.discovery.enabled=false ----- -==== - -Some Spring Cloud components use the `DiscoveryClient` in order to obtain information about the local service instance. For -this to work, you need to align the Kubernetes service name with the `spring.application.name` property. - -NOTE: `spring.application.name` has no effect as far as the name registered for the application within Kubernetes - -Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the -`DiscoveryClient` implementation accordingly. By "watch" we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay` -milliseconds (by default it is `30000`). The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints -(for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event -should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract. -The endpoints will be queried in either : - - - all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`) - - - specific namespaces (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example: - -``` -spring: - cloud: - kubernetes: - discovery: - namespaces: - - namespace-a - - namespace-b -``` - -- we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken. - -In order to enable this functionality you need to add -`@EnableScheduling` on a configuration class in your application. - -By default, we use the `Endpoints`(see https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) API to find out the current state of services. There is another way though, via `EndpointSlices` (https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/). Such support can be enabled via a property: `spring.cloud.kubernetes.discovery.use-endpoint-slices=true` (by default it is `false`). Of course, your cluster has to support it also. As a matter of fact, if you enable this property, but your cluster does not support it, we will fail starting the application. If you decide to enable such support, you also need proper Role/ClusterRole set-up. For example: - -``` -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - namespace: default - name: namespace-reader -rules: - - apiGroups: ["discovery.k8s.io"] - resources: ["endpointslices"] - verbs: ["get", "list", "watch"] -``` diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc deleted file mode 100644 index cf518c7c51..0000000000 --- a/docs/src/main/asciidoc/index.adoc +++ /dev/null @@ -1 +0,0 @@ -include::spring-cloud-kubernetes.adoc[] diff --git a/docs/src/main/asciidoc/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc deleted file mode 100644 index 25a00f1c5b..0000000000 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ /dev/null @@ -1,1049 +0,0 @@ -== Kubernetes PropertySource implementations - -The most common approach to configuring your Spring Boot application is to create an `application.properties` or `application.yaml` or -an `application-profile.properties` or `application-profile.yaml` file that contains key-value pairs that provide customization values to your -application or Spring Boot starters. You can override these properties by specifying system properties or environment -variables. - -To enable this functionality you need to set `spring.config.import=kubernetes:` in your application's configuration properties. -Currently you can not specify a ConfigMap or Secret to load using `spring.config.import`, by default Spring Cloud Kubernetes -will load a ConfigMap and/or Secret based on the `spring.application.name` property. If `spring.application.name` is not set it will -load a ConfigMap and/or Secret with the name `application`. - -If you would like to load Kubernetes ``PropertySource``s during the bootstrap phase like it worked prior to the 3.0.x release -you can either add `spring-cloud-starter-bootstrap` to your application's classpath or set `spring.cloud.bootstrap.enabled=true` -as an environment variable. - -[[configmap-propertysource]] -=== Using a `ConfigMap` `PropertySource` - -Kubernetes provides a resource named https://kubernetes.io/docs/user-guide/configmap/[`ConfigMap`] to externalize the -parameters to pass to your application in the form of key-value pairs or embedded `application.properties` or `application.yaml` files. -The link:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-fabric8-config[Spring Cloud Kubernetes Config] project makes Kubernetes `ConfigMap` instances available -during application startup and triggers hot reloading of beans or Spring context when changes are detected on -observed `ConfigMap` instances. - -Everything that follows is explained mainly referring to examples using ConfigMaps, but the same stands for -Secrets, i.e.: every feature is supported for both. - -The default behavior is to create a `Fabric8ConfigMapPropertySource` (or a `KubernetesClientConfigMapPropertySource`) based on a Kubernetes `ConfigMap` that has a `metadata.name` value of either the name of -your Spring application (as defined by its `spring.application.name` property) or a custom name defined within the -`application.properties` file under the following key: `spring.cloud.kubernetes.config.name`. - -However, more advanced configuration is possible where you can use multiple `ConfigMap` instances. -The `spring.cloud.kubernetes.config.sources` list makes this possible. -For example, you could define the following `ConfigMap` instances: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - config: - name: default-name - namespace: default-namespace - sources: - # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace - - name: c1 - # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2 - - namespace: n2 - # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3 - - namespace: n3 - name: c3 ----- -==== - -In the preceding example, if `spring.cloud.kubernetes.config.namespace` had not been set, -the `ConfigMap` named `c1` would be looked up in the namespace that the application runs. -See <> to get a better understanding of how the namespace -of the application is resolved. - - -Any matching `ConfigMap` that is found is processed as follows: - -* Apply individual configuration properties. -* Apply as `yaml` (or `properties`) the content of any property that is named by the value of `spring.application.name` - (if it's not present, by `application.yaml/properties`) -* Apply as a properties file the content of the above name + each active profile. - -An example should make a lot more sense. Let's suppose that `spring.application.name=my-app` and that -we have a single active profile called `k8s`. For a configuration as below: - - -==== -[source] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: my-app -data: - my-app.yaml: |- - ... - my-app-k8s.yaml: |- - .. - my-app-dev.yaml: |- - .. - someProp: someValue ----- -==== - -These is what we will end-up loading: - - - `my-app.yaml` treated as a file - - `my-app-k8s.yaml` treated as a file - - `my-app-dev.yaml` _ignored_, since `dev` is _not_ an active profile - - `someProp: someValue` plain property - -The single exception to the aforementioned flow is when the `ConfigMap` contains a *single* key that indicates -the file is a YAML or properties file. In that case, the name of the key does NOT have to be `application.yaml` or -`application.properties` (it can be anything) and the value of the property is treated correctly. -This features facilitates the use case where the `ConfigMap` was created by using something like the following: - -==== -[source] ----- -kubectl create configmap game-config --from-file=/path/to/app-config.yaml ----- -==== - -Assume that we have a Spring Boot application named `demo` that uses the following properties to read its thread pool -configuration. - -* `pool.size.core` -* `pool.size.maximum` - -This can be externalized to config map in `yaml` format as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - pool.size.core: 1 - pool.size.max: 16 ----- -==== - -Individual properties work fine for most cases. However, sometimes, embedded `yaml` is more convenient. In this case, we -use a single property named `application.yaml` to embed our `yaml`, as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yaml: |- - pool: - size: - core: 1 - max:16 ----- -==== - -The following example also works: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - custom-name.yaml: |- - pool: - size: - core: 1 - max:16 ----- -==== - -You can also define the search to happen based on labels, for example: - - -==== -[source,yaml] ----- -spring: - application: - name: labeled-configmap-with-prefix - cloud: - kubernetes: - config: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - letter: a ----- -==== - -This will search for every configmap in namespace `spring-k8s` that has labels `{letter : a}`. The important -thing to notice here is that unlike reading a configmap by name, this can result in _multiple_ config maps read. -As usual, the same feature is supported for secrets. - -You can also configure Spring Boot applications differently depending on active profiles that are merged together -when the `ConfigMap` is read. You can provide different property values for different profiles by using an -`application.properties` or `application.yaml` property, specifying profile-specific values, each in their own document -(indicated by the `---` sequence), as follows: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yml: |- - greeting: - message: Say Hello to the World - farewell: - message: Say Goodbye - --- - spring: - profiles: development - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers - --- - spring: - profiles: production - greeting: - message: Say Hello to the Ops ----- -==== - -In the preceding case, the configuration loaded into your Spring Application with the `development` profile is as follows: - -==== -[source,yaml] ----- - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers ----- -==== - -However, if the `production` profile is active, the configuration becomes: - -==== -[source,yaml] ----- - greeting: - message: Say Hello to the Ops - farewell: - message: Say Goodbye ----- -==== - -If both profiles are active, the property that appears last within the `ConfigMap` overwrites any preceding values. - -Another option is to create a different config map per profile and spring boot will automatically fetch it based -on active profiles - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo -data: - application.yml: |- - greeting: - message: Say Hello to the World - farewell: - message: Say Goodbye ----- -==== -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo-development -data: - application.yml: |- - spring: - profiles: development - greeting: - message: Say Hello to the Developers - farewell: - message: Say Goodbye to the Developers ----- -==== -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: demo-production -data: - application.yml: |- - spring: - profiles: production - greeting: - message: Say Hello to the Ops - farewell: - message: Say Goodbye ----- -==== - - -To tell Spring Boot which `profile` should be enabled see the https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles[Spring Boot documentation]. -One option for activating a specific profile when deploying to Kubernetes is to launch your Spring Boot application with an environment variable that you can define in the PodSpec at the container specification. - Deployment resource file, as follows: - -==== -[source,yaml] ----- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: deployment-name - labels: - app: deployment-name -spec: - replicas: 1 - selector: - matchLabels: - app: deployment-name - template: - metadata: - labels: - app: deployment-name - spec: - containers: - - name: container-name - image: your-image - env: - - name: SPRING_PROFILES_ACTIVE - value: "development" ----- -==== - -You could run into a situation where there are multiple configs maps that have the same property names. For example: - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-one -data: - application.yml: |- - greeting: - message: Say Hello from one ----- -==== - -and - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-two -data: - application.yml: |- - greeting: - message: Say Hello from two ----- -==== - -Depending on the order in which you place these in `bootstrap.yaml|properties`, you might end up with an un-expected result (the last config map wins). For example: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - config: - namespace: default-namespace - sources: - - name: config-map-two - - name: config-map-one ----- -==== - -will result in property `greetings.message` being `Say Hello from one`. - -There is a way to change this default configuration by specifying `useNameAsPrefix`. For example: - -==== -[source,yaml] ----- -spring: - application: - name: with-prefix - cloud: - kubernetes: - config: - useNameAsPrefix: true - namespace: default-namespace - sources: - - name: config-map-one - useNameAsPrefix: false - - name: config-map-two ----- -==== - -Such a configuration will result in two properties being generated: - - - `greetings.message` equal to `Say Hello from one`. - - - `config-map-two.greetings.message` equal to `Say Hello from two` - -Notice that `spring.cloud.kubernetes.config.useNameAsPrefix` has a _lower_ priority than `spring.cloud.kubernetes.config.sources.useNameAsPrefix`. -This allows you to set a "default" strategy for all sources, at the same time allowing to override only a few. - -If using the config map name is not an option, you can specify a different strategy, called : `explicitPrefix`. Since this is an _explicit_ prefix that -you select, it can only be supplied to the `sources` level. At the same time it has a higher priority than `useNameAsPrefix`. Let's suppose we have a third config map with these entries: - - -==== -[source,yaml] ----- -kind: ConfigMap -apiVersion: v1 -metadata: - name: config-map-three -data: - application.yml: |- - greeting: - message: Say Hello from three ----- -==== - -A configuration like the one below: - -==== -[source,yaml] ----- -spring: - application: - name: with-prefix - cloud: - kubernetes: - config: - useNameAsPrefix: true - namespace: default-namespace - sources: - - name: config-map-one - useNameAsPrefix: false - - name: config-map-two - explicitPrefix: two - - name: config-map-three ----- -==== - -will result in three properties being generated: - - - `greetings.message` equal to `Say Hello from one`. - - - `two.greetings.message` equal to `Say Hello from two`. - - - `config-map-three.greetings.message` equal to `Say Hello from three`. - -The same way you configure a prefix for configmaps, you can do it for secrets also; both for secrets that are based on name -and the ones based on labels. For example: - -==== -[source.yaml] ----- -spring: - application: - name: prefix-based-secrets - cloud: - kubernetes: - secrets: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - letter: a - useNameAsPrefix: false - - labels: - letter: b - explicitPrefix: two - - labels: - letter: c - - labels: - letter: d - useNameAsPrefix: true - - name: my-secret ----- -==== - -The same processing rules apply when generating property source as for config maps. The only difference is that -potentially, looking up secrets by labels can mean that we find more than one source. In such a case, prefix (if specified via `useNameAsPrefix`) -will be the names of all secrets found for those particular labels. - -One more thing to bear in mind is that we support `prefix` per _source_, not per secret. The easiest way to explain this is via an example: - -==== -[source.yaml] ----- -spring: - application: - name: prefix-based-secrets - cloud: - kubernetes: - secrets: - enableApi: true - useNameAsPrefix: true - namespace: spring-k8s - sources: - - labels: - color: blue - useNameAsPrefix: true ----- -==== - -Suppose that a query matching such a label will provide two secrets as a result: `secret-a` and `secret-b`. -Both of these secrets have the same property name: `color=sea-blue` and `color=ocean-blue`. It is undefined which -`color` will end-up as part of property sources, but the prefix for it will be `secret-a.secret-b` -(concatenated sorted naturally, names of the secrets). - -If you need more fine-grained results, adding more labels to identify the secret uniquely would be an option. - - - -By default, besides reading the config map that is specified in the `sources` configuration, Spring will also try to read -all properties from "profile aware" sources. The easiest way to explain this is via an example. Let's suppose your application -enables a profile called "dev" and you have a configuration like the one below: - -==== -[source,yaml] ----- -spring: - application: - name: spring-k8s - cloud: - kubernetes: - config: - namespace: default-namespace - sources: - - name: config-map-one ----- -==== - -Besides reading the `config-map-one`, Spring will also try to read `config-map-one-dev`; in this particular order. Each active profile -generates such a profile aware config map. - -Though your application should not be impacted by such a config map, it can be disabled if needed: - -==== -[source,yaml] ----- -spring: - application: - name: spring-k8s - cloud: - kubernetes: - config: - includeProfileSpecificSources: false - namespace: default-namespace - sources: - - name: config-map-one - includeProfileSpecificSources: false ----- -==== - -Notice that just like before, there are two levels where you can specify this property: for all config maps or -for individual ones; the latter having a higher priority. - -NOTE: You should check the security configuration section. To access config maps from inside a pod you need to have the correct -Kubernetes service accounts, roles and role bindings. - -Another option for using `ConfigMap` instances is to mount them into the Pod by running the Spring Cloud Kubernetes application -and having Spring Cloud Kubernetes read them from the file system. - -NOTE: This feature is deprecated and will be removed in a future release (Use `spring.config.import` instead). -This behavior is controlled by the `spring.cloud.kubernetes.config.paths` property. You can use it in -addition to or instead of the mechanism described earlier. -`spring.cloud.kubernetes.config.paths` expects a List of full paths to each property file, because directories are not being recursively parsed. For example: - -``` -spring: - cloud: - kubernetes: - config: - paths: - - /tmp/application.properties - - /var/application.yaml -``` - -NOTE: If you use `spring.cloud.kubernetes.config.paths` or `spring.cloud.kubernetes.secrets.path` the automatic reload -functionality will not work. You will need to make a `POST` request to the `/actuator/refresh` endpoint or -restart/redeploy the application. - -[#config-map-fail-fast] -In some cases, your application may be unable to load some of your `ConfigMaps` using the Kubernetes API. -If you want your application to fail the start-up process in such cases, you can set -`spring.cloud.kubernetes.config.fail-fast=true` to make the application start-up fail with an Exception. - -[#config-map-retry] -You can also make your application retry loading `ConfigMap` property sources on a failure. First, you need to -set `spring.cloud.kubernetes.config.fail-fast=true`. Then you need to add `spring-retry` -and `spring-boot-starter-aop` to your classpath. You can configure retry properties such as -the maximum number of attempts, backoff options like initial interval, multiplier, max interval by setting the -`spring.cloud.kubernetes.config.retry.*` properties. - -NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason -and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `ConfigMap` `PropertySources` -by setting `spring.cloud.kubernetes.config.retry.enabled=false`. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.config.enabled` | `Boolean` | `true` | Enable ConfigMaps `PropertySource` -| `spring.cloud.kubernetes.config.name` | `String` | `${spring.application.name}` | Sets the name of `ConfigMap` to look up -| `spring.cloud.kubernetes.config.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to lookup -| `spring.cloud.kubernetes.config.paths` | `List` | `null` | Sets the paths where `ConfigMap` instances are mounted -| `spring.cloud.kubernetes.config.enableApi` | `Boolean` | `true` | Enable or disable consuming `ConfigMap` instances through APIs -| `spring.cloud.kubernetes.config.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `ConfigMap` -| `spring.cloud.kubernetes.config.retry.enabled` | `Boolean` | `true` | Enable or disable config retry. -| `spring.cloud.kubernetes.config.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. -| `spring.cloud.kubernetes.config.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. -| `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. -| `spring.cloud.kubernetes.config.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. -|=== - -=== Secrets PropertySource - -Kubernetes has the notion of https://kubernetes.io/docs/concepts/configuration/secret/[Secrets] for storing -sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with `Secrets` to make secrets -accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the `spring.cloud.kubernetes.secrets.enabled` property. - -When enabled, the `Fabric8SecretsPropertySource` looks up Kubernetes for `Secrets` from the following sources: - -. Reading recursively from secrets mounts -. Named after the application (as defined by `spring.application.name`) -. Matching some labels - -*Note:* - -By default, consuming Secrets through the API (points 2 and 3 above) *is not enabled* for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. -Further, we recommend that containers share secrets through mounted volumes. - -If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. -For more information about risks and best practices when consuming Secrets through the API refer to https://kubernetes.io/docs/concepts/configuration/secret/#best-practices[this doc]. - -If the secrets are found, their data is made available to the application. - -Assume that we have a spring boot application named `demo` that uses properties to read its database -configuration. We can create a Kubernetes secret by using the following command: - -==== -[source] ----- -kubectl create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd ----- -==== - -The preceding command would create the following secret (which you can see by using `kubectl get secrets db-secret -o yaml`): - -==== -[source,yaml] ----- -apiVersion: v1 -data: - password: cDQ1NXcwcmQ= - username: dXNlcg== -kind: Secret -metadata: - creationTimestamp: 2017-07-04T09:15:57Z - name: db-secret - namespace: default - resourceVersion: "357496" - selfLink: /api/v1/namespaces/default/secrets/db-secret - uid: 63c89263-6099-11e7-b3da-76d6186905a8 -type: Opaque ----- -==== - -Note that the data contains Base64-encoded versions of the literal provided by the `create` command. - -Your application can then use this secret -- for example, by exporting the secret's value as environment variables: - -==== -[source,yaml] ----- -apiVersion: v1 -kind: Deployment -metadata: - name: ${project.artifactId} -spec: - template: - spec: - containers: - - env: - - name: DB_USERNAME - valueFrom: - secretKeyRef: - name: db-secret - key: username - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: db-secret - key: password ----- -==== - -You can select the Secrets to consume in a number of ways: - -. By listing the directories where secrets are mapped: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql ----- -==== -+ -If you have all the secrets mapped to a common root, you can set them like: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.paths=/etc/secrets ----- -==== - -. By setting a named secret: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.name=db-secret ----- -==== - -. By defining a list of labels: -+ -==== -[source,bash] ----- --Dspring.cloud.kubernetes.secrets.labels.broker=activemq --Dspring.cloud.kubernetes.secrets.labels.db=postgresql ----- -==== - -As the case with `ConfigMap`, more advanced configuration is also possible where you can use multiple `Secret` -instances. The `spring.cloud.kubernetes.secrets.sources` list makes this possible. -For example, you could define the following `Secret` instances: - -==== -[source,yaml] ----- -spring: - application: - name: cloud-k8s-app - cloud: - kubernetes: - secrets: - name: default-name - namespace: default-namespace - sources: - # Spring Cloud Kubernetes looks up a Secret named s1 in namespace default-namespace - - name: s1 - # Spring Cloud Kubernetes looks up a Secret named default-name in namespace n2 - - namespace: n2 - # Spring Cloud Kubernetes looks up a Secret named s3 in namespace n3 - - namespace: n3 - name: s3 ----- -==== - -In the preceding example, if `spring.cloud.kubernetes.secrets.namespace` had not been set, -the `Secret` named `s1` would be looked up in the namespace that the application runs. -See <> to get a better understanding of how the namespace -of the application is resolved. - -<>; if you want your application to fail to start -when it is unable to load `Secrets` property sources, you can set `spring.cloud.kubernetes.secrets.fail-fast=true`. - -It is also possible to enable retry for `Secret` property sources <>. -As with the `ConfigMap` property sources, first you need to set `spring.cloud.kubernetes.secrets.fail-fast=true`. -Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath. -Retry behavior of the `Secret` property sources can be configured by setting the `spring.cloud.kubernetes.secrets.retry.*` -properties. - -NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason -and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `Secrets` `PropertySources` -by setting `spring.cloud.kubernetes.secrets.retry.enabled=false`. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.secrets.enabled` | `Boolean` | `true` | Enable Secrets `PropertySource` -| `spring.cloud.kubernetes.secrets.name` | `String` | `${spring.application.name}` | Sets the name of the secret to look up -| `spring.cloud.kubernetes.secrets.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to look up -| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` | Sets the labels used to lookup secrets -| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` | Sets the paths where secrets are mounted (example 1) -| `spring.cloud.kubernetes.secrets.enableApi` | `Boolean` | `false` | Enables or disables consuming secrets through APIs (examples 2 and 3) -| `spring.cloud.kubernetes.secrets.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `Secret` -| `spring.cloud.kubernetes.secrets.retry.enabled` | `Boolean` | `true` | Enable or disable secrets retry. -| `spring.cloud.kubernetes.secrets.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds. -| `spring.cloud.kubernetes.secrets.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts. -| `spring.cloud.kubernetes.secrets.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. -| `spring.cloud.kubernetes.secrets.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval. -|=== - -Notes: - -* The `spring.cloud.kubernetes.secrets.labels` property behaves as defined by -https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#map-based-binding[Map-based binding]. -* The `spring.cloud.kubernetes.secrets.paths` property behaves as defined by -https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#collection-based-binding[Collection-based binding]. -* Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod. - -You can find an example of an application that uses secrets (though it has not been updated to use the new `spring-cloud-kubernetes` project) at -https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-camel-config] - -[[namespace-resolution]] -=== Namespace resolution -Finding an application namespace happens on a best-effort basis. There are some steps that we iterate in order -to find it. The easiest and most common one, is to specify it in the proper configuration, for example: - -==== -[source,yaml] ----- -spring: - application: - name: app - cloud: - kubernetes: - secrets: - name: secret - namespace: default - sources: - # Spring Cloud Kubernetes looks up a Secret named 'a' in namespace 'default' - - name: a - # Spring Cloud Kubernetes looks up a Secret named 'secret' in namespace 'b' - - namespace: b - # Spring Cloud Kubernetes looks up a Secret named 'd' in namespace 'c' - - namespace: c - name: d ----- -==== - -Remember that the same can be done for config maps. If such a namespace is not specified, it will be read (in this order): - -1. from property `spring.cloud.kubernetes.client.namespace` -2. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath` property -3. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file -(kubernetes default namespace path) -4. from a designated client method call (for example fabric8's : `KubernetesClient::getNamespace`), if the client provides -such a method. This, in turn, could be configured via environment properties. For example fabric8 client can be configured via -"KUBERNETES_NAMESPACE" property; consult the client documentation for exact details. - -Failure to find a namespace from the above steps will result in an Exception being raised. - -[[order_of_configMaps_and_secrets]] -=== Order of ConfigMaps and Secrets - -If, for whatever reason, you enabled both configmaps and secrets, and there is a common property between them, the value from the ConfigMap will have a higher precedence. That is: it will override whatever values are found in secrets. - -=== `PropertySource` Reload - -WARNING: This functionality has been deprecated in the 2020.0 release. Please see -the <> controller for an alternative way -to achieve the same functionality. - -Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. -The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or -`Secret` changes. - -By default, this feature is disabled. You can enable it by using the `spring.cloud.kubernetes.reload.enabled=true` configuration property (for example, in the `application.properties` file). -Please notice that this will enable monitoring of configmaps only (i.e.: `spring.cloud.kubernetes.reload.monitoring-config-maps` will be set to `true`). -If you want to enable monitoring of secrets, this must be done explicitly via : `spring.cloud.kubernetes.reload.monitoring-secrets=true`. - -The following levels of reload are supported (by setting the `spring.cloud.kubernetes.reload.strategy` property): - -* `refresh` (default): Only configuration beans annotated with `@ConfigurationProperties` or `@RefreshScope` are reloaded. -This reload level leverages the refresh feature of Spring Cloud Context. - -* `restart_context`: the whole Spring `ApplicationContext` is gracefully restarted. Beans are recreated with the new configuration. -In order for the restart context functionality to work properly you must enable and expose the restart actuator endpoint -[source,yaml] -==== ----- -management: - endpoint: - restart: - enabled: true - endpoints: - web: - exposure: - include: restart ----- -==== - -* `shutdown`: the Spring `ApplicationContext` is shut down to activate a restart of the container. - When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the `ApplicationContext` -and that a replication controller or replica set is configured to restart the pod. - -Assuming that the reload feature is enabled with default settings (`refresh` mode), the following bean is refreshed when the config map changes: - -==== -[java, source] ----- -@Configuration -@ConfigurationProperties(prefix = "bean") -public class MyConfig { - - private String message = "a message that can be changed live"; - - // getter and setters - -} ----- -==== - -To see that changes effectively happen, you can create another bean that prints the message periodically, as follows - -==== -[source,java] ----- -@Component -public class MyBean { - - @Autowired - private MyConfig config; - - @Scheduled(fixedDelay = 5000) - public void hello() { - System.out.println("The message is: " + config.getMessage()); - } -} ----- -==== - -You can change the message printed by the application by using a `ConfigMap`, as follows: - -==== -[source,yaml] ----- -apiVersion: v1 -kind: ConfigMap -metadata: - name: reload-example -data: - application.properties: |- - bean.message=Hello World! ----- -==== - -Any change to the property named `bean.message` in the `ConfigMap` associated with the pod is reflected in the -output. More generally speaking, changes associated to properties prefixed with the value defined by the `prefix` -field of the `@ConfigurationProperties` annotation are detected and reflected in the application. -<> is explained earlier in this chapter. - -The reload feature supports two operating modes: - -* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). -Any event produces a re-check on the configuration and, in case of changes, a reload. -The `view` role on the service account is required in order to listen for config map changes. A higher level role (such as `edit`) is required for secrets -(by default, secrets are not monitored). -* Polling: Periodically re-creates the configuration from config maps and secrets to see if it has changed. -You can configure the polling period by using the `spring.cloud.kubernetes.reload.period` property and defaults to 15 seconds. -It requires the same role as the monitored property source. -This means, for example, that using polling on file-mounted secret sources does not require particular privileges. - -[[namespace-label-filtering]] -=== Reload namespace and label filtering -By default, a namespace chosen using the steps outlined in <> will be used to listen to changes -in configmaps and secrets. i.e.: if you do not tell reload what namespaces and configmaps/secrets to watch for, -it will watch all configmaps/secrets from the namespace that will be computed using the above algorithm. - -On the other hand, you can define a more fine-grained approach. For example, you can specify the namespaces where -changes will be monitored: - -==== -[source,yaml] ----- -spring: - application: - name: event-reload - cloud: - kubernetes: - reload: - enabled: true - strategy: shutdown - mode: event - namespaces: - - my-namespace ----- -==== - -Such a configuration will make the app watch changes only in the `my-namespace` namespace. Mind that this will -watch _all_ configmaps/secrets (depending on which one you enable). If you want an even more fine-grained approach, -you can enable "label-filtering". First we need to enable such support via : `enable-reload-filtering: true` - -==== -[source,yaml] ----- -spring: - application: - name: event-reload - cloud: - kubernetes: - reload: - enabled: true - strategy: shutdown - mode: event - namespaces: - - my-namespaces - monitoring-config-maps: true - enable-reload-filtering: true ----- -==== - -What this will do, is watch configmaps/secrets that only have the `spring.cloud.kubernetes.config.informer.enabled: true` label. - -.Properties: -[options="header,footer"] -|=== -| Name | Type | Default | Description -| `spring.cloud.kubernetes.reload.enabled` | `Boolean` | `false` | Enables monitoring of property sources and configuration reload -| `spring.cloud.kubernetes.reload.monitoring-config-maps` | `Boolean` | `true` | Allow monitoring changes in config maps -| `spring.cloud.kubernetes.reload.monitoring-secrets` | `Boolean` | `false` | Allow monitoring changes in secrets -| `spring.cloud.kubernetes.reload.strategy` | `Enum` | `refresh` | The strategy to use when firing a reload (`refresh`, `restart_context`, or `shutdown`) -| `spring.cloud.kubernetes.reload.mode` | `Enum` | `event` | Specifies how to listen for changes in property sources (`event` or `polling`) -| `spring.cloud.kubernetes.reload.period` | `Duration`| `15s` | The period for verifying changes when using the `polling` strategy -| `spring.cloud.kubernetes.reload.namespaces` | `String[]`| | namespaces where we should watch for changes -| `spring.cloud.kubernetes.reload.enable-reload-filtering` | `String` | | enabled labeled filtering for reload functionality -|=== - -Notes: - -* You should not use properties under `spring.cloud.kubernetes.reload` in config maps or secrets. Changing such properties at runtime may lead to unexpected results. -* Deleting a property or the whole config map does not restore the original state of the beans when you use the `refresh` level. diff --git a/docs/src/main/asciidoc/sagan-boot.adoc b/docs/src/main/asciidoc/sagan-boot.adoc deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/src/main/asciidoc/sagan-index.adoc b/docs/src/main/asciidoc/sagan-index.adoc deleted file mode 100644 index a7f394a780..0000000000 --- a/docs/src/main/asciidoc/sagan-index.adoc +++ /dev/null @@ -1,12 +0,0 @@ -Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. -The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes. - - -## Features - -* Kubernetes awareness -* `DiscoveryClient` implementation -* `PropertySource` objects configured via ConfigMaps - -## Getting Started -The easiest way to get started is by including the Spring Cloud BOM and then adding `spring-cloud-starter-kubernetes-client-all` to your application's classpath. If you don't want to include all of the Spring Cloud Kubernetes features you can add individual starters for the features you would like. By default Spring Cloud Kubernetes will enable the `kubernetes` profile when it detects it is running inside a Kubernetes cluster. You can take advantage of this by creating a `kubernetes-application` configuration properties for anything specific to Kubernetes you might want to configure. Once the starter is on the classpath the application should behave as any other Spring Cloud application. diff --git a/pom.xml b/pom.xml index e17b33d4ec..97bc416d38 100644 --- a/pom.xml +++ b/pom.xml @@ -25,12 +25,12 @@ org.springframework.cloud spring-cloud-build - 4.1.0-SNAPSHOT + 4.1.1-SNAPSHOT spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT pom Spring Cloud Kubernetes @@ -62,14 +62,18 @@ + + + + + surefire-reports + failsafe-reports 4.8.1 - 4.1.0-SNAPSHOT - 4.1.0-SNAPSHOT - 4.1.0-SNAPSHOT - 4.1.0-SNAPSHOT - - 3.1.0 + 4.1.2-SNAPSHOT + 4.1.1-SNAPSHOT + 4.1.1-SNAPSHOT + 4.1.1-SNAPSHOT true true @@ -205,8 +209,18 @@ ${testsToRun} + ${project.build.directory}/${surefire-reports-directory} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${project.build.directory}/${failsafe-reports-directory} + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index 4d5fa33886..e3990fe6b4 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash set -e +rm ~/.testcontainers.properties +echo 'testcontainers.reuse.enable=true' > ~/.testcontainers.properties + ./mvnw clean install -B -Pdocs ${@} +rm ~/.testcontainers.properties +docker kill $(docker ps -q) diff --git a/spring-cloud-kubernetes-client-autoconfig/pom.xml b/spring-cloud-kubernetes-client-autoconfig/pom.xml index ed1dc54104..a9d63bd51d 100644 --- a/spring-cloud-kubernetes-client-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-client-autoconfig/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientActuatorConfiguration.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientActuatorConfiguration.java index eae0776446..82f9f67fdc 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientActuatorConfiguration.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientActuatorConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,8 @@ import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.cloud.CloudPlatform; import org.springframework.cloud.kubernetes.commons.PodUtils; +import org.springframework.cloud.kubernetes.commons.autoconfig.ConditionalOnKubernetesHealthIndicatorEnabled; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,8 +29,7 @@ * @author wind57 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(HealthIndicator.class) -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnKubernetesHealthIndicatorEnabled public class KubernetesClientActuatorConfiguration { @Bean diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java index 26c5c3a4d5..e58107464d 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java @@ -73,7 +73,7 @@ public KubernetesNamespaceProvider kubernetesNamespaceProvider(Environment envir @ConditionalOnMissingBean public KubernetesClientPodUtils kubernetesPodUtils(CoreV1Api client, KubernetesNamespaceProvider kubernetesNamespaceProvider) { - return new KubernetesClientPodUtils(client, kubernetesNamespaceProvider.getNamespace()); + return new KubernetesClientPodUtils(client, kubernetesNamespaceProvider.getNamespace(), true); } } diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientHealthIndicator.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientHealthIndicator.java index 7a2490b18a..26506fdf8b 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientHealthIndicator.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientHealthIndicator.java @@ -41,7 +41,7 @@ public KubernetesClientHealthIndicator(PodUtils utils) { @Override protected Map getDetails() { - V1Pod current = this.utils.currentPod().get(); + V1Pod current = utils.currentPod().get(); if (current != null) { Map details = CollectionUtils.newHashMap(8); details.put(INSIDE, true); diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientInfoContributor.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientInfoContributor.java index 4d9d5e687f..6cd0c8053e 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientInfoContributor.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientInfoContributor.java @@ -41,7 +41,7 @@ public KubernetesClientInfoContributor(PodUtils utils) { @Override public Map getDetails() { - V1Pod current = this.utils.currentPod().get(); + V1Pod current = utils.currentPod().get(); if (current != null) { Map details = CollectionUtils.newHashMap(7); details.put(INSIDE, true); diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java index f50394db19..a87fd61409 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java @@ -58,6 +58,9 @@ public class KubernetesClientPodUtils implements PodUtils { private final String serviceHost; + private final boolean failFast; + + @Deprecated(forRemoval = true) public KubernetesClientPodUtils(CoreV1Api client, String namespace) { if (client == null) { throw new IllegalArgumentException("Must provide an instance of KubernetesClient"); @@ -68,6 +71,22 @@ public KubernetesClientPodUtils(CoreV1Api client, String namespace) { this.serviceHost = EnvReader.getEnv(KUBERNETES_SERVICE_HOST); this.current = LazilyInstantiate.using(this::internalGetPod); this.namespace = namespace; + this.failFast = false; + } + + // mainly needed for the health and info contributors, so that they report DOWN + // correctly + public KubernetesClientPodUtils(CoreV1Api client, String namespace, boolean failFast) { + if (client == null) { + throw new IllegalArgumentException("Must provide an instance of KubernetesClient"); + } + + this.client = client; + this.hostName = EnvReader.getEnv(HOSTNAME); + this.serviceHost = EnvReader.getEnv(KUBERNETES_SERVICE_HOST); + this.current = LazilyInstantiate.using(this::internalGetPod); + this.namespace = namespace; + this.failFast = failFast; } @Override @@ -84,10 +103,14 @@ private V1Pod internalGetPod() { try { if (isServiceHostEnvVarPresent() && isHostNameEnvVarPresent() && isServiceAccountFound()) { LOG.debug("reading pod in namespace : " + namespace); + // The hostname of your pod is typically also its name. return client.readNamespacedPod(hostName, namespace, null); } } catch (Throwable t) { + if (failFast) { + throw new RuntimeException(t); + } if (t instanceof ApiException apiException) { LOG.warn("error reading pod, with error : " + apiException.getResponseBody()); } diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientUtils.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientUtils.java index b931e07dd6..35dff0f136 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientUtils.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientUtils.java @@ -49,8 +49,16 @@ public static ApiClient kubernetesApiClient() { return apiClient; } catch (Exception e) { - LOG.info("Could not create the Kubernetes ApiClient in a cluster environment, because : ", e); - LOG.info("Trying to use a \"standard\" configuration to create the Kubernetes ApiClient"); + if (e instanceof IllegalStateException illegalStateException + && illegalStateException.getCause() instanceof NumberFormatException) { + LOG.info("Could not create the Kubernetes ApiClient in a cluster environment, because connection port " + + "was not provided."); + } + else { + LOG.info("Could not create the Kubernetes ApiClient in a cluster environment, because : ", e); + } + LOG.info(""" + Trying to use a "standard" configuration to create the Kubernetes ApiClient"""); try { ApiClient apiClient = ClientBuilder.defaultClient(); LOG.info("Created standard API client. Unless $KUBECONFIG or $HOME/.kube/config is defined, " diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/profile/KubernetesClientProfileEnvironmentPostProcessor.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/profile/KubernetesClientProfileEnvironmentPostProcessor.java index 767d082355..78d87e3565 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/profile/KubernetesClientProfileEnvironmentPostProcessor.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/profile/KubernetesClientProfileEnvironmentPostProcessor.java @@ -33,7 +33,8 @@ public class KubernetesClientProfileEnvironmentPostProcessor extends AbstractKub @Override protected boolean isInsideKubernetes(Environment environment) { CoreV1Api api = new CoreV1Api(); - KubernetesClientPodUtils utils = new KubernetesClientPodUtils(api, environment.getProperty(NAMESPACE_PROPERTY)); + KubernetesClientPodUtils utils = new KubernetesClientPodUtils(api, environment.getProperty(NAMESPACE_PROPERTY), + false); return environment.containsProperty(ENV_SERVICE_HOST) || utils.isInsideKubernetes(); } diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledFailFastExceptionTest.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledFailFastExceptionTest.java new file mode 100644 index 0000000000..b851245b05 --- /dev/null +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledFailFastExceptionTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.util.Config; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.example.App; +import org.springframework.cloud.kubernetes.commons.EnvReader; +import org.springframework.context.annotation.Bean; + +/** + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { App.class, ActuatorEnabledFailFastExceptionTest.ActuatorConfig.class }, + properties = { "management.endpoint.health.show-details=always", + "management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health", + "spring.main.cloud-platform=KUBERNETES" }) +class ActuatorEnabledFailFastExceptionTest { + + private static final boolean FAIL_FAST = true; + + private static MockedStatic envReaderMockedStatic; + + private static MockedStatic pathsMockedStatic; + + private static final CoreV1Api coreV1Api = Mockito.mock(CoreV1Api.class); + + @Autowired + private KubernetesClientHealthIndicator healthIndicator; + + @AfterEach + void afterEach() { + envReaderMockedStatic.close(); + pathsMockedStatic.close(); + } + + @Test + void test() throws ApiException { + Health health = healthIndicator.getHealth(true); + Assertions.assertEquals(health.getStatus(), Status.DOWN); + Mockito.verify(coreV1Api).readNamespacedPod("host", "my-namespace", null); + } + + private static void mocks() { + envReaderMockedStatic = Mockito.mockStatic(EnvReader.class); + pathsMockedStatic = Mockito.mockStatic(Paths.class); + + envReaderMockedStatic.when(() -> EnvReader.getEnv(KubernetesClientPodUtils.KUBERNETES_SERVICE_HOST)) + .thenReturn("k8s-host"); + envReaderMockedStatic.when(() -> EnvReader.getEnv(KubernetesClientPodUtils.HOSTNAME)).thenReturn("host"); + + Path serviceAccountTokenPath = Mockito.mock(Path.class); + File serviceAccountTokenFile = Mockito.mock(File.class); + Mockito.when(serviceAccountTokenPath.toFile()).thenReturn(serviceAccountTokenFile); + Mockito.when(serviceAccountTokenFile.exists()).thenReturn(true); + pathsMockedStatic.when(() -> Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH)).thenReturn(serviceAccountTokenPath); + + Path serviceAccountCAPath = Mockito.mock(Path.class); + File serviceAccountCAFile = Mockito.mock(File.class); + Mockito.when(serviceAccountCAPath.toFile()).thenReturn(serviceAccountCAFile); + Mockito.when(serviceAccountCAFile.exists()).thenReturn(true); + pathsMockedStatic.when(() -> Paths.get(Config.SERVICEACCOUNT_CA_PATH)).thenReturn(serviceAccountCAPath); + } + + @TestConfiguration + static class ActuatorConfig { + + // will be created "instead" of + // KubernetesClientAutoConfiguration::kubernetesPodUtils + @Bean + KubernetesClientPodUtils kubernetesPodUtils() throws ApiException { + + mocks(); + + Mockito.when(coreV1Api.readNamespacedPod("host", "my-namespace", null)) + .thenThrow(new RuntimeException("just because")); + + return new KubernetesClientPodUtils(coreV1Api, "my-namespace", FAIL_FAST); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledNoFailFastExceptionTest.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledNoFailFastExceptionTest.java new file mode 100644 index 0000000000..f6bba80d3e --- /dev/null +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/ActuatorEnabledNoFailFastExceptionTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.util.Config; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.example.App; +import org.springframework.cloud.kubernetes.commons.EnvReader; +import org.springframework.context.annotation.Bean; + +/** + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { App.class, ActuatorEnabledNoFailFastExceptionTest.ActuatorConfig.class }, + properties = { "management.endpoint.health.show-details=always", + "management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health", + "spring.main.cloud-platform=KUBERNETES" }) + +class ActuatorEnabledNoFailFastExceptionTest { + + private static final boolean FAIL_FAST = false; + + private static MockedStatic envReaderMockedStatic; + + private static MockedStatic pathsMockedStatic; + + private static final CoreV1Api coreV1Api = Mockito.mock(CoreV1Api.class); + + @Autowired + private KubernetesClientHealthIndicator healthIndicator; + + @AfterEach + void afterEach() { + envReaderMockedStatic.close(); + pathsMockedStatic.close(); + } + + // without a fail-fast, we would not fail and actuator would return "UP" + // This is not a real case we have, it just makes sure + @Test + void test() throws ApiException { + Health health = healthIndicator.getHealth(true); + Assertions.assertEquals(health.getStatus(), Status.UP); + Mockito.verify(coreV1Api).readNamespacedPod("host", "my-namespace", null); + } + + private static void mocks() { + envReaderMockedStatic = Mockito.mockStatic(EnvReader.class); + pathsMockedStatic = Mockito.mockStatic(Paths.class); + + envReaderMockedStatic.when(() -> EnvReader.getEnv(KubernetesClientPodUtils.KUBERNETES_SERVICE_HOST)) + .thenReturn("k8s-host"); + envReaderMockedStatic.when(() -> EnvReader.getEnv(KubernetesClientPodUtils.HOSTNAME)).thenReturn("host"); + + Path serviceAccountTokenPath = Mockito.mock(Path.class); + File serviceAccountTokenFile = Mockito.mock(File.class); + Mockito.when(serviceAccountTokenPath.toFile()).thenReturn(serviceAccountTokenFile); + Mockito.when(serviceAccountTokenFile.exists()).thenReturn(true); + pathsMockedStatic.when(() -> Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH)).thenReturn(serviceAccountTokenPath); + + Path serviceAccountCAPath = Mockito.mock(Path.class); + File serviceAccountCAFile = Mockito.mock(File.class); + Mockito.when(serviceAccountCAPath.toFile()).thenReturn(serviceAccountCAFile); + Mockito.when(serviceAccountCAFile.exists()).thenReturn(true); + pathsMockedStatic.when(() -> Paths.get(Config.SERVICEACCOUNT_CA_PATH)).thenReturn(serviceAccountCAPath); + } + + @TestConfiguration + static class ActuatorConfig { + + // will be created "instead" of + // KubernetesClientAutoConfiguration::kubernetesPodUtils + @Bean + KubernetesClientPodUtils kubernetesPodUtils() throws ApiException { + + mocks(); + + Mockito.when(coreV1Api.readNamespacedPod("host", "my-namespace", null)) + .thenThrow(new RuntimeException("just because")); + + return new KubernetesClientPodUtils(coreV1Api, "my-namespace", FAIL_FAST); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java index a81bbefb5a..e97b901d23 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java @@ -39,7 +39,7 @@ /** * @author wind57 */ -public class KubernetesClientPodUtilsTests { +class KubernetesClientPodUtilsTests { private static final String KUBERNETES_SERVICE_HOST = KubernetesClientPodUtils.KUBERNETES_SERVICE_HOST; @@ -70,73 +70,73 @@ public class KubernetesClientPodUtilsTests { private MockedStatic paths; @BeforeEach - public void before() { + void before() { envReader = Mockito.mockStatic(EnvReader.class); paths = Mockito.mockStatic(Paths.class); } @AfterEach - public void after() { + void after() { envReader.close(); paths.close(); } @Test - public void constructorThrowsIllegalArgumentExceptionWhenKubeClientIsNull() { - assertThatThrownBy(() -> new KubernetesClientPodUtils(null, "namespace")) + void constructorThrowsIllegalArgumentExceptionWhenKubeClientIsNull() { + assertThatThrownBy(() -> new KubernetesClientPodUtils(null, "namespace", false)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Must provide an instance of KubernetesClient"); } @Test - public void serviceHostNotPresent() { + void serviceHostNotPresent() { mockHost(null); - KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace", false); Supplier sup = util.currentPod(); assertSupplierAndClient(sup, util); } @Test - public void hostNameNotPresent() { + void hostNameNotPresent() { mockHost(HOST); mockHostname(null); - KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace", false); Supplier sup = util.currentPod(); assertSupplierAndClient(sup, util); } @Test - public void serviceAccountPathNotPresent() { + void serviceAccountPathNotPresent() { mockTokenPath(false); mockHostname(HOST); - KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace", false); Supplier sup = util.currentPod(); assertSupplierAndClient(sup, util); } @Test - public void serviceAccountCertPathNotPresent() { + void serviceAccountCertPathNotPresent() { mockTokenPath(true); mockCertPath(false); mockHostname(HOST); - KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace", false); Supplier sup = util.currentPod(); assertSupplierAndClient(sup, util); } @Test - public void allPresent() throws ApiException { + void allPresent() throws ApiException { mockTokenPath(true); mockCertPath(true); mockHost(HOST); mockHostname(POD_HOSTNAME); mockPodResult(); - KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace", false); Supplier sup = util.currentPod(); assertThat(sup.get()).isNotNull(); assertThat(util.isInsideKubernetes()).isTrue(); diff --git a/spring-cloud-kubernetes-client-config/pom.xml b/spring-cloud-kubernetes-client-config/pom.xml index 80a8d7dbed..8e0f926015 100644 --- a/spring-cloud-kubernetes-client-config/pom.xml +++ b/spring-cloud-kubernetes-client-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java index 1b6a6c5bfb..488b09cf91 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java @@ -55,9 +55,8 @@ static List byNamespace(CoreV1Api coreV1Api, String nam List result = CACHE.computeIfAbsent(namespace, x -> { try { b[0] = true; - return strippedConfigMaps(coreV1Api - .listNamespacedConfigMap(namespace, null, null, null, null, null, null, null, null, null, null) - .getItems()); + return strippedConfigMaps(coreV1Api.listNamespacedConfigMap(namespace, null, null, null, null, null, + null, null, null, null, null, null).getItems()); } catch (ApiException apiException) { throw new RuntimeException(apiException.getResponseBody(), apiException); diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java index 8381d9a410..7544408647 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java @@ -57,9 +57,8 @@ static List byNamespace(CoreV1Api coreV1Api, String nam List result = CACHE.computeIfAbsent(namespace, x -> { try { b[0] = true; - return strippedSecrets(coreV1Api - .listNamespacedSecret(namespace, null, null, null, null, null, null, null, null, null, null) - .getItems()); + return strippedSecrets(coreV1Api.listNamespacedSecret(namespace, null, null, null, null, null, null, + null, null, null, null, null).getItems()); } catch (ApiException apiException) { throw new RuntimeException(apiException.getResponseBody(), apiException); diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java index f5f36591ea..0581017f69 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java @@ -123,11 +123,12 @@ void inform() { } SharedInformerFactory factory = new SharedInformerFactory(apiClient); factories.add(factory); - informer = factory.sharedIndexInformerFor( - (CallGeneratorParams params) -> coreV1Api.listNamespacedConfigMapCall(namespace, null, null, null, - null, filter[0], null, params.resourceVersion, null, params.timeoutSeconds, params.watch, - null), - V1ConfigMap.class, V1ConfigMapList.class); + informer = factory + .sharedIndexInformerFor( + (CallGeneratorParams params) -> coreV1Api.listNamespacedConfigMapCall(namespace, null, null, + null, null, filter[0], null, params.resourceVersion, null, null, + params.timeoutSeconds, params.watch, null), + V1ConfigMap.class, V1ConfigMapList.class); LOG.debug(() -> "added configmap informer for namespace : " + namespace + " with filter : " + filter[0]); diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java index 2beb30dbdc..f97da12a2b 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java @@ -128,8 +128,8 @@ void inform() { factories.add(factory); informer = factory.sharedIndexInformerFor( (CallGeneratorParams params) -> coreV1Api.listNamespacedSecretCall(namespace, null, null, null, - null, filter[0], null, params.resourceVersion, null, params.timeoutSeconds, params.watch, - null), + null, filter[0], null, params.resourceVersion, null, null, params.timeoutSeconds, + params.watch, null), V1Secret.class, V1SecretList.class); LOG.debug(() -> "added secret informer for namespace : " + namespace + " with filter : " + filter[0]); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolverTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolverTests.java index 3e234007d1..8442cf5591 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolverTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolverTests.java @@ -22,6 +22,7 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.springframework.boot.DefaultBootstrapContext; @@ -31,6 +32,8 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; import org.springframework.cloud.kubernetes.commons.config.ConfigDataRetryableConfigMapPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.config.ConfigDataRetryableSecretsPropertySourceLocator; @@ -43,6 +46,7 @@ /** * @author wind57 */ +@ExtendWith(OutputCaptureExtension.class) class KubernetesClientConfigDataLocationResolverTests { private static final DeferredLogFactory FACTORY = Supplier::get; @@ -142,7 +146,7 @@ void testBothPresent() { * these are not retryable beans. */ @Test - void testBothPresentExplicitly() { + void testBothPresentExplicitly(CapturedOutput capturedOutput) { MockEnvironment environment = new MockEnvironment(); environment.setProperty("spring.cloud.kubernetes.config.enabled", "true"); environment.setProperty("spring.cloud.kubernetes.secrets.enabled", "true"); @@ -172,6 +176,10 @@ void testBothPresentExplicitly() { SecretsPropertySourceLocator secretsPropertySourceLocator = context.get(SecretsPropertySourceLocator.class); Assertions.assertSame(KubernetesClientSecretsPropertySourceLocator.class, secretsPropertySourceLocator.getClass()); + + Assertions.assertTrue(capturedOutput.getOut() + .contains("Could not create the Kubernetes ApiClient in a cluster environment, because connection port " + + "was not provided.")); } /* diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixConfigDataTests.java index c4e0fe404b..3af64c5210 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixConfigDataTests.java @@ -29,7 +29,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithPrefixConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithPrefixConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixTests.java index 647e3cb6c3..b2bd7da8ad 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_prefix/LabeledConfigMapWithPrefixTests.java @@ -26,8 +26,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; /** - * Stud data is in - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithPrefixConfigurationStub} + * Stub data is in + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithPrefixConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileConfigDataTests.java index 4e3991b126..df53725772 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileConfigDataTests.java @@ -30,7 +30,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithProfileConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithProfileConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java index 4a206dab65..a382d020f0 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java @@ -26,8 +26,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; /** - * Stud data is in - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithProfileConfigurationStub} + * Stub data is in + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithProfileConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixConfigDataTests.java index 42cbe38aa6..ce0c8dc358 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixConfigDataTests.java @@ -29,7 +29,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithPrefixConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithPrefixConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixTests.java index 6300f24f2d..8577c1d72f 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_prefix/LabeledSecretWithPrefixTests.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithPrefixConfigurationStub; +import org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithPrefixConfigurationStub; import org.springframework.test.web.reactive.server.WebTestClient; /** diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileConfigDataTests.java index 14cff5d216..50e17abb45 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileConfigDataTests.java @@ -30,7 +30,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithProfileConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithProfileConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java index fbcb546a78..9244434bbb 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java @@ -39,7 +39,7 @@ /** * Stubs for this test are in - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithProfileConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithProfileConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixConfigDataTests.java index 4bd5cdbd85..62ea60ef8b 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixConfigDataTests.java @@ -29,7 +29,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithPrefixConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithPrefixConfigurationStub.stubData; /** * @author Ryan Baxter diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixTests.java index 881b628416..71d4359ea1 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_prefix/NamedConfigMapWithPrefixTests.java @@ -27,7 +27,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithPrefixConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithPrefixConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileApp.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileApp.java index 81db4b8077..5ce6f69d11 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileApp.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileApp.java @@ -25,7 +25,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithProfileConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithProfileConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileConfigDataTests.java index d3c660d3d0..449aa6a28e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileConfigDataTests.java @@ -30,7 +30,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithProfileConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithProfileConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileTests.java index a997b4e037..21a86346ac 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_config_map_with_profile/NamedConfigMapWithProfileTests.java @@ -27,7 +27,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithProfileConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithProfileConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixConfigDataTests.java index bacf92cb44..de518cf1ed 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixConfigDataTests.java @@ -29,7 +29,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithPrefixConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithPrefixConfigurationStub.stubData; /** * @author Ryan Baxter diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixTests.java index 58a6545d2a..68c0de45c1 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_prefix/NamedSecretWithPrefixTests.java @@ -27,7 +27,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithPrefixConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithPrefixConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileConfigDataTests.java index 327af36374..3b036954f2 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileConfigDataTests.java @@ -30,7 +30,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithProfileConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithProfileConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileTests.java index 20093f5dc5..631a2db87a 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/named_secret_with_profile/NamedSecretWithProfileTests.java @@ -27,7 +27,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithProfileConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithProfileConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesConfigDataTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesConfigDataTests.java index fa56b6811b..64fa102f89 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesConfigDataTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesConfigDataTests.java @@ -30,7 +30,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub.stubData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SingleSourceMultipleFilesConfigurationStub.stubData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesTests.java index 6ba97caee4..4dd41e9ab0 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/single_source_multiple_files/SingleSourceMultipleFilesTests.java @@ -29,7 +29,7 @@ * @author wind57 * * Stub for this test is here : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SingleSourceMultipleFilesConfigurationStub} * * issue: https://github.com/spring-cloud/spring-cloud-kubernetes/issues/640 * diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataRetryableSourcesOrderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataRetryableSourcesOrderTests.java index f8c23716bb..59e075dd89 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataRetryableSourcesOrderTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataRetryableSourcesOrderTests.java @@ -32,12 +32,12 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub.stubConfigMapData; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub.stubSecretsData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub.stubConfigMapData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub.stubSecretsData; /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataSourcesOrderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataSourcesOrderTests.java index 1aea3d0290..7b32c3ef98 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataSourcesOrderTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/ConfigDataSourcesOrderTests.java @@ -29,8 +29,8 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.mockito.Mockito.mockStatic; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub.stubConfigMapData; -import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub.stubSecretsData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub.stubConfigMapData; +import static org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub.stubSecretsData; /** * @author wind57 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java index 7e7d59f2ba..a32ede6fa0 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java @@ -31,7 +31,7 @@ /** * The stub data for this test is in : - * {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub} + * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub} * * @author wind57 */ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/BootstrapKubernetesClientSanitizeEnvEndpointStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/BootstrapKubernetesClientSanitizeEnvEndpointStub.java new file mode 100644 index 0000000000..872120e04b --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/BootstrapKubernetesClientSanitizeEnvEndpointStub.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.ClientBuilder; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * A test bootstrap that takes care to initialize ApiClient _before_ our main bootstrap + * context; with some stub data already present. + * + * @author wind57 + */ +@Order(0) +@Configuration +@ConditionalOnProperty("bootstrap.sanitize") +public class BootstrapKubernetesClientSanitizeEnvEndpointStub { + + @Bean + public WireMockServer wireMock() { + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + return server; + } + + @Bean + public ApiClient apiClient(WireMockServer wireMockServer) { + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + io.kubernetes.client.openapi.Configuration.setDefaultApiClient(apiClient); + apiClient.setDebugging(true); + stubData(); + return apiClient; + } + + public static void stubData() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("sanitize-configmap").withNamespace("test").build()) + .addToData(Map.of("sanitize.sanitizeConfigMapName", "sanitizeConfigMapValue")).build(); + + V1Secret secretOne = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("sanitize-secret").withNamespace("test").build()) + .addToData(Map.of("sanitize.sanitizeSecretName", "sanitizeSecretValue".getBytes())).build(); + + V1Secret secretTwo = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("sanitize-secret-two").withNamespace("test").build()) + .addToData(Map.of("sanitize.sanitizeSecretNameTwo", "sanitizeSecretValueTwo".getBytes())).build(); + + // the actual stub for CoreV1Api calls + V1ConfigMapList configMapList = new V1ConfigMapList(); + configMapList.addItemsItem(one); + + V1SecretList secretList = new V1SecretList(); + secretList.addItemsItem(secretOne); + secretList.addItemsItem(secretTwo); + + WireMock.stubFor(WireMock.get("/api/v1/namespaces/test/configmaps") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(configMapList)))); + + WireMock.stubFor(WireMock.get("/api/v1/namespaces/test/secrets") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(secretList)))); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java index c55e19164a..cce9bdd9c1 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithPrefixConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Collections; import java.util.Map; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java index 09205f36e6..a0a2025bd5 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Collections; import java.util.Map; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithPrefixConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithPrefixConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithPrefixConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithPrefixConfigurationStub.java index ea30c86b88..f5008b7363 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithPrefixConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithPrefixConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Collections; import java.util.Map; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithProfileConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java index 813d37d54e..34b5632ef8 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/LabeledSecretWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.nio.charset.StandardCharsets; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java similarity index 97% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java index c021349cd1..aafb86d4a3 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithPrefixConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Arrays; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithProfileConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithProfileConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithProfileConfigurationStub.java index 8e2147c9ef..fdf2ad6d6b 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedConfigMapWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedConfigMapWithProfileConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Arrays; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithPrefixConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithPrefixConfigurationStub.java similarity index 97% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithPrefixConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithPrefixConfigurationStub.java index dc8ab3dfd5..a6e41f64d8 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithPrefixConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithPrefixConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Arrays; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithProfileConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithProfileConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithProfileConfigurationStub.java index b1e1a19542..3630a5f798 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/NamedSecretWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/NamedSecretWithProfileConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.Arrays; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SingleSourceMultipleFilesConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SingleSourceMultipleFilesConfigurationStub.java similarity index 97% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SingleSourceMultipleFilesConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SingleSourceMultipleFilesConfigurationStub.java index e92948d747..c62d8a0164 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SingleSourceMultipleFilesConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SingleSourceMultipleFilesConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.util.HashMap; import java.util.Map; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SourcesOrderConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SourcesOrderConfigurationStub.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SourcesOrderConfigurationStub.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SourcesOrderConfigurationStub.java index 9b6bd8587a..8f04ab4487 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/SourcesOrderConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/SourcesOrderConfigurationStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap.stubs; +package org.springframework.cloud.kubernetes.client.config.bootstrap.stubs; import java.nio.charset.StandardCharsets; import java.util.Collections; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationInsideK8s.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationInsideK8s.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationInsideK8s.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationInsideK8s.java index 898d664fa9..7734050bad 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationInsideK8s.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationInsideK8s.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationNotInsideK8s.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationNotInsideK8s.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationNotInsideK8s.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationNotInsideK8s.java index b84ef3c7e5..8b9e1e8da3 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesClientBootstrapConfigurationNotInsideK8s.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesClientBootstrapConfigurationNotInsideK8s.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesDisabled.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesDisabled.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesDisabled.java index bfe295c45d..c3c26557d3 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesDisabled.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabled.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabled.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabled.java index fc43330fe9..0978c87371 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabled.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledConfigDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledConfigDisabled.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledConfigDisabled.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledConfigDisabled.java index e8ee40b554..298e3ec55e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledConfigDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledConfigDisabled.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledOnPurpose.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledOnPurpose.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledOnPurpose.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledOnPurpose.java index 821d15e2c1..8dcb00b509 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledOnPurpose.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledOnPurpose.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsAndConfigDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsAndConfigDisabled.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsAndConfigDisabled.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsAndConfigDisabled.java index 52c8f9a515..01745a0312 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsAndConfigDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsAndConfigDisabled.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsDisabled.java similarity index 95% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsDisabled.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsDisabled.java index db268a9683..320e883277 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap_configuration/KubernetesEnabledSecretsDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap_configurations/KubernetesEnabledSecretsDisabled.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.config.boostrap_configuration; +package org.springframework.cloud.kubernetes.client.config.bootstrap_configurations; import org.junit.jupiter.api.Test; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeConfigpropsEndpointTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeConfigpropsEndpointTests.java new file mode 100644 index 0000000000..585747b5a5 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeConfigpropsEndpointTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class BootstrapKubernetesClientSanitizeConfigpropsEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "bootstrap.sanitize=true", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class DefaultSettingsTest { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.configprops.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.configprops.show-values=NEVER", "bootstrap.sanitize=true", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class ExplicitNever { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=false", "bootstrap.sanitize=true", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithoutSanitizingFunction { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize-two", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=true", "bootstrap.sanitize=true", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithSanitizingFunction { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretNameTwo") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeEnvEndpointTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeEnvEndpointTests.java new file mode 100644 index 0000000000..98fca219b4 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/BootstrapKubernetesClientSanitizeEnvEndpointTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class BootstrapKubernetesClientSanitizeEnvEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "bootstrap.sanitize=true", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class DefaultSettingsTest { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.env.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.env.show-values=NEVER", "bootstrap.sanitize=true", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class ExplicitNever { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=false", + "bootstrap.sanitize=true", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithoutSanitizingFunction { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize-two", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=true", + "bootstrap.sanitize=true", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithSanitizingFunction { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretNameTwo'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java new file mode 100644 index 0000000000..c1100c04ce --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class ConfigDataFabric8ConfigpropsEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @Nested + class DefaultSettingsTest extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.configprops.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=NEVER", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @Nested + class ExplicitNever extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=false", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @Nested + class AlwaysWithoutSanitizingFunction extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=true", + "spring.config.import=kubernetes:,classpath:./sanitize-two.yaml" }) + @Nested + class AlwaysWithSanitizingFunction extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretNameTwo") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataKubernetesClientSanitizeEnvEndpointTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataKubernetesClientSanitizeEnvEndpointTests.java new file mode 100644 index 0000000000..07156c3f1f --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataKubernetesClientSanitizeEnvEndpointTests.java @@ -0,0 +1,215 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class ConfigDataKubernetesClientSanitizeEnvEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoints.web.exposure.include=*", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class DefaultSettingsTest extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.env.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoint.env.show-values=NEVER", "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class ExplicitNever extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=false", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithoutSanitizingFunction extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize-two.yaml", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=true", + "spring.cloud.kubernetes.client.namespace=test" }) + @Nested + class AlwaysWithSanitizingFunction extends ConfigDataSanitize { + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretNameTwo'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataSanitize.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataSanitize.java new file mode 100644 index 0000000000..974e5ae181 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/ConfigDataSanitize.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.BootstrapKubernetesClientSanitizeEnvEndpointStub.stubData; + +/** + * @author wind57 + */ +abstract class ConfigDataSanitize { + + private static MockedStatic clientUtilsMock; + + @BeforeAll + static void wireMock() { + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + clientUtilsMock = mockStatic(KubernetesClientUtils.class); + clientUtilsMock.when(KubernetesClientUtils::kubernetesApiClient) + .thenReturn(new ClientBuilder().setBasePath(server.baseUrl()).build()); + clientUtilsMock + .when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn("test"); + stubData(); + } + + @AfterAll + static void teardown() { + clientUtilsMock.close(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapApp.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeApp.java similarity index 81% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapApp.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeApp.java index 08f7065777..22b88146e2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapApp.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeApp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.polling.reload; +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -23,13 +23,12 @@ /** * @author wind57 */ - +@EnableConfigurationProperties(SanitizeProperties.class) @SpringBootApplication -@EnableConfigurationProperties(ConfigMapProperties.class) -public class ConfigMapApp { +class SanitizeApp { public static void main(String[] args) { - SpringApplication.run(ConfigMapApp.class, args); + SpringApplication.run(SanitizeApp.class, args); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapController.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeController.java similarity index 62% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapController.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeController.java index 7d8cbedfde..37d63a392e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapController.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.polling.reload; +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,17 +23,22 @@ * @author wind57 */ @RestController -public class ConfigMapController { +class SanitizeController { - private final ConfigMapProperties properties; + private final SanitizeProperties sanitizeProperties; - public ConfigMapController(ConfigMapProperties properties) { - this.properties = properties; + SanitizeController(SanitizeProperties sanitizeProperties) { + this.sanitizeProperties = sanitizeProperties; } - @GetMapping("/key") - public String key() { - return properties.getKey(); + @GetMapping("/secret") + String secret() { + return sanitizeProperties.getSanitizeSecretName(); + } + + @GetMapping("/configmap") + String configmap() { + return sanitizeProperties.getSanitizeConfigMapName(); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeProperties.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeProperties.java new file mode 100644 index 0000000000..2ad2eaf224 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/sanitize_secrets/SanitizeProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config.sanitize_secrets; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author wind57 + */ +@ConfigurationProperties("sanitize") +class SanitizeProperties { + + private String sanitizeSecretName; + + private String sanitizeSecretNameTwo; + + private String sanitizeConfigMapName; + + public String getSanitizeSecretName() { + return sanitizeSecretName; + } + + public void setSanitizeSecretName(String sanitizeSecretName) { + this.sanitizeSecretName = sanitizeSecretName; + } + + public String getSanitizeConfigMapName() { + return sanitizeConfigMapName; + } + + public void setSanitizeConfigMapName(String sanitizeConfigMapName) { + this.sanitizeConfigMapName = sanitizeConfigMapName; + } + + public String getSanitizeSecretNameTwo() { + return sanitizeSecretNameTwo; + } + + public void setSanitizeSecretNameTwo(String sanitizeSecretNameTwo) { + this.sanitizeSecretNameTwo = sanitizeSecretNameTwo; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories index 00abb43956..f453b1b3bd 100644 --- a/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories @@ -1,14 +1,15 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithProfileConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithPrefixConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithPrefixConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithProfileConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithPrefixConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithProfileConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithPrefixConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithProfileConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.IncludeProfileSpecificSourcesConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.ConfigMapNameAsPrefixConfigurationStub, \ -org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SourcesOrderConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithProfileConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedConfigMapWithPrefixConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithPrefixConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.NamedSecretWithProfileConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithPrefixConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledSecretWithProfileConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithPrefixConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.LabeledConfigMapWithProfileConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SingleSourceMultipleFilesConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.IncludeProfileSpecificSourcesConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.ConfigMapNameAsPrefixConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.boostrap.stubs.BootstrapKubernetesClientSanitizeEnvEndpointStub, \ org.springframework.cloud.kubernetes.client.config.EnableRetryBootstrapConfiguration diff --git a/spring-cloud-kubernetes-client-config/src/test/resources/sanitize-two.yaml b/spring-cloud-kubernetes-client-config/src/test/resources/sanitize-two.yaml new file mode 100644 index 0000000000..7037403969 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/resources/sanitize-two.yaml @@ -0,0 +1,16 @@ +spring: + application: + name: sanitize + cloud: + kubernetes: + secrets: + enableApi: true + sources: + - name: sanitize-secret + namespaces: test + - name: sanitize-secret-two + namespaces: test + config: + sources: + - name: sanitize-configmap + namespace: test diff --git a/spring-cloud-kubernetes-client-config/src/test/resources/sanitize.yaml b/spring-cloud-kubernetes-client-config/src/test/resources/sanitize.yaml new file mode 100644 index 0000000000..f91869d894 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/resources/sanitize.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: sanitize + cloud: + kubernetes: + secrets: + enableApi: true + sources: + - name: sanitize-secret + namespaces: test + config: + sources: + - name: sanitize-configmap + namespace: test diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml index db48e67fca..5e7952df0f 100644 --- a/spring-cloud-kubernetes-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-client-discovery/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnBlockingOrReactiveEnabled.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnBlockingOrReactiveEnabled.java index 6bb44aa493..1d53204690 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnBlockingOrReactiveEnabled.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnBlockingOrReactiveEnabled.java @@ -26,12 +26,14 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnBlockingOrReactiveDiscoveryEnabled; import org.springframework.context.annotation.Conditional; /** * Conditional that is resolved to active when either - * {@link ConditionalOnBlockingOrReactiveEnabled} or + * {@link ConditionalOnBlockingDiscoveryEnabled} or * {@link ConditionalOnReactiveDiscoveryEnabled} matches. + * @deprecated in favor of {@link ConditionalOnBlockingOrReactiveDiscoveryEnabled} * * @author wind57 */ @@ -40,6 +42,7 @@ @Documented @Inherited @Conditional(ConditionalOnBlockingOrReactiveEnabled.OnBlockingOrReactiveEnabled.class) +@Deprecated(forRemoval = true) public @interface ConditionalOnBlockingOrReactiveEnabled { class OnBlockingOrReactiveEnabled extends AnyNestedCondition { diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java new file mode 100644 index 0000000000..49628ebd71 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1ObjectReference; +import io.kubernetes.client.openapi.models.V1Service; + +import org.springframework.cloud.kubernetes.commons.discovery.InstanceIdHostPodName; + +/** + * @author wind57 + */ +final class K8sInstanceIdHostPodNameSupplier implements Supplier { + + private final V1EndpointAddress endpointAddress; + + private final V1Service service; + + private K8sInstanceIdHostPodNameSupplier(V1EndpointAddress endpointAddress, V1Service service) { + this.endpointAddress = endpointAddress; + this.service = service; + } + + @Override + public InstanceIdHostPodName get() { + return new InstanceIdHostPodName(instanceId(), host(), podName()); + } + + /** + * to be used when .spec.type of the Service is != 'ExternalName'. + */ + static K8sInstanceIdHostPodNameSupplier nonExternalName(V1EndpointAddress endpointAddress, V1Service service) { + return new K8sInstanceIdHostPodNameSupplier(endpointAddress, service); + } + + /** + * to be used when .spec.type of the Service is == 'ExternalName'. + */ + static K8sInstanceIdHostPodNameSupplier externalName(V1Service service) { + return new K8sInstanceIdHostPodNameSupplier(null, service); + } + + // instanceId is usually the pod-uid as seen in the .metadata.uid + private String instanceId() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getTargetRef).map(V1ObjectReference::getUid) + .orElseGet(() -> service.getMetadata().getUid()); + } + + private String host() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getIp) + .orElseGet(() -> service.getSpec().getExternalName()); + } + + private String podName() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getTargetRef) + .filter(objectReference -> "Pod".equals(objectReference.getKind())).map(V1ObjectReference::getName) + .orElse(null); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java new file mode 100644 index 0000000000..21c32f8f8a --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.kubernetes.commons.discovery.PodLabelsAndAnnotations; +import org.springframework.core.log.LogAccessor; + +/** + * @author wind57 + */ +final class K8sPodLabelsAndAnnotationsSupplier implements Function { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(K8sPodLabelsAndAnnotationsSupplier.class)); + + private final CoreV1Api coreV1Api; + + private final String namespace; + + private K8sPodLabelsAndAnnotationsSupplier(CoreV1Api coreV1Api, String namespace) { + this.coreV1Api = coreV1Api; + this.namespace = namespace; + } + + /** + * to be used when .spec.type of the Service is != 'ExternalName'. + */ + static K8sPodLabelsAndAnnotationsSupplier nonExternalName(CoreV1Api coreV1Api, String namespace) { + return new K8sPodLabelsAndAnnotationsSupplier(coreV1Api, namespace); + } + + /** + * to be used when .spec.type of the Service is == 'ExternalName'. + */ + static K8sPodLabelsAndAnnotationsSupplier externalName() { + return new K8sPodLabelsAndAnnotationsSupplier(null, null); + } + + @Override + public PodLabelsAndAnnotations apply(String podName) { + + V1ObjectMeta objectMeta; + + try { + objectMeta = Optional.ofNullable(coreV1Api.readNamespacedPod(podName, namespace, null).getMetadata()) + .orElse(new V1ObjectMetaBuilder().withLabels(Map.of()).withAnnotations(Map.of()).build()); + } + catch (ApiException e) { + LOG.warn(e, "Could not get pod metadata"); + objectMeta = new V1ObjectMetaBuilder().withLabels(Map.of()).withAnnotations(Map.of()).build(); + } + + return new PodLabelsAndAnnotations(Optional.ofNullable(objectMeta.getLabels()).orElse(Map.of()), + Optional.ofNullable(objectMeta.getAnnotations()).orElse(Map.of())); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java index 282b843817..6976d08159 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.Collections; -import java.util.List; import io.kubernetes.client.informer.SharedIndexInformer; import io.kubernetes.client.informer.SharedInformerFactory; @@ -30,23 +29,19 @@ import io.kubernetes.client.util.Namespaces; import io.kubernetes.client.util.generic.GenericKubernetesApi; import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; -import org.springframework.boot.BootstrapContext; import org.springframework.boot.BootstrapRegistry; -import org.springframework.boot.context.properties.bind.BindHandler; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver.PropertyResolver; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; -import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.Environment; -import org.springframework.util.ClassUtils; import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.kubernetesApiClient; @@ -55,73 +50,53 @@ */ class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + private static final Log LOG = LogFactory.getLog(KubernetesClientConfigServerBootstrapper.class); + @Override public void initialize(BootstrapRegistry registry) { - if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + if (hasConfigServerInstanceProvider()) { return; } - // We need to pass a lambda here rather than create a new instance of - // ConfigServerInstanceProvider.Function - // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on - // the classpath - registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); - } - - final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { - - private final BootstrapContext context; - - private KubernetesFunction(BootstrapContext context) { - this.context = context; - } - - static KubernetesFunction create(BootstrapContext context) { - return new KubernetesFunction(context); - } - - @Override - public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { - if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { - // If we don't have the Binder or BinderHandler from the - // ConfigDataLocationResolverContext - // we won't be able to create the necessary configuration - // properties to configure the - // Kubernetes DiscoveryClient - return Collections.emptyList(); + registry.registerIfAbsent(KubernetesDiscoveryProperties.class, context -> { + if (!getDiscoveryEnabled(context)) { + return null; } - KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, - bindHandler); - KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); - return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) - .getInstances(serviceId); - } + return createKubernetesDiscoveryProperties(context); + }); - protected KubernetesConfigServerInstanceProvider getInstanceProvider( - KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, - BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { + registry.registerIfAbsent(KubernetesClientProperties.class, context -> { + if (!getDiscoveryEnabled(context)) { + return null; + } + return createKubernetesClientProperties(context); + }); + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> { + if (!getDiscoveryEnabled(context)) { + return (id) -> Collections.emptyList(); + } if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) { KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class); return client::getInstances; } else { - + PropertyResolver propertyResolver = getPropertyResolver(context); ApiClient defaultApiClient = kubernetesApiClient(); - defaultApiClient.setUserAgent(binder.bind("spring.cloud.kubernetes.client.user-agent", String.class) - .orElse(KubernetesClientProperties.DEFAULT_USER_AGENT)); + defaultApiClient.setUserAgent(propertyResolver.get("spring.cloud.kubernetes.client.user-agent", + String.class, KubernetesClientProperties.DEFAULT_USER_AGENT)); KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration(); ApiClient apiClient = context.getOrElseSupply(ApiClient.class, () -> defaultApiClient); KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration - .kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler)); - + .kubernetesNamespaceProvider(getNamespaceEnvironment(propertyResolver)); + KubernetesDiscoveryProperties discoveryProperties = context.get(KubernetesDiscoveryProperties.class); String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties); SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient); - final GenericKubernetesApi servicesApi = new GenericKubernetesApi<>( - V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient); + GenericKubernetesApi servicesApi = new GenericKubernetesApi<>(V1Service.class, + V1ServiceList.class, "", "v1", "services", apiClient); SharedIndexInformer serviceSharedIndexInformer = sharedInformerFactory .sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace); Lister serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer()); - final GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>( + GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>( V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient); SharedIndexInformer endpointsSharedIndexInformer = sharedInformerFactory .sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace); @@ -134,40 +109,32 @@ protected KubernetesConfigServerInstanceProvider getInstanceProvider( return discoveryClient::getInstances; } catch (Exception e) { - if (log != null) { - log.warn("Error initiating informer discovery client", e); - } + LOG.warn("Error initiating informer discovery client", e); return (serviceId) -> Collections.emptyList(); } finally { sharedInformerFactory.stopAllRegisteredInformers(); } } - } + }); - private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider, - KubernetesDiscoveryProperties discoveryProperties) { - return discoveryProperties.allNamespaces() ? Namespaces.NAMESPACE_ALL - : kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT - : kubernetesNamespaceProvider.getNamespace(); - } - - private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) { - return new AbstractEnvironment() { - @Override - public String getProperty(String key) { - return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key)); - } - }; - } + } - // This method should never be called, but is there for backward - // compatibility purposes - @Override - public List apply(String serviceId) { - return apply(serviceId, null, null, null); - } + private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider, + KubernetesDiscoveryProperties discoveryProperties) { + return discoveryProperties.allNamespaces() ? Namespaces.NAMESPACE_ALL + : kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT + : kubernetesNamespaceProvider.getNamespace(); + } + private Environment getNamespaceEnvironment( + ConfigServerConfigDataLocationResolver.PropertyResolver propertyResolver) { + return new AbstractEnvironment() { + @Override + public String getProperty(String key) { + return propertyResolver.get(key, String.class, super.getProperty(key)); + } + }; } } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java index 2b73621095..b81d96f1db 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnBlockingOrReactiveDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -56,7 +57,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled @ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnBlockingOrReactiveEnabled +@ConditionalOnBlockingOrReactiveDiscoveryEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @Conditional(ConditionalOnSelectiveNamespacesMissing.class) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java index 5f44ed9b21..23f1ed26a2 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnBlockingOrReactiveDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -56,7 +57,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled @ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnBlockingOrReactiveEnabled +@ConditionalOnBlockingOrReactiveDiscoveryEnabled @Conditional(ConditionalOnSelectiveNamespacesPresent.class) @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @@ -67,10 +68,8 @@ public class KubernetesClientInformerSelectiveNamespacesAutoConfiguration { LogFactory.getLog(KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class)); // we rely on the order of namespaces to enable listers, as such provide a bean of - // namespaces - // as a list, instead of the incoming Set. + // namespaces as a list, instead of the incoming Set. @Bean - @ConditionalOnMissingBean public List selectiveNamespaces(KubernetesDiscoveryProperties properties) { List selectiveNamespaces = properties.namespaces().stream().sorted().toList(); LOG.debug(() -> "using selective namespaces : " + selectiveNamespaces); diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java index 9e0a88aeae..864d469dc5 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java @@ -17,15 +17,19 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.time.Duration; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceSpec; @@ -33,14 +37,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; import org.springframework.core.log.LogAccessor; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.util.CollectionUtils; -import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.keysWithPrefix; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NAMESPACE_METADATA_KEY; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.SERVICE_TYPE; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.UNSET_PORT_NAME; +import static org.springframework.util.StringUtils.hasText; /** * @author wind57 @@ -82,40 +87,6 @@ static boolean matchesServiceLabels(V1Service service, KubernetesDiscoveryProper } - /** - * This adds the following metadata.
-	 *     - labels (if requested)
-	 *     - annotations (if requested)
-	 *     - metadata
-	 *     - service type
-	 * 
- */ - static Map serviceMetadata(KubernetesDiscoveryProperties properties, V1Service service, - String serviceId) { - - Map serviceMetadata = new HashMap<>(); - KubernetesDiscoveryProperties.Metadata metadataProps = properties.metadata(); - if (metadataProps.addLabels()) { - Map labelMetadata = keysWithPrefix(service.getMetadata().getLabels(), - metadataProps.labelsPrefix()); - LOG.debug(() -> "Adding labels metadata: " + labelMetadata + " for serviceId: " + serviceId); - serviceMetadata.putAll(labelMetadata); - } - if (metadataProps.addAnnotations()) { - Map annotationMetadata = keysWithPrefix(service.getMetadata().getAnnotations(), - metadataProps.annotationsPrefix()); - LOG.debug(() -> "Adding annotations metadata: " + annotationMetadata + " for serviceId: " + serviceId); - serviceMetadata.putAll(annotationMetadata); - } - - serviceMetadata.put(NAMESPACE_METADATA_KEY, - Optional.ofNullable(service.getMetadata()).map(V1ObjectMeta::getNamespace).orElse(null)); - serviceMetadata.put(SERVICE_TYPE, - Optional.ofNullable(service.getSpec()).map(V1ServiceSpec::getType).orElse(null)); - - return serviceMetadata; - } - static Predicate filter(KubernetesDiscoveryProperties properties) { String spelExpression = properties.filter(); Predicate predicate; @@ -159,4 +130,38 @@ static void postConstruct(List sharedInformerFactories, } + static ServiceMetadata serviceMetadata(V1Service service) { + V1ObjectMeta metadata = service.getMetadata(); + V1ServiceSpec serviceSpec = service.getSpec(); + return new ServiceMetadata(metadata.getName(), metadata.getNamespace(), serviceSpec.getType(), + metadata.getLabels(), metadata.getAnnotations()); + } + + /** + * a service is allowed to have a single port defined without a name. + */ + static Map endpointSubsetsPortData(List endpointSubsets) { + return endpointSubsets.stream() + .flatMap(endpointSubset -> Optional.ofNullable(endpointSubset.getPorts()).orElse(List.of()).stream()) + .collect(Collectors.toMap( + endpointPort -> hasText(endpointPort.getName()) ? endpointPort.getName() : UNSET_PORT_NAME, + CoreV1EndpointPort::getPort)); + } + + static List addresses(V1EndpointSubset endpointSubset, + KubernetesDiscoveryProperties properties) { + List addresses = Optional.ofNullable(endpointSubset.getAddresses()).map(ArrayList::new) + .orElse(new ArrayList<>()); + + if (properties.includeNotReadyAddresses()) { + List notReadyAddresses = endpointSubset.getNotReadyAddresses(); + if (CollectionUtils.isEmpty(notReadyAddresses)) { + return addresses; + } + addresses.addAll(notReadyAddresses); + } + + return addresses; + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java index e504eb3219..6480085976 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnBlockingOrReactiveDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -58,7 +59,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled @ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnBlockingOrReactiveEnabled +@ConditionalOnBlockingOrReactiveDiscoveryEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @Conditional(ConditionalOnSelectiveNamespacesMissing.class) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java index 572ecd3510..251d4a3a56 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java @@ -17,43 +17,47 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import io.kubernetes.client.informer.SharedInformer; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; -import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1Service; import jakarta.annotation.PostConstruct; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; +import org.springframework.cloud.kubernetes.commons.discovery.ServicePortNameAndNumber; +import org.springframework.cloud.kubernetes.commons.discovery.ServicePortSecureResolver; import org.springframework.core.log.LogAccessor; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import static org.springframework.cloud.kubernetes.client.discovery.K8sInstanceIdHostPodNameSupplier.externalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sInstanceIdHostPodNameSupplier.nonExternalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sPodLabelsAndAnnotationsSupplier.externalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sPodLabelsAndAnnotationsSupplier.nonExternalName; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.addresses; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.endpointSubsetsPortData; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.filter; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.matchesServiceLabels; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.postConstruct; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.serviceMetadata; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTP; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTPS; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.PRIMARY_PORT_NAME_LABEL_KEY; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.SECURED; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.UNSET_PORT_NAME; +import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.endpointsPort; +import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.serviceInstance; +import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.serviceInstanceMetadata; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.EXTERNAL_NAME; /** * @author Min Kim @@ -76,6 +80,13 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { private final Predicate filter; + private final ServicePortSecureResolver servicePortSecureResolver; + + // visible only for testing and + // must be constructor injected in a future release + @Autowired + CoreV1Api coreV1Api; + @Deprecated(forRemoval = true) public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, @@ -87,6 +98,7 @@ public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } public KubernetesInformerDiscoveryClient(SharedInformerFactory sharedInformerFactory, @@ -99,6 +111,7 @@ public KubernetesInformerDiscoveryClient(SharedInformerFactory sharedInformerFac this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } public KubernetesInformerDiscoveryClient(List sharedInformerFactories, @@ -119,6 +132,7 @@ public KubernetesInformerDiscoveryClient(List sharedInfor this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } @Override @@ -130,104 +144,76 @@ public String description() { public List getInstances(String serviceId) { Objects.requireNonNull(serviceId, "serviceId must be provided"); - List services = serviceListers.stream().flatMap(x -> x.list().stream()) + List allServices = serviceListers.stream().flatMap(x -> x.list().stream()) .filter(scv -> scv.getMetadata() != null).filter(svc -> serviceId.equals(svc.getMetadata().getName())) - .filter(scv -> matchesServiceLabels(scv, properties)).filter(filter).toList(); - return services.stream().flatMap(service -> getServiceInstanceDetails(service, serviceId)).toList(); + .filter(scv -> matchesServiceLabels(scv, properties)).toList(); + + List serviceInstances = allServices.stream().filter(filter) + .flatMap(service -> serviceInstances(service, serviceId).stream()) + .collect(Collectors.toCollection(ArrayList::new)); + + if (properties.includeExternalNameServices()) { + LOG.debug(() -> "Searching for 'ExternalName' type of services with serviceId : " + serviceId); + List externalNameServices = allServices.stream().filter(s -> s.getSpec() != null) + .filter(s -> EXTERNAL_NAME.equals(s.getSpec().getType())).toList(); + for (V1Service service : externalNameServices) { + ServiceMetadata serviceMetadata = serviceMetadata(service); + Map serviceInstanceMetadata = serviceInstanceMetadata(Map.of(), serviceMetadata, + properties); + + K8sInstanceIdHostPodNameSupplier supplierOne = externalName(service); + K8sPodLabelsAndAnnotationsSupplier supplierTwo = externalName(); + + ServiceInstance externalNameServiceInstance = serviceInstance(null, serviceMetadata, supplierOne, + supplierTwo, new ServicePortNameAndNumber(-1, null), serviceInstanceMetadata, properties); + serviceInstances.add(externalNameServiceInstance); + } + } + + return serviceInstances; } - private Stream getServiceInstanceDetails(V1Service service, String serviceId) { - Map serviceMetadata = serviceMetadata(properties, service, serviceId); + private List serviceInstances(V1Service service, String serviceId) { - List endpoints = endpointsListers.stream() - .map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()) - .get(service.getMetadata().getName())) - .filter(Objects::nonNull).filter(ep -> ep.getSubsets() != null).toList(); + List instances = new ArrayList<>(); - Optional discoveredPrimaryPortName = Optional.empty(); - if (service.getMetadata() != null && service.getMetadata().getLabels() != null) { - discoveredPrimaryPortName = Optional - .ofNullable(service.getMetadata().getLabels().get(PRIMARY_PORT_NAME_LABEL_KEY)); - } - final String primaryPortName = discoveredPrimaryPortName.orElse(properties.primaryPortName()); - - final boolean secured = isSecured(service); - - return endpoints.stream() - .flatMap(ep -> ep.getSubsets().stream() - .filter(subset -> subset.getPorts() != null && subset.getPorts().size() > 0) // safeguard - .flatMap(subset -> { - Map metadata = new HashMap<>(serviceMetadata); - List endpointPorts = subset.getPorts(); - if (properties.metadata() != null && properties.metadata().addPorts()) { - endpointPorts.forEach(p -> metadata.put( - StringUtils.hasText(p.getName()) ? p.getName() : UNSET_PORT_NAME, - Integer.toString(p.getPort()))); - } - List addresses = subset.getAddresses(); - if (addresses == null) { - addresses = new ArrayList<>(); - } - if (properties.includeNotReadyAddresses() - && !CollectionUtils.isEmpty(subset.getNotReadyAddresses())) { - addresses.addAll(subset.getNotReadyAddresses()); - } - - final int port = findEndpointPort(endpointPorts, primaryPortName, serviceId); - return addresses.stream() - .map(addr -> new DefaultKubernetesServiceInstance( - addr.getTargetRef() != null ? addr.getTargetRef().getUid() : "", serviceId, - addr.getIp(), port, metadata, secured, service.getMetadata().getNamespace(), - // TODO find out how to get cluster name - // possibly from - // KubeConfig - null)); - })); - } + List allEndpoints = endpointsListers.stream() + .map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()).get(serviceId)) + .filter(Objects::nonNull).toList(); - private static boolean isSecured(V1Service service) { - Optional securedOpt = Optional.empty(); - if (service.getMetadata() != null && service.getMetadata().getAnnotations() != null) { - securedOpt = Optional.ofNullable(service.getMetadata().getAnnotations().get(SECURED)); - } - if (!securedOpt.isPresent() && service.getMetadata() != null && service.getMetadata().getLabels() != null) { - securedOpt = Optional.ofNullable(service.getMetadata().getLabels().get(SECURED)); - } - return Boolean.parseBoolean(securedOpt.orElse("false")); - } + for (V1Endpoints endpoints : allEndpoints) { + List subsets = endpoints.getSubsets(); + if (subsets == null || subsets.isEmpty()) { + LOG.debug(() -> "serviceId : " + serviceId + " does not have any subsets"); + } + else { + ServiceMetadata serviceMetadata = serviceMetadata(service); + Map portsData = endpointSubsetsPortData(subsets); + Map serviceInstanceMetadata = serviceInstanceMetadata(portsData, serviceMetadata, + properties); - private int findEndpointPort(List endpointPorts, String primaryPortName, String serviceId) { - if (endpointPorts.size() == 1) { - return endpointPorts.get(0).getPort(); - } - else { - Map ports = endpointPorts.stream().filter(p -> StringUtils.hasText(p.getName())) - .collect(Collectors.toMap(CoreV1EndpointPort::getName, CoreV1EndpointPort::getPort)); - // This oneliner is looking for a port with a name equal to the primary port - // name specified in the service label - // or in spring.cloud.kubernetes.discovery.primary-port-name, equal to https, - // or equal to http. - // In case no port has been found return -1 to log a warning and fall back to - // the first port in the list. - int discoveredPort = ports.getOrDefault(primaryPortName, - ports.getOrDefault(HTTPS, ports.getOrDefault(HTTP, -1))); - - if (discoveredPort == -1) { - if (StringUtils.hasText(primaryPortName)) { - LOG.warn(() -> "Could not find a port named '" + primaryPortName - + "', 'https', or 'http' for service '" + serviceId + "'."); - } - else { - LOG.warn(() -> "Could not find a port named 'https' or 'http' for service '" + serviceId + "'."); + for (V1EndpointSubset endpointSubset : subsets) { + + Map endpointsPortData = endpointSubsetsPortData(List.of(endpointSubset)); + ServicePortNameAndNumber portData = endpointsPort(endpointsPortData, serviceMetadata, properties); + + List addresses = addresses(endpointSubset, properties); + for (V1EndpointAddress endpointAddress : addresses) { + + K8sInstanceIdHostPodNameSupplier supplierOne = nonExternalName(endpointAddress, service); + K8sPodLabelsAndAnnotationsSupplier supplierTwo = nonExternalName(coreV1Api, + service.getMetadata().getNamespace()); + + ServiceInstance serviceInstance = serviceInstance(servicePortSecureResolver, serviceMetadata, + supplierOne, supplierTwo, portData, serviceInstanceMetadata, properties); + instances.add(serviceInstance); + } } - LOG.warn( - () -> "Make sure that either the primary-port-name label has been added to the service, or that spring.cloud.kubernetes.discovery.primary-port-name has been configured."); - LOG.warn(() -> "Alternatively name the primary port 'https' or 'http'"); - LOG.warn(() -> "An incorrect configuration may result in non-deterministic behaviour."); - discoveredPort = endpointPorts.get(0).getPort(); + } - return discoveredPort; } + + return instances; } @Override diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java index 57341b29b9..5a08ebd0b3 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java @@ -25,22 +25,16 @@ import io.kubernetes.client.openapi.models.V1Service; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; import org.springframework.cloud.client.CommonsClientAutoConfiguration; -import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; -import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.PodUtils; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -54,10 +48,7 @@ * @author wind57 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnBlockingDiscoveryEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnSpringCloudKubernetesBlockingDiscovery @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class, @@ -84,8 +75,7 @@ public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient( * bean of type DiscoveryClientHealthIndicator via ObjectProvider. */ @Bean - @ConditionalOnClass({ HealthIndicator.class }) - @ConditionalOnDiscoveryHealthIndicatorEnabled + @ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer( ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { LOG.debug(() -> "Will publish InstanceRegisteredEvent from blocking implementation"); diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatch.java index 2a3d5d44fb..bb9fbe5c67 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatch.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatch.java @@ -36,6 +36,7 @@ import org.springframework.core.log.LogAccessor; import org.springframework.scheduling.annotation.Scheduled; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_GROUP; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_VERSION; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.ENDPOINT_SLICE; @@ -67,7 +68,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } - @Scheduled(fixedDelayString = "${spring.cloud.kubernetes.discovery.catalogServicesWatchDelay:30000}") + @Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}") void catalogServicesWatch() { try { diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfiguration.java index c1b1e8cd10..be874dce08 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfiguration.java @@ -20,13 +20,10 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogWatcherEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -39,9 +36,7 @@ * @author wind57 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesCatalogEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnKubernetesCatalogWatcherEnabled @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) class KubernetesCatalogWatchAutoConfiguration { diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointSlicesCatalogWatch.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointSlicesCatalogWatch.java index 46deca3d1a..1d14db25f5 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointSlicesCatalogWatch.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointSlicesCatalogWatch.java @@ -81,7 +81,7 @@ else if (!context.properties().namespaces().isEmpty()) { private List endpointSlices(DiscoveryV1Api api, Map labels) { try { return api.listEndpointSliceForAllNamespaces(null, null, null, labelSelector(labels), null, null, null, - null, null, null).getItems(); + null, null, null, null).getItems(); } catch (ApiException e) { LOG.warn(e, () -> "can not list endpoint slices in all namespaces"); @@ -93,7 +93,7 @@ private List namespacedEndpointSlices(DiscoveryV1Api api, Strin Map labels) { try { return api.listNamespacedEndpointSlice(namespace, null, null, null, null, labelSelector(labels), null, null, - null, null, null).getItems(); + null, null, null, null).getItems(); } catch (ApiException e) { LOG.warn(e, () -> "can not list endpoint slices in namespace " + namespace); diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointsCatalogWatch.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointsCatalogWatch.java index 3409e58d19..3bac70afb4 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointsCatalogWatch.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesEndpointsCatalogWatch.java @@ -91,7 +91,7 @@ else if (!context.properties().namespaces().isEmpty()) { private List endpoints(CoreV1Api client, Map labels) { try { return client.listEndpointsForAllNamespaces(null, null, null, labelSelector(labels), null, null, null, null, - null, null).getItems(); + null, null, null).getItems(); } catch (ApiException e) { LOG.warn(e, () -> "can not list endpoints in all namespaces"); @@ -102,7 +102,7 @@ private List endpoints(CoreV1Api client, Map labels private List namespacedEndpoints(CoreV1Api client, String namespace, Map labels) { try { return client.listNamespacedEndpoints(namespace, null, null, null, null, labelSelector(labels), null, null, - null, null, null).getItems(); + null, null, null, null).getItems(); } catch (ApiException e) { LOG.warn(e, () -> "can not list endpoints in namespace " + namespace); diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java index 62f7393912..9d7f1b40e9 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java @@ -27,13 +27,7 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; -import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; -import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; import org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; @@ -48,7 +42,8 @@ import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.PodUtils; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -65,10 +60,7 @@ */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnReactiveDiscoveryEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnSpringCloudKubernetesReactiveDiscovery @AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class, ReactiveCommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class, @@ -106,8 +98,7 @@ public KubernetesInformerReactiveDiscoveryClient kubernetesReactiveDiscoveryClie * Post an event so that health indicator is initialized. */ @Bean - @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") - @ConditionalOnDiscoveryHealthIndicatorEnabled + @ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer KubernetesDiscoveryClientHealthIndicatorInitializer reactiveIndicatorInitializer( ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { LOG.debug(() -> "Will publish InstanceRegisteredEvent from reactive implementation"); @@ -118,8 +109,7 @@ KubernetesDiscoveryClientHealthIndicatorInitializer reactiveIndicatorInitializer * unlike the blocking implementation, we need to register the health indicator. */ @Bean - @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") - @ConditionalOnDiscoveryHealthIndicatorEnabled + @ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( KubernetesInformerReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties) { return new ReactiveDiscoveryClientHealthIndicator(client, properties); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java new file mode 100644 index 0000000000..fd9ce80b59 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.discovery.InstanceIdHostPodName; + +/** + * @author wind57 + */ +class K8sInstanceIdHostPodNameSupplierTests { + + @Test + void instanceIdNoEndpointAddress() { + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().build()) + .withMetadata(new V1ObjectMetaBuilder().withUid("123").build()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.instanceId(), "123"); + } + + @Test + void instanceIdWithEndpointAddress() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withUid("456").build()).build(); + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().build()) + .withMetadata(new V1ObjectMetaBuilder().withUid("123").build()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.instanceId(), "456"); + } + + @Test + void hostNoEndpointAddress() { + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.host(), "external-name"); + } + + @Test + void hostWithEndpointAddress() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder().withIp("127.0.0.1").build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.host(), "127.0.0.1"); + } + + @Test + void testPodNameIsNull() { + V1Service service = new V1ServiceBuilder().withMetadata(new V1ObjectMetaBuilder().withUid("123").build()) + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()).build(); + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertNull(result.podName()); + } + + @Test + void podNameKindNotPod() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withKind("Service").build()).build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertNull(result.podName()); + } + + @Test + void podNameKindIsPod() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withKind("Pod").withName("my-pod").build()).build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.podName(), "my-pod"); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java new file mode 100644 index 0000000000..8b9ad13589 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1PodBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.commons.discovery.PodLabelsAndAnnotations; + +/** + * @author wind57 + */ +class K8sPodLabelsAndAnnotationsSupplierTests { + + private static final String NAMESPACE = "spring-k8s"; + + private static final String POD_NAME = "my-pod"; + + private final CoreV1Api coreV1Api = Mockito.mock(CoreV1Api.class); + + @AfterEach + void afterEach() { + Mockito.reset(coreV1Api); + } + + @Test + void noObjetMeta() throws Exception { + + Mockito.when(coreV1Api.readNamespacedPod(POD_NAME, NAMESPACE, null)).thenReturn( + new V1PodBuilder().withMetadata(new V1ObjectMetaBuilder().withName(POD_NAME).build()).build()); + + PodLabelsAndAnnotations result = K8sPodLabelsAndAnnotationsSupplier.nonExternalName(coreV1Api, NAMESPACE) + .apply(POD_NAME); + Assertions.assertNotNull(result); + Assertions.assertTrue(result.labels().isEmpty()); + Assertions.assertTrue(result.annotations().isEmpty()); + } + + @Test + void labelsAndAnnotationsPresent() throws Exception { + + Mockito.when(coreV1Api.readNamespacedPod(POD_NAME, NAMESPACE, null)) + .thenReturn(new V1PodBuilder().withMetadata(new V1ObjectMetaBuilder().withName(POD_NAME) + .withLabels(Map.of("a", "b")).withAnnotations(Map.of("c", "d")).build()).build()); + + PodLabelsAndAnnotations result = K8sPodLabelsAndAnnotationsSupplier.nonExternalName(coreV1Api, NAMESPACE) + .apply(POD_NAME); + Assertions.assertNotNull(result); + Assertions.assertEquals(result.labels(), Map.of("a", "b")); + Assertions.assertEquals(result.annotations(), Map.of("c", "d")); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfigurationTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfigurationTests.java new file mode 100644 index 0000000000..c65544cc4c --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfigurationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.List; +import java.util.Set; + +import io.kubernetes.client.openapi.ApiClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +@ExtendWith(OutputCaptureExtension.class) +class KubernetesClientInformerSelectiveNamespacesAutoConfigurationTests { + + private static final String NAMESPACE_A = "a"; + + private static final String NAMESPACE_B = "b"; + + private static final String NAMESPACE_C = "c"; + + private static final String NAMESPACE_D = "d"; + + @Test + void testBeansCreates(CapturedOutput output) { + + new ApplicationContextRunner() + .withPropertyValues("spring.cloud.discovery.enabled=true", + "spring.cloud.kubernetes.discovery.enabled=true", + "spring.cloud.kubernetes.discovery.namespaces[0]=" + NAMESPACE_A, + "spring.cloud.kubernetes.discovery.namespaces[1]=" + NAMESPACE_B, + "spring.main.cloud-platform=kubernetes") + .withConfiguration( + AutoConfigurations.of(KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class)) + .withUserConfiguration(Config.class).run(context -> { + assertThat(context.getBean("selectiveNamespaces")).isNotNull(); + + @SuppressWarnings("unchecked") + Set selectiveNamespaces = context.getBean("selectiveNamespaces", Set.class); + Assertions.assertEquals(selectiveNamespaces, Set.of("a", "b")); + + @SuppressWarnings("unchecked") + Set namespaces = context.getBean("namespaces", Set.class); + Assertions.assertEquals(namespaces, Set.of("c", "d")); + }); + + assertThat(output.getOut().contains("registering lister (for services) in namespace : " + NAMESPACE_A)) + .isTrue(); + assertThat(output.getOut().contains("registering lister (for services) in namespace : " + NAMESPACE_B)) + .isTrue(); + + assertThat(output.getOut().contains("registering lister (for services) in namespace : " + NAMESPACE_C)) + .isFalse(); + assertThat(output.getOut().contains("registering lister (for services) in namespace : " + NAMESPACE_D)) + .isFalse(); + + assertThat(output.getOut().contains("registering lister (for endpoints) in namespace : " + NAMESPACE_A)) + .isTrue(); + assertThat(output.getOut().contains("registering lister (for endpoints) in namespace : " + NAMESPACE_B)) + .isTrue(); + + assertThat(output.getOut().contains("registering lister (for endpoints) in namespace : " + NAMESPACE_C)) + .isFalse(); + assertThat(output.getOut().contains("registering lister (for endpoints) in namespace : " + NAMESPACE_D)) + .isFalse(); + } + + @Configuration + @EnableConfigurationProperties(KubernetesDiscoveryProperties.class) + static class Config { + + @Bean + ApiClient apiClient() { + return Mockito.mock(ApiClient.class); + } + + @Bean + List namespaces() { + return List.of(NAMESPACE_C, NAMESPACE_D); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java index 312f396fc0..f04a3b9299 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java @@ -20,6 +20,7 @@ import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; import okhttp3.OkHttpClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -91,6 +92,11 @@ private void setup(String... env) { @Configuration(proxyBeanMethods = false) protected static class EnvironmentKnobbler { + @Bean + CoreV1Api coreV1Api(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + @Bean ApiClient apiClient() { ApiClient apiClient = mock(ApiClient.class); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java new file mode 100644 index 0000000000..3071b3671e --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -0,0 +1,277 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServicePort; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.assertj.core.util.Strings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; + +import static java.util.Map.entry; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientFilterMetadataTest { + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void testAllExtraMetadataDisabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "lab"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).isEqualTo(Map.of("k8s_namespace", "ns", "type", "ClusterIP")); + } + + @Test + void testLabelsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1", "l2", "v2"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("l1", "v1"), entry("l2", "v2"), + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); + } + + @Test + void testLabelsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, "l_", false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1", "l2", "v2"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("l_l1", "v1"), entry("l_l2", "v2"), + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); + } + + @Test + void testAnnotationsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, true, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("a1", "v1"), entry("a2", "v2"), + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); + } + + @Test + void testAnnotationsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, true, + "a_", false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("a_a1", "v1"), entry("a_a2", "v2"), + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); + } + + @Test + void testPortsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "test", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("http", "80"), entry("k8s_namespace", "test"), + entry("", "5555"), entry("type", "ClusterIP")); + } + + @Test + void testPortsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, "p_"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("p_http", "80"), entry("k8s_namespace", "ns"), + entry("p_", "5555"), entry("type", "ClusterIP")); + } + + @Test + void testLabelsAndAnnotationsAndPortsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, "l_", true, + "a_", true, "p_"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "la1"), Map.of("a1", "an1", "a2", "an2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("a_a1", "an1"), entry("a_a2", "an2"), + entry("l_l1", "la1"), entry("p_http", "80"), entry("k8s_namespace", "ns"), entry("type", "ClusterIP"), + entry("p_", "5555")); + } + + private void setup(String serviceId, String namespace, Map labels, Map annotations, + Map ports) { + + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").withPorts(getServicePorts(ports)).build()) + .withNewMetadata().withName(serviceId).withNamespace(namespace).withLabels(labels) + .withAnnotations(annotations).endMetadata().build(); + + servicesCache.add(service); + + V1ObjectMeta objectMeta = new V1ObjectMeta(); + objectMeta.setNamespace(namespace); + objectMeta.setName(serviceId); + + V1Endpoints endpoints = new V1EndpointsBuilder().withMetadata(objectMeta).addNewSubset() + .addAllToPorts(getEndpointPorts(ports)).addNewAddress().endAddress().endSubset().build(); + + endpointsCache.add(endpoints); + + } + + private List getServicePorts(Map ports) { + return ports.entrySet().stream().map(e -> { + V1ServicePortBuilder servicePortBuilder = new V1ServicePortBuilder(); + servicePortBuilder.withPort(e.getKey()); + if (!Strings.isNullOrEmpty(e.getValue())) { + servicePortBuilder.withName(e.getValue()); + } + return servicePortBuilder.build(); + }).collect(toList()); + } + + private List getEndpointPorts(Map ports) { + return ports.entrySet().stream().map(e -> { + CoreV1EndpointPortBuilder endpointPortBuilder = new CoreV1EndpointPortBuilder(); + endpointPortBuilder.withPort(e.getKey()); + if (!Strings.isNullOrEmpty(e.getValue())) { + endpointPortBuilder.withName(e.getValue()); + } + return endpointPortBuilder.build(); + }).collect(toList()); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java deleted file mode 100644 index 5d6b904aa1..0000000000 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.discovery; - -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1Service; -import io.kubernetes.client.openapi.models.V1ServiceSpec; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; - -/** - * @author wind57 - */ -@ExtendWith(OutputCaptureExtension.class) -class KubernetesDiscoveryClientServiceMetadataTests { - - /** - *
-	 *     - labels are not added
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataEmpty() { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default")); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 2); - Assertions.assertEquals(result, Map.of("k8s_namespace", "default", "type", "ClusterIP")); - } - - /** - *
-	 *     - labels are added without a prefix
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsNoPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = ""; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default").labels(Map.of("a", "b"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 3); - Assertions.assertEquals(result, Map.of("a", "b", "k8s_namespace", "default", "type", "ClusterIP")); - String labelsMetadata = filterOnK8sNamespaceAndType(result); - Assertions.assertTrue( - output.getOut().contains("Adding labels metadata: " + labelsMetadata + " for serviceId: my-service")); - } - - /** - *
-	 *     - labels are added with prefix
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsWithPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = "prefix-"; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default").labels(Map.of("a", "b", "c", "d"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 4); - Assertions.assertEquals(result, - Map.of("prefix-a", "b", "prefix-c", "d", "k8s_namespace", "default", "type", "ClusterIP")); - // so that result is deterministic in assertion - String labelsMetadata = filterOnK8sNamespaceAndType(result); - Assertions.assertTrue( - output.getOut().contains("Adding labels metadata: " + labelsMetadata + " for serviceId: my-service")); - } - - /** - *
-	 *     - labels are not added
-	 *     - annotations are added without prefix
-	 * 
- */ - @Test - void testServiceMetadataAddAnnotationsNoPrefix(CapturedOutput output) { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = true; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata( - new V1ObjectMeta().namespace("default").labels(Map.of("a", "b")).annotations(Map.of("aa", "bb"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 3); - Assertions.assertEquals(result, Map.of("aa", "bb", "k8s_namespace", "default", "type", "ClusterIP")); - Assertions - .assertTrue(output.getOut().contains("Adding annotations metadata: {aa=bb} for serviceId: my-service")); - } - - /** - *
-	 *     - labels are not added
-	 *     - annotations are added with prefix
-	 * 
- */ - @Test - void testServiceMetadataAddAnnotationsWithPrefix(CapturedOutput output) { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = true; - String annotationsPrefix = "prefix-"; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata(new V1ObjectMeta() - .namespace("default").labels(Map.of("a", "b")).annotations(Map.of("aa", "bb", "cc", "dd"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 4); - Assertions.assertEquals(result, - Map.of("prefix-aa", "bb", "prefix-cc", "dd", "k8s_namespace", "default", "type", "ClusterIP")); - // so that result is deterministic in assertion - String annotations = filterOnK8sNamespaceAndType(result); - Assertions.assertTrue( - output.getOut().contains("Adding annotations metadata: " + annotations + " for serviceId: my-service")); - } - - /** - *
-	 *     - labels are added with prefix
-	 *     - annotations are added with prefix
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsAndAnnotationsWithPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = "label-"; - boolean addAnnotations = true; - String annotationsPrefix = "annotation-"; - boolean addPorts = false; - String portsPrefix = ""; - - KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, - labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata(new V1ObjectMeta() - .namespace("default").labels(Map.of("a", "b", "c", "d")).annotations(Map.of("aa", "bb", "cc", "dd"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 6); - Assertions.assertEquals(result, Map.of("annotation-aa", "bb", "annotation-cc", "dd", "label-a", "b", "label-c", - "d", "k8s_namespace", "default", "type", "ClusterIP")); - // so that result is deterministic in assertion - String labels = result.entrySet().stream().filter(en -> en.getKey().contains("label")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); - String annotations = result.entrySet().stream().filter(en -> en.getKey().contains("annotation")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); - Assertions.assertTrue( - output.getOut().contains("Adding labels metadata: " + labels + " for serviceId: my-service")); - Assertions.assertTrue( - output.getOut().contains("Adding annotations metadata: " + annotations + " for serviceId: my-service")); - } - - private String filterOnK8sNamespaceAndType(Map result) { - return result.entrySet().stream().filter(en -> !en.getKey().contains("k8s_namespace")) - .filter(en -> !en.getKey().equals("type")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); - } - -} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java new file mode 100644 index 0000000000..5bfcc509f2 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientServiceWithoutPortNameTests { + + private static final String NAMESPACE = "spring-k8s"; + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void testDiscoveryWithoutAServicePortName() { + + V1Endpoints endpoints = new V1EndpointsBuilder() + .withSubsets( + new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(8080).build()) + .withAddresses(new V1EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata( + new V1ObjectMetaBuilder().withName("no-port-name-service").withNamespace(NAMESPACE).build()) + .build(); + endpointsCache.add(endpoints); + + V1Service service = new V1ServiceBuilder() + .withSpec( + new V1ServiceSpecBuilder().withPorts(new V1ServicePortBuilder().withPort(8080).build()).build()) + .withMetadata( + new V1ObjectMetaBuilder().withName("no-port-name-service").withNamespace(NAMESPACE).build()) + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").build()).build(); + servicesCache.add(service); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(NAMESPACE), + true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List serviceInstances = discoveryClient.getInstances("no-port-name-service"); + Assertions.assertEquals(serviceInstances.size(), 1); + Assertions.assertEquals(serviceInstances.get(0).getMetadata(), + Map.of("port.", "8080", "k8s_namespace", "spring-k8s", "type", "ClusterIP")); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java new file mode 100644 index 0000000000..b531bf3083 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java @@ -0,0 +1,457 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientTests { + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddress() { + Map labels = Map.of("l", "v"); + String serviceId = "id"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("10"); + List names = List.of("http"); + List protocols = List.of("TCP"); + List ports = List.of(80); + List appProtocols = List.of("appTCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("id"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("10")).hasSize(1); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePorts() { + Map labels = Map.of("l2", "v2"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("20"); + List names = List.of("http", "mgmt"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(80, 900); + List appProtocols = List.of("http_tcp", "mgmt_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), labels, "http_tcp", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("20")).hasSize(1).filteredOn(s -> 80 == s.getPort()) + .hasSize(1); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsMultipleAddresses() { + Map labels = Map.of("l1", "v1"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1", "ip2"); + List uuids = List.of("40", "50"); + List names = List.of("https"); + List protocols = List.of("TCP"); + List ports = List.of(443); + List appProtocols = List.of("https_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, "port."); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(443, 8443), labels, null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(2).filteredOn(ServiceInstance::isSecure).extracting(ServiceInstance::getHost) + .containsOnly("ip1", "ip2"); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() { + + Map labels = Map.of("l", "v"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("60"); + List names = List.of("http"); + List protocols = List.of("TCP"); + List ports = List.of(80); + List appProtocols = List.of("https_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + ips = List.of("ip2"); + uuids = List.of("70"); + namespace = "test2"; + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(2); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip2") && !s.isSecure()).hasSize(1); + assertThat(instances).filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test2")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("60")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("70")).hasSize(1); + } + + @Test + void instanceWithoutSubsetsShouldBeSkipped() { + V1Endpoints endpoints = new V1EndpointsBuilder().withNewMetadata().withName("endpoint1").withNamespace("test") + .withLabels(Collections.emptyMap()).endMetadata().build(); + endpointsCache.add(endpoints); + + V1Service service = new V1ServiceBuilder().withNewMetadata().withName("endpoint1").withNamespace("test").and() + .build(); + servicesCache.add(service); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint1"); + + assertThat(instances).isEmpty(); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePortsUsingPrimaryPortNameLabel() { + Map labels = Map.of("primary-port-name", "https"); + String serviceId = "endpoint2"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("80"); + List names = List.of("http", "https"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(80, 443); + List appProtocols = List.of("http", "https"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint2"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("80")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndMisconfiguredPrimaryPortNameInLabelWithoutFallbackShouldLogWarning() { + Map labels = Map.of("primary-port-name", "oops"); + String serviceId = "endpoint3"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("90"); + List names = List.of("httpA", "httpB", "httpC", "httpD"); + List protocols = List.of("TCP", "TCP", "TCP", "TCP"); + List ports = List.of(8443, 443, 80, 8080); + List appProtocols = List.of("https1", "https2", "http1", "http2"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint3"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("90")).hasSize(1).hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndMisconfiguredGenericPrimaryPortNameWithoutFallbackShouldLogWarning() { + Map labels = Map.of(); + String serviceId = "endpoint4"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("100"); + List names = List.of("httpA", "httpB", "httpC", "httpD"); + List protocols = List.of("TCP", "TCP", "TCP", "TCP"); + List ports = List.of(8443, 443, 80, 8080); + List appProtocols = List.of("https1", "https2", "http1", "http2"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), "oops", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint4"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("100")).hasSize(1).hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldFallBackToHttps() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("110"); + List names = List.of("httpA", "httpB"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(443, 80); + List appProtocols = List.of("http", "https"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("110")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedOrHttpsPortShouldFallBackToHttp() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("120"); + List names = List.of("httpA", "httpB", "httpC"); + List protocols = List.of("http", "http", "http"); + List ports = List.of(80, 8443, 80); + List appProtocols = List.of("TCP", "TCP", "TCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, + KubernetesDiscoveryProperties.DEFAULT); + + List instances = discoveryClient.getInstances("endpoint5"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("120")).hasSize(1).filteredOn(s -> 80 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldLogWarning() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("130"); + List names = List.of("http", "https"); + List protocols = List.of("http", "https"); + List ports = List.of(80, 443); + List appProtocols = List.of("TCP", "TCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + true, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + // We're returning the first discovered port to not change previous behaviour + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("130")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + public void instanceWithoutPorts() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("130"); + List names = List.of(); + List protocols = List.of(); + List ports = List.of(); + List appProtocols = List.of(); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = KubernetesDiscoveryProperties.DEFAULT; + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + // We're returning the first discovered port to not change previous behaviour + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getUri().toASCIIString().equals("http://ip1")) + .filteredOn(s -> s.getInstanceId().equals("130")).hasSize(1).filteredOn(s -> 0 == s.getPort()) + .hasSize(1); + } + + private void setup(String serviceId, String serviceType, String namespace, Map labels, + List ips, List uuids, List names, List protocols, List ports, + List appProtocols) { + + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().withType(serviceType).build()) + .withNewMetadata().withName(serviceId).withNamespace(namespace).withLabels(labels).endMetadata() + .build(); + + servicesCache.add(service); + + V1ObjectMeta objectMeta = new V1ObjectMeta(); + objectMeta.setNamespace(namespace); + objectMeta.setName(serviceId); + + V1Endpoints endpoints = new V1EndpointsBuilder().withNewMetadata().withName(serviceId).withNamespace(namespace) + .withLabels(labels).endMetadata().build(); + + List addresses = new ArrayList<>(); + for (int i = 0; i < ips.size(); ++i) { + V1EndpointAddress address = new V1EndpointAddressBuilder().withIp(ips.get(i)).withNewTargetRef() + .withUid(uuids.get(i)).endTargetRef().build(); + addresses.add(address); + } + + V1EndpointSubset subset = new V1EndpointSubset(); + subset.setAddresses(addresses); + + List corePorts = new ArrayList<>(); + for (int i = 0; i < names.size(); ++i) { + CoreV1EndpointPort port = new CoreV1EndpointPortBuilder().withName(names.get(i)) + .withProtocol(protocols.get(i)).withPort(ports.get(i)).withAppProtocol(appProtocols.get(i)).build(); + corePorts.add(port); + } + subset.setPorts(corePorts); + endpoints.setSubsets(List.of(subset)); + + endpointsCache.add(endpoints); + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java index 4f3551e628..4cf2620b7a 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java @@ -17,10 +17,16 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceBuilder; @@ -32,6 +38,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.endpointSubsetsPortData; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.matchesServiceLabels; /** @@ -161,6 +168,139 @@ void testFour(CapturedOutput output) { Assertions.assertTrue(output.getOut().contains("Service labels from service : {a=b, c=d}")); } + @Test + void testPortsDataOne() { + List endpointSubsets = List.of( + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8081).withName("").build()).build(), + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8080).withName("https").build()).build()); + + Map portsData = endpointSubsetsPortData(endpointSubsets); + Assertions.assertEquals(portsData.size(), 2); + Assertions.assertEquals(portsData.get("https"), 8080); + Assertions.assertEquals(portsData.get(""), 8081); + } + + @Test + void testPortsDataTwo() { + List endpointSubsets = List.of( + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8081).withName("http").build()).build(), + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8080).withName("https").build()).build()); + + Map portsData = endpointSubsetsPortData(endpointSubsets); + Assertions.assertEquals(portsData.size(), 2); + Assertions.assertEquals(portsData.get("https"), 8080); + Assertions.assertEquals(portsData.get("http"), 8081); + } + + @Test + void endpointSubsetPortsDataWithoutPorts() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder().build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 0); + } + + @Test + void endpointSubsetPortsDataSinglePort() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withName("name").withPort(80).build()).build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get("name"), 80); + } + + @Test + void endpointSubsetPortsDataSinglePortNoName() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(80).build()).build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(""), 80); + } + + /** + *
+	 *      - ready addresses are empty
+	 *      - not ready addresses are not included
+	 * 
+ */ + @Test + void testEmptyAddresses() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder().build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 0); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses are not included
+	 * 
+ */ + @Test + void testReadyAddressesOnly() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 2); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses has a single entry, but we do not take it
+	 * 
+ */ + @Test + void testReadyAddressesTakenNotReadyAddressesNotTaken() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .withNotReadyAddresses(new V1EndpointAddressBuilder().withHostname("three").build()).build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 2); + List hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList(); + Assertions.assertEquals(hostNames, List.of("one", "two")); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses has a single entry, but we do not take it
+	 * 
+ */ + @Test + void testBothAddressesTaken() { + boolean includeNotReadyAddresses = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .withNotReadyAddresses(new V1EndpointAddressBuilder().withHostname("three").build()).build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 3); + List hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList(); + Assertions.assertEquals(hostNames, List.of("one", "three", "two")); + } + // preserve order for testing reasons private Map ordered(Map input) { return input.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect( diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java index 626009fa2b..a83aeeac37 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java @@ -21,16 +21,33 @@ import java.util.Map; import java.util.Set; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Cache; import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodBuilder; import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -38,6 +55,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.Assertions.assertThat; class KubernetesInformerDiscoveryClientTests { @@ -89,8 +107,8 @@ void testServiceWithUnsetPortNames() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1").toArray()) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("", "80", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "1.1.1.1", 80, + Map.of("port.", "80", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -136,11 +154,10 @@ void testDiscoveryInstancesWithServiceLabels() { assertThat(discoveryClient.getInstances("test-svc-1").toArray()).isEmpty(); assertThat(discoveryClient.getInstances("test-svc-3").toArray()) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-3", "2.2.2.2", 8080, Map.of("spring", "true", "", "8080", "k8s", - "true", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + .containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-3", "2.2.2.2", 8080, Map.of("spring", "true", "port.", "8080", "k8s", + "true", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -188,8 +205,8 @@ void testDiscoveryGetInstanceAllNamespaceShouldWork() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -202,8 +219,8 @@ void testDiscoveryGetInstanceOneNamespaceShouldWork() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -232,8 +249,8 @@ void testDiscoveryGetInstanceWithNotReadyAddressesIncludedShouldWork() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -250,7 +267,7 @@ void instanceWithoutEndpointsShouldBeSkipped() { } @Test - void instanceWithoutPortsShouldBeSkipped() { + void instanceWithoutPortsWillNotBeSkipped() { Lister serviceLister = setupServiceLister(SERVICE_1); Lister endpointsLister = setupEndpointsLister(ENDPOINTS_NO_PORTS); @@ -258,7 +275,9 @@ void instanceWithoutPortsShouldBeSkipped() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, KubernetesDiscoveryProperties.DEFAULT); - assertThat(discoveryClient.getInstances("test-svc-1")).isEmpty(); + assertThat(discoveryClient.getInstances("test-svc-1")) + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "1.1.1.1", 0, + Map.of("k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @Test @@ -269,12 +288,10 @@ void instanceWithMultiplePortsAndPrimaryPortNameConfiguredWithLabelShouldWork() KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-1", "1.1.1.1", 443, Map.of("http", "80", "primary-port-name", "https", - "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-1", "1.1.1.1", 443, Map.of("port.http", "80", "primary-port-name", "https", + "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -287,11 +304,10 @@ void instanceWithMultiplePortsAndMisconfiguredPrimaryPortNameInLabelShouldReturn SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-1", "1.1.1.1", 80, Map.of("tcp1", "80", "primary-port-name", "oops", - "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + .containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-1", "1.1.1.1", 80, Map.of("port.tcp1", "80", "primary-port-name", "oops", + "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -302,10 +318,10 @@ void instanceWithMultiplePortsAndGenericPrimaryPortNameConfiguredShouldWork() { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, - Map.of("http", "80", "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 443, + Map.of("port.http", "80", "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -317,10 +333,10 @@ void instanceWithMultiplePortsAndMisconfiguredGenericPrimaryPortNameShouldReturn KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("tcp1", "80", "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.tcp1", "80", "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -331,10 +347,10 @@ void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldFallBackTo KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, - Map.of("http", "80", "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 443, + Map.of("port.http", "80", "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -345,10 +361,10 @@ void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedOrHttpsPortShoul KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("http", "80", "tcp", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.http", "80", "port.tcp", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + "namespace1", null)); } @Test @@ -360,10 +376,10 @@ void instanceWithMultiplePortsAndWithoutAnyConfigurationShouldPickTheFirstPort() KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("tcp1", "80", "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.tcp1", "80", "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -375,11 +391,11 @@ void getInstancesShouldReturnInstancesWithTheSameServiceIdFromNamespaces() { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly( - new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null), - new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace2", "type", "ClusterIP"), false, + new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace2", "type", "ClusterIP"), false, "namespace2", null)); } @@ -496,6 +512,94 @@ void testServicesWithSameMetadataLabels() { assertThat(serviceInstances.get(1).getMetadata().get("k8s_namespace")).isEqualTo("namespaceB"); } + @Test + void testExternalNameService() { + V1Service externalNameService = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withType("ExternalName").withExternalName("k8s-spring-b").build()) + .withNewMetadata().withLabels(Map.of("label-key", "label-value")).withAnnotations(Map.of("abc", "def")) + .withName("blue-service").withNamespace("b").endMetadata().build(); + + V1Endpoints endpoints = new V1EndpointsBuilder().withMetadata(new V1ObjectMeta().namespace("irrelevant")) + .build(); + + Lister serviceLister = setupServiceLister(externalNameService); + Lister endpointsLister = setupEndpointsLister(endpoints); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, + "labels-prefix-", true, "annotations-prefix-", true, "ports-prefix"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of("a", "b"), true, + 60L, false, "", Set.of(), Map.of(), "", metadata, 0, false, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, properties); + + List result = discoveryClient.getInstances("blue-service"); + Assertions.assertEquals(result.size(), 1); + DefaultKubernetesServiceInstance externalNameServiceInstance = (DefaultKubernetesServiceInstance) result.get(0); + Assertions.assertEquals(externalNameServiceInstance.getServiceId(), "blue-service"); + Assertions.assertEquals(externalNameServiceInstance.getHost(), "k8s-spring-b"); + Assertions.assertEquals(externalNameServiceInstance.getPort(), -1); + Assertions.assertFalse(externalNameServiceInstance.isSecure()); + Assertions.assertEquals(externalNameServiceInstance.getUri().toASCIIString(), "k8s-spring-b"); + Assertions.assertEquals(externalNameServiceInstance.getMetadata(), Map.of("k8s_namespace", "b", + "labels-prefix-label-key", "label-value", "annotations-prefix-abc", "def", "type", "ExternalName")); + } + + @Test + void testPodMetadata() { + V1Service nonExternalNameService = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").build()).withNewMetadata() + .withName("blue-service").withNamespace("a").endMetadata().build(); + + V1Endpoints endpoints = new V1EndpointsBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("blue-service").withNamespace("a").build()) + .withSubsets( + new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(8080).build()) + .withAddresses(new V1EndpointAddressBuilder().withIp("127.0.0.1").withTargetRef( + new V1ObjectReferenceBuilder().withKind("Pod").withName("my-pod").build()) + .build()) + .build()) + .build(); + + Lister serviceLister = setupServiceLister(nonExternalNameService); + Lister endpointsLister = setupEndpointsLister(endpoints); + + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + server.port()).build(); + + V1Pod pod = new V1PodBuilder().withNewMetadata().withName("my-pod").withLabels(Map.of("a", "b")) + .withAnnotations(Map.of("c", "d")).endMetadata().build(); + + WireMock.stubFor(WireMock.get("/api/v1/namespaces/a/pods/my-pod") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(pod)))); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, + "labels-prefix-", true, "annotations-prefix-", true, "ports-prefix", true, true); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of("a", "b"), + true, 60L, false, "", Set.of(), Map.of(), "", metadata, 0, false, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, properties); + discoveryClient.coreV1Api = new CoreV1Api(apiClient); + + List result = discoveryClient.getInstances("blue-service"); + Assertions.assertEquals(result.size(), 1); + DefaultKubernetesServiceInstance serviceInstance = (DefaultKubernetesServiceInstance) result.get(0); + Assertions.assertEquals(serviceInstance.getServiceId(), "blue-service"); + Assertions.assertEquals(serviceInstance.getHost(), "127.0.0.1"); + Assertions.assertEquals(serviceInstance.getPort(), 8080); + Assertions.assertFalse(serviceInstance.isSecure()); + Assertions.assertEquals(serviceInstance.getUri().toASCIIString(), "http://127.0.0.1:8080"); + Assertions.assertEquals(serviceInstance.getMetadata(), + Map.of("k8s_namespace", "a", "type", "ClusterIP", "ports-prefix", "8080")); + Assertions.assertEquals(serviceInstance.podMetadata().get("labels"), Map.of("a", "b")); + Assertions.assertEquals(serviceInstance.podMetadata().get("annotations"), Map.of("c", "d")); + + server.shutdown(); + } + private Lister setupServiceLister(V1Service... services) { Cache serviceCache = new Cache<>(); Lister serviceLister = new Lister<>(serviceCache); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapProperties.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java similarity index 60% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapProperties.java rename to spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java index 074c72500b..cae63d6ece 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapProperties.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,24 +14,18 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.client.discovery; -import org.springframework.boot.context.properties.ConfigurationProperties; +import io.kubernetes.client.informer.SharedInformerFactory; /** * @author wind57 */ -@ConfigurationProperties("from.yaml") -public class ConfigMapProperties { +final class SharedInformerFactoryStub extends SharedInformerFactory { - private String key1; + @Override + public void startAllRegisteredInformers() { - public String getKey1() { - return key1; - } - - public void setKey1(String key1) { - this.key1 = key1; } } diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java new file mode 100644 index 0000000000..1aea2b6036 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.informer.ResourceEventHandler; +import io.kubernetes.client.informer.SharedInformer; +import io.kubernetes.client.informer.TransformFunc; + +/** + * @author wind57 + */ +final class SharedInformerStub implements SharedInformer { + + @Override + public void addEventHandler(ResourceEventHandler handler) { + + } + + @Override + public void addEventHandlerWithResyncPeriod(ResourceEventHandler handler, long resyncPeriod) { + + } + + @Override + public void run() { + + } + + @Override + public void stop() { + + } + + // this is the only method we care about + @Override + public boolean hasSynced() { + return true; + } + + @Override + public String lastSyncResourceVersion() { + return null; + } + + @Override + public void setTransform(TransformFunc transformFunc) { + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java index 1bee1f8e3d..cd54467cfd 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/catalog/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java @@ -67,24 +67,23 @@ void kubernetesDiscoveryEnabled() { applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); } - // disabling discovery has no impact on the catalog watch. + // disabling discovery, disables catalog watcher. @Test void kubernetesDiscoveryDisabled() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); - applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); + applicationContextRunner.run(context -> assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class)); } /** - * both blocking and reactive configs are disabled, should not influence catalog - * watcher in any way. + * both blocking and reactive configs are disabled, catalog watcher is disabled also. */ @Test void disableBlockingAndReactive() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); assertThat(context).doesNotHaveBean(SharedInformerFactory.class); @@ -94,15 +93,39 @@ void disableBlockingAndReactive() { } /** - * spring.cloud.kubernetes.discovery.enabled is false, but does not influence catalog - * watcher. + * blocking is disabled, reactive is enabled, catalog watcher is enabled. + */ + @Test + void disableBlockingEnableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=true"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + }); + } + + /** + * blocking is enabled, reactive is disabled, catalog watcher is enabled. + */ + @Test + void enableBlockingDisableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=true", "spring.cloud.discovery.reactive.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + }); + } + + /** + * spring.cloud.kubernetes.discovery.enabled is false, catalog watcher is disabled + * also. */ @Test void disableKubernetesDiscovery() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); }); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java index 16d98059e3..a5b9e32124 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java @@ -30,6 +30,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -128,8 +129,8 @@ void testDiscoveryGetInstanceAllNamespaceShouldWork() { kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) - .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("type", "ClusterIP", "", "8080", "k8s_namespace", "namespace1"), false, + .expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("type", "ClusterIP", "port.", "8080", "k8s_namespace", "namespace1"), false, "namespace1", null)) .expectComplete().verify(); @@ -149,8 +150,8 @@ void testDiscoveryGetInstanceOneNamespaceShouldWork() { kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) - .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("type", "ClusterIP", "", "8080", "k8s_namespace", "namespace1"), false, + .expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("type", "ClusterIP", "port.", "8080", "k8s_namespace", "namespace1"), false, "namespace1", null)) .expectComplete().verify(); @@ -237,9 +238,11 @@ void testAllNamespacesTwoEndpointsPresent() { boolean allNamespaces = true; V1Service serviceXNamespaceA = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); V1Service serviceXNamespaceB = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); serviceCache.add(serviceXNamespaceA); serviceCache.add(serviceXNamespaceB); @@ -286,9 +289,11 @@ void testAllSingleTwoEndpointsPresent() { boolean allNamespaces = true; V1Service serviceXNamespaceA = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); V1Service serviceXNamespaceB = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); serviceCache.add(serviceXNamespaceA); serviceCache.add(serviceXNamespaceB); diff --git a/spring-cloud-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-client-loadbalancer/pom.xml index dbed72cbe6..1c1bd42db3 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/pom.xml +++ b/spring-cloud-kubernetes-client-loadbalancer/pom.xml @@ -6,7 +6,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java index 0f54c87a3d..878db3a11a 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,10 @@ package org.springframework.cloud.kubernetes.client.loadbalancer; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.ConditionalOnKubernetesLoadBalancerEnabled; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.context.annotation.Bean; @@ -32,7 +32,7 @@ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(KubernetesLoadBalancerProperties.class) @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) -@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true) +@ConditionalOnKubernetesLoadBalancerEnabled @LoadBalancerClients(defaultConfiguration = KubernetesClientLoadBalancerClientConfiguration.class) public class KubernetesClientLoadBalancerAutoConfiguration { diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java index 2a0585303d..9d57b3bc01 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.ConditionalOnKubernetesLoadBalancerServiceModeEnabled; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -32,7 +32,7 @@ public class KubernetesClientLoadBalancerClientConfiguration { @Bean - @ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE") + @ConditionalOnKubernetesLoadBalancerServiceModeEnabled ServiceInstanceListSupplier kubernetesServicesListSupplier(Environment environment, CoreV1Api coreV1Api, KubernetesClientServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties, KubernetesNamespaceProvider kubernetesNamespaceProvider, ConfigurableApplicationContext context) { diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java index 2d6cb01026..1d1d8b7c45 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; @@ -37,27 +36,20 @@ /** * @author Ryan Baxter */ -public class KubernetesClientServicesListSupplier extends KubernetesServicesListSupplier { +public class KubernetesClientServicesListSupplier extends KubernetesServicesListSupplier { private static final Log LOG = LogFactory.getLog(KubernetesClientServicesListSupplier.class); - private CoreV1Api coreV1Api; + private final CoreV1Api coreV1Api; - private KubernetesClientProperties kubernetesClientProperties; + private final String namespace; - private KubernetesNamespaceProvider kubernetesNamespaceProvider; - - public KubernetesClientServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper, - KubernetesDiscoveryProperties discoveryProperties, CoreV1Api coreV1Api, - KubernetesNamespaceProvider kubernetesNamespaceProvider) { + public KubernetesClientServicesListSupplier(Environment environment, + KubernetesServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties, + CoreV1Api coreV1Api, KubernetesNamespaceProvider kubernetesNamespaceProvider) { super(environment, mapper, discoveryProperties); this.coreV1Api = coreV1Api; - this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; - } - - private String getNamespace() { - return kubernetesNamespaceProvider != null ? kubernetesNamespaceProvider.getNamespace() - : kubernetesClientProperties.namespace(); + this.namespace = kubernetesNamespaceProvider.getNamespace(); } @Override @@ -68,11 +60,11 @@ public Flux> get() { try { if (discoveryProperties.allNamespaces()) { services = coreV1Api.listServiceForAllNamespaces(null, null, "metadata.name=" + this.getServiceId(), - null, null, null, null, null, null, null).getItems(); + null, null, null, null, null, null, null, null).getItems(); } else { - services = coreV1Api.listNamespacedService(getNamespace(), null, null, null, - "metadata.name=" + this.getServiceId(), null, null, null, null, null, null).getItems(); + services = coreV1Api.listNamespacedService(namespace, null, null, null, + "metadata.name=" + this.getServiceId(), null, null, null, null, null, null, null).getItems(); } services.forEach(service -> result.add(mapper.map(service))); } diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java index bfc95cf5bb..68b8471e8b 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java @@ -20,6 +20,7 @@ import java.util.Collections; import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.util.ClientBuilder; import org.junit.jupiter.api.Test; @@ -52,13 +53,13 @@ */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KubernetesClientLoadBalancerPodModeTests.App.class) -public class KubernetesClientLoadBalancerPodModeTests { +class KubernetesClientLoadBalancerPodModeTests { @Autowired private RestTemplate restTemplate; @Test - public void testLoadBalancer() { + void testLoadBalancer() { String resp = restTemplate.getForObject("http://servicea-wiremock", String.class); assertThat(resp).isEqualTo("hello"); } @@ -67,12 +68,17 @@ public void testLoadBalancer() { static class App { @Bean - public ApiClient apiClient() { + CoreV1Api coreV1Api(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + + @Bean + ApiClient apiClient() { return new ClientBuilder().build(); } @Bean - public BlockingLoadBalancerClient blockingLoadBalancerClient() { + BlockingLoadBalancerClient blockingLoadBalancerClient() { BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class); try { ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK); @@ -87,14 +93,14 @@ public BlockingLoadBalancerClient blockingLoadBalancerClient() { } @Bean - public KubernetesNamespaceProvider kubernetesNamespaceProvider() { + KubernetesNamespaceProvider kubernetesNamespaceProvider() { KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); when(provider.getNamespace()).thenReturn("test"); return provider; } @Bean - public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class); ServiceInstance instance = new DefaultServiceInstance("servicea-wiremock1", "servicea-wiremock", "fake", 8888, false); diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java index bebf4b072e..3e8c23cf71 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java @@ -98,7 +98,7 @@ public CoreV1Api coreV1Api() { try { when(coreV1Api.listNamespacedService(eq("default"), eq(null), eq(null), eq(null), eq("metadata.name=servicea-wiremock"), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null))).thenReturn(SERVICE_LIST); + eq(null), eq(null))).thenReturn(SERVICE_LIST); } catch (ApiException e) { e.printStackTrace(); diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index 879d2009dc..e95e520f9d 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/ConditionalOnSanitizeSecrets.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/ConditionalOnSanitizeSecrets.java new file mode 100644 index 0000000000..0e3d6849a0 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/ConditionalOnSanitizeSecrets.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional + * spring.cloud.kubernetes.sanitize.secrets. + * + * @author wind57 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = ConditionalOnSanitizeSecrets.VALUE, matchIfMissing = false) +public @interface ConditionalOnSanitizeSecrets { + + /** + * Conditional value to use. + */ + String VALUE = "spring.cloud.kubernetes.sanitize.secrets"; + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfiguration.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfiguration.java new file mode 100644 index 0000000000..1244d1b3dd --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons; + +import java.util.Collection; + +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * @author wind57 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnClass(name = "org.springframework.boot.actuate.endpoint.SanitizableData") +@ConditionalOnSanitizeSecrets +class KubernetesCommonsSanitizeAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + SanitizingFunction secretsPropertySourceSanitizingFunction() { + return data -> { + PropertySource propertySource = data.getPropertySource(); + if (propertySource instanceof BootstrapPropertySource bootstrapPropertySource) { + PropertySource source = bootstrapPropertySource.getDelegate(); + if (source instanceof SecretsPropertySource) { + return new SanitizableData(propertySource, data.getKey(), data.getValue()) + .withValue(SanitizableData.SANITIZED_VALUE); + } + } + + if (propertySource instanceof SecretsPropertySource) { + return new SanitizableData(propertySource, data.getKey(), data.getValue()) + .withValue(SanitizableData.SANITIZED_VALUE); + } + + // at the moment, our structure is pretty simply, CompositePropertySource + // children can be SecretsPropertySource; i.e.: there is no recursion + // needed to get all children. If this structure changes, there are enough + // unit tests that will start failing. + if (propertySource instanceof CompositePropertySource compositePropertySource) { + Collection> sources = compositePropertySource.getPropertySources(); + for (PropertySource one : sources) { + if (one.containsProperty(data.getKey()) && one instanceof SecretsPropertySource) { + return new SanitizableData(propertySource, data.getKey(), data.getValue()) + .withValue(SanitizableData.SANITIZED_VALUE); + } + } + } + return data; + }; + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java index fa8d2ab473..9d639d9294 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java @@ -35,8 +35,6 @@ */ public class KubernetesNamespaceProvider { - private static final DeferredLog LOG = new DeferredLog(); - /** * Property name for namespace. */ @@ -47,6 +45,10 @@ public class KubernetesNamespaceProvider { */ public static final String NAMESPACE_PATH_PROPERTY = "spring.cloud.kubernetes.client.serviceAccountNamespacePath"; + private static final DeferredLog LOG = new DeferredLog(); + + private String namespacePropertyValue; + private BindHandler bindHandler; private String serviceAccountNamespace; @@ -65,7 +67,36 @@ public KubernetesNamespaceProvider(Binder binder, BindHandler bindHandler) { this.bindHandler = bindHandler; } + public KubernetesNamespaceProvider(String namespacePropertyValue) { + this.namespacePropertyValue = namespacePropertyValue; + } + + public static String getNamespaceFromServiceAccountFile(String path) { + String namespace = null; + LOG.debug("Looking for service account namespace at: [" + path + "]."); + Path serviceAccountNamespacePath = Paths.get(path); + boolean serviceAccountNamespaceExists = Files.isRegularFile(serviceAccountNamespacePath); + if (serviceAccountNamespaceExists) { + LOG.debug("Found service account namespace at: [" + serviceAccountNamespacePath + "]."); + + try { + namespace = new String(Files.readAllBytes((serviceAccountNamespacePath))); + LOG.debug("Service account namespace value: " + serviceAccountNamespacePath); + } + catch (IOException ioe) { + LOG.error("Error reading service account namespace from: [" + serviceAccountNamespacePath + "].", ioe); + } + + } + return namespace; + } + public String getNamespace() { + // If they provided the namespace in the constructor just return that + if (!ObjectUtils.isEmpty(namespacePropertyValue)) { + return namespacePropertyValue; + } + // No namespace provided so try to get it from another source String namespace = null; if (environment != null) { namespace = environment.getProperty(NAMESPACE_PROPERTY); @@ -96,24 +127,4 @@ private String getServiceAccountNamespace() { return serviceAccountNamespace; } - public static String getNamespaceFromServiceAccountFile(String path) { - String namespace = null; - LOG.debug("Looking for service account namespace at: [" + path + "]."); - Path serviceAccountNamespacePath = Paths.get(path); - boolean serviceAccountNamespaceExists = Files.isRegularFile(serviceAccountNamespacePath); - if (serviceAccountNamespaceExists) { - LOG.debug("Found service account namespace at: [" + serviceAccountNamespacePath + "]."); - - try { - namespace = new String(Files.readAllBytes((serviceAccountNamespacePath))); - LOG.debug("Service account namespace value: " + serviceAccountNamespacePath); - } - catch (IOException ioe) { - LOG.error("Error reading service account namespace from: [" + serviceAccountNamespacePath + "].", ioe); - } - - } - return namespace; - } - } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/autoconfig/ConditionalOnKubernetesHealthIndicatorEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/autoconfig/ConditionalOnKubernetesHealthIndicatorEnabled.java new file mode 100644 index 0000000000..2538937edd --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/autoconfig/ConditionalOnKubernetesHealthIndicatorEnabled.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.autoconfig; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.cloud.CloudPlatform; + +/** + * @author wind57 + **/ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public @interface ConditionalOnKubernetesHealthIndicatorEnabled { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java index 3e82989911..d804d2cc6d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java @@ -16,11 +16,14 @@ package org.springframework.cloud.kubernetes.commons.config; +import org.springframework.boot.BootstrapContext; import org.springframework.boot.BootstrapRegistryInitializer; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver.PropertyResolver; import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; @@ -37,18 +40,44 @@ public static boolean hasConfigServerInstanceProvider() { public static KubernetesDiscoveryProperties createKubernetesDiscoveryProperties(Binder binder, BindHandler bindHandler) { - return binder.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryProperties.class), + return binder.bind(KubernetesDiscoveryProperties.PREFIX, Bindable.of(KubernetesDiscoveryProperties.class), bindHandler).orElseGet(() -> KubernetesDiscoveryProperties.DEFAULT); } + public static KubernetesDiscoveryProperties createKubernetesDiscoveryProperties(BootstrapContext bootstrapContext) { + PropertyResolver propertyResolver = getPropertyResolver(bootstrapContext); + return propertyResolver.resolveConfigurationProperties(KubernetesDiscoveryProperties.PREFIX, + KubernetesDiscoveryProperties.class, () -> KubernetesDiscoveryProperties.DEFAULT); + } + public static KubernetesClientProperties createKubernetesClientProperties(Binder binder, BindHandler bindHandler) { return binder.bindOrCreate(KubernetesClientProperties.PREFIX, Bindable.of(KubernetesClientProperties.class)) .withNamespace(new KubernetesNamespaceProvider(binder, bindHandler).getNamespace()); } + public static KubernetesClientProperties createKubernetesClientProperties(BootstrapContext bootstrapContext) { + PropertyResolver propertyResolver = getPropertyResolver(bootstrapContext); + return getPropertyResolver(bootstrapContext) + .resolveOrCreateConfigurationProperties(KubernetesClientProperties.PREFIX, + KubernetesClientProperties.class) + .withNamespace( + propertyResolver.get(KubernetesNamespaceProvider.NAMESPACE_PROPERTY, String.class, null)); + } + public static Boolean getDiscoveryEnabled(Binder binder, BindHandler bindHandler) { return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Bindable.of(Boolean.class), bindHandler) .orElse(false); } + public static Boolean getDiscoveryEnabled(BootstrapContext bootstrapContext) { + return getPropertyResolver(bootstrapContext).get(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class, + false); + } + + protected static PropertyResolver getPropertyResolver(BootstrapContext context) { + return context.getOrElseSupply(ConfigServerConfigDataLocationResolver.PropertyResolver.class, + () -> new ConfigServerConfigDataLocationResolver.PropertyResolver(context.get(Binder.class), + context.getOrElse(BindHandler.class, null))); + } + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java index b3faabd7cc..0e59fe5d97 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java @@ -16,12 +16,14 @@ package org.springframework.cloud.kubernetes.commons.config; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -32,7 +34,6 @@ import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES; import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.PROPERTIES_TO_MAP; -import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.throwingMerger; import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.yamlParserGenerator; /** @@ -73,42 +74,77 @@ else if (propertyName.endsWith(".properties")) { return defaultProcessAllEntries(input, environment); } - private static Map defaultProcessAllEntries(Map input, Environment environment) { + /** + *
+	 * 		we want to sort entries coming from the k8s source in a specific way:
+	 *
+	 * 	    1. "application.yaml/yml/properties" have to come first
+	 * 	       (or the value from spring.application.name)
+	 * 	    2. then profile specific entries, like "application-dev.yaml"
+	 * 	    3. then plain properties
+	 * 
+ */ + static List> sorted(Map input, Environment environment) { + + record WeightedEntry(Map.Entry entry, int weight) { + + } // we pass empty Strings on purpose, the logic here is either the value of - // "spring.application.name" - // or literal "application". + // "spring.application.name" or literal "application". String applicationName = ConfigUtils.getApplicationName(environment, "", ""); String[] activeProfiles = environment.getActiveProfiles(); - Set fileNames = Stream - .concat(Stream.of(applicationName), - Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile)) - .collect(Collectors.toSet()); + // the order here is important, first has to come "application.yaml" and then + // "application-dev.yaml" + List orderedFileNames = Stream.concat(Stream.of(applicationName), + Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile)).toList(); + + int current = orderedFileNames.size() - 1; + List weightedEntries = new ArrayList<>(); + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey(); + if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) { + String withoutExtension = key.split("\\.", 2)[0]; + int index = orderedFileNames.indexOf(withoutExtension); + if (index >= 0) { + weightedEntries.add(new WeightedEntry(entry, index)); + } + else { + LOG.warn("entry : " + key + " will be skipped"); + } + } + else { + weightedEntries.add(new WeightedEntry(entry, ++current)); + } + } + + return weightedEntries.stream().sorted(Comparator.comparing(WeightedEntry::weight)).map(WeightedEntry::entry) + .toList(); + } + + private static Map defaultProcessAllEntries(Map input, Environment environment) { + + List> sortedEntries = sorted(input, environment); + Map result = new HashMap<>(); + for (Map.Entry entry : sortedEntries) { + result.putAll(extractProperties(entry.getKey(), entry.getValue(), environment)); + } + return result; - return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), fileNames, environment)) - .flatMap(m -> m.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, throwingMerger(), HashMap::new)); } - private static Map extractProperties(String resourceName, String content, Set fileNames, - Environment environment) { + private static Map extractProperties(String resourceName, String content, Environment environment) { if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) { - if (fileNames.contains(resourceName.split("\\.", 2)[0])) { - if (resourceName.endsWith(".properties")) { - LOG.debug("entry : " + resourceName + " will be treated as a single properties file"); - return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content); - } - else { - LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file"); - return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content); - } + if (resourceName.endsWith(".properties")) { + LOG.debug("entry : " + resourceName + " will be treated as a single properties file"); + return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content); } else { - LOG.warn("entry : " + resourceName + " will be skipped"); - return Collections.emptyMap(); + LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file"); + return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnBlockingOrReactiveDiscoveryEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnBlockingOrReactiveDiscoveryEnabled.java new file mode 100644 index 0000000000..bc71c2ceeb --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnBlockingOrReactiveDiscoveryEnabled.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.context.annotation.Conditional; + +/** + * Conditional that is resolved to active when either + * {@link ConditionalOnBlockingDiscoveryEnabled} or + * {@link ConditionalOnReactiveDiscoveryEnabled} matches. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Conditional(ConditionalOnBlockingOrReactiveDiscoveryEnabled.OnBlockingOrReactiveDiscoveryEnabled.class) +public @interface ConditionalOnBlockingOrReactiveDiscoveryEnabled { + + class OnBlockingOrReactiveDiscoveryEnabled extends AnyNestedCondition { + + OnBlockingOrReactiveDiscoveryEnabled() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBlockingDiscoveryEnabled + static class OnBlockingDiscoveryEnabled { + + } + + @ConditionalOnReactiveDiscoveryEnabled + static class OnReactiveDiscoveryEnabled { + + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnHttpDiscoveryCatalogWatcherEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnHttpDiscoveryCatalogWatcherEnabled.java new file mode 100644 index 0000000000..40a8392a8c --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnHttpDiscoveryCatalogWatcherEnabled.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional for: + * spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled", matchIfMissing = false) +public @interface ConditionalOnHttpDiscoveryCatalogWatcherEnabled { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnKubernetesCatalogWatcherEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnKubernetesCatalogWatcherEnabled.java new file mode 100644 index 0000000000..89337bca52 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnKubernetesCatalogWatcherEnabled.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; + +/** + * Provides common conditionals to be used for catalog watcher. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnDiscoveryEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@ConditionalOnBlockingOrReactiveDiscoveryEnabled +@ConditionalOnKubernetesCatalogEnabled +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public @interface ConditionalOnKubernetesCatalogWatcherEnabled { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscovery.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscovery.java new file mode 100644 index 0000000000..72f4988bce --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscovery.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; + +/** + * Provides common conditionals to be used for blocking discovery. + * + * @author wind57 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnDiscoveryEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@ConditionalOnBlockingDiscoveryEnabled +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public @interface ConditionalOnSpringCloudKubernetesBlockingDiscovery { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer.java new file mode 100644 index 0000000000..0f897927f7 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; + +/** + * Provides common conditionals to be used for blocking discovery health initializer. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") +@ConditionalOnDiscoveryHealthIndicatorEnabled +public @interface ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscovery.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscovery.java new file mode 100644 index 0000000000..0d39906a30 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscovery.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; + +/** + * Provides common conditionals to be used for reactive discovery. + * + * @author wind57 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnDiscoveryEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@ConditionalOnReactiveDiscoveryEnabled +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public @interface ConditionalOnSpringCloudKubernetesReactiveDiscovery { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer.java new file mode 100644 index 0000000000..b0ee2001d4 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; + +/** + * Provides common conditionals to be used for reactive discovery health initializer. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") +@ConditionalOnDiscoveryHealthIndicatorEnabled +public @interface ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java index 35ebfcea02..62d1d0a0e3 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java @@ -185,7 +185,10 @@ static Map> podMetadata(String podName, Map "Pod labels/annotations were requested"); + if (podName != null) { + LOG.debug(() -> "getting labels/annotation for pod: " + podName); PodLabelsAndAnnotations both = podLabelsAndMetadata.apply(podName); Map> result = new HashMap<>(); if (properties.metadata().addPodLabels() && !both.labels().isEmpty()) { diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java index 8d8cd991d2..c2d320e8c8 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java @@ -48,7 +48,7 @@ private void postConstruct() { InstanceRegisteredEvent instanceRegisteredEvent = new InstanceRegisteredEvent<>( new RegisteredEventSource("kubernetes", podUtils.isInsideKubernetes(), podUtils.currentPod().get()), null); - this.applicationEventPublisher.publishEvent(instanceRegisteredEvent); + applicationEventPublisher.publishEvent(instanceRegisteredEvent); } /** diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java index 6865d52a30..59d599baa1 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java @@ -82,4 +82,20 @@ private KubernetesDiscoveryConstants() { */ public static final String SECURED = "secured"; + /** + * catalog watch delay property name. + */ + public static final String CATALOG_WATCH_PROPERTY_NAME = "spring.cloud.kubernetes.discovery.catalogServicesWatchDelay"; + + /** + * default delay for the configuration watcher. + */ + public static final String CATALOG_WATCHER_DEFAULT_DELAY = "30000"; + + /** + * catalog watch delay property name with default value. + */ + public static final String CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE = CATALOG_WATCH_PROPERTY_NAME + ":" + + CATALOG_WATCHER_DEFAULT_DELAY; + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java index f5f0dfbaa6..a07f367ae4 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java @@ -48,7 +48,7 @@ * have "type: ExternalName" in their spec. */ // @formatter:off -@ConfigurationProperties("spring.cloud.kubernetes.discovery") +@ConfigurationProperties(KubernetesDiscoveryProperties.PREFIX) public record KubernetesDiscoveryProperties( @DefaultValue("true") boolean enabled, boolean allNamespaces, @DefaultValue Set namespaces, @@ -60,9 +60,15 @@ public record KubernetesDiscoveryProperties( @DefaultValue Metadata metadata, @DefaultValue("" + DEFAULT_ORDER) int order, boolean useEndpointSlices, - boolean includeExternalNameServices) { + boolean includeExternalNameServices, + String discoveryServerUrl) { // @formatter:on + /** + * Prefix of the properties. + */ + public static final String PREFIX = "spring.cloud.kubernetes.discovery"; + @ConstructorBinding public KubernetesDiscoveryProperties { @@ -75,7 +81,20 @@ public KubernetesDiscoveryProperties(@DefaultValue("true") boolean enabled, bool @DefaultValue Map serviceLabels, String primaryPortName, @DefaultValue Metadata metadata, @DefaultValue("" + DEFAULT_ORDER) int order, boolean useEndpointSlices) { this(enabled, allNamespaces, namespaces, waitCacheReady, cacheLoadingTimeoutSeconds, includeNotReadyAddresses, - filter, knownSecurePorts, serviceLabels, primaryPortName, metadata, order, useEndpointSlices, false); + filter, knownSecurePorts, serviceLabels, primaryPortName, metadata, order, useEndpointSlices, false, + null); + } + + public KubernetesDiscoveryProperties(@DefaultValue("true") boolean enabled, boolean allNamespaces, + @DefaultValue Set namespaces, @DefaultValue("true") boolean waitCacheReady, + @DefaultValue("60") long cacheLoadingTimeoutSeconds, boolean includeNotReadyAddresses, String filter, + @DefaultValue({ "443", "8443" }) Set knownSecurePorts, + @DefaultValue Map serviceLabels, String primaryPortName, @DefaultValue Metadata metadata, + @DefaultValue("" + DEFAULT_ORDER) int order, boolean useEndpointSlices, + boolean includeExternalNameServices) { + this(enabled, allNamespaces, namespaces, waitCacheReady, cacheLoadingTimeoutSeconds, includeNotReadyAddresses, + filter, knownSecurePorts, serviceLabels, primaryPortName, metadata, order, useEndpointSlices, + includeExternalNameServices, null); } /** @@ -83,7 +102,7 @@ public KubernetesDiscoveryProperties(@DefaultValue("true") boolean enabled, bool */ public static final KubernetesDiscoveryProperties DEFAULT = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, - false); + false, null); /** * @param addLabels include labels as metadata diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/EndpointSubsetNS.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/Service.java similarity index 67% rename from spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/EndpointSubsetNS.java rename to spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/Service.java index 8a7de4b74a..1fa2db1e30 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/EndpointSubsetNS.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/Service.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,13 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.commons.discovery; import java.util.List; -import io.fabric8.kubernetes.api.model.EndpointSubset; - /** - * @author Haytham Mohamed - **/ -record EndpointSubsetNS(String namespace, List endpointSubset) { + * Use for discovery service implementation. + * @author wind57 + */ +public record Service(String name, List serviceInstances) { } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerEnabled.java new file mode 100644 index 0000000000..caa165e814 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerEnabled.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.loadbalancer; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional + * spring.cloud.kubernetes.loadbalancer.enabled. + * + * @author wind57 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true) +public @interface ConditionalOnKubernetesLoadBalancerEnabled { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerServiceModeEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerServiceModeEnabled.java new file mode 100644 index 0000000000..73fd006035 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/ConditionalOnKubernetesLoadBalancerServiceModeEnabled.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.loadbalancer; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a conditional for: spring.cloud.kubernetes.loadbalancer.mode. + * + * @author wind57 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE") +public @interface ConditionalOnKubernetesLoadBalancerServiceModeEnabled { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapper.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapper.java index f77f85cc6c..57f693c828 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapper.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,52 +16,69 @@ package org.springframework.cloud.kubernetes.commons.loadbalancer; -import java.util.HashMap; import java.util.Map; +import java.util.StringJoiner; + +import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.core.log.LogAccessor; import org.springframework.util.StringUtils; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.keysWithPrefix; + /** * @author Ryan Baxter */ public interface KubernetesServiceInstanceMapper { + /** + * Logger instance. + */ + LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesServiceInstanceMapper.class)); + KubernetesServiceInstance map(T service); static String createHost(String serviceName, String namespace, String clusterDomain) { - return String.format("%s.%s.svc.%s", serviceName, StringUtils.hasText(namespace) ? namespace : "default", - clusterDomain); + String namespaceToUse = StringUtils.hasText(namespace) ? namespace : "default"; + return new StringJoiner(".").add(serviceName).add(namespaceToUse).add("svc").add(clusterDomain).toString(); } static boolean isSecure(Map labels, Map annotations, String servicePortName, Integer servicePort) { - if (labels != null) { - final String securedLabelValue = labels.getOrDefault("secured", "false"); - if (securedLabelValue.equals("true")) { - return true; - } + + if (hasTrueSecuredValue(labels)) { + LOG.debug(() -> "Service has a true 'secured' label"); + return true; + } + + if (hasTrueSecuredValue(annotations)) { + LOG.debug(() -> "Service has a true 'secured' annotation"); + return true; } - if (annotations != null) { - final String securedAnnotationValue = annotations.getOrDefault("secured", "false"); - if (securedAnnotationValue.equals("true")) { - return true; - } + if (servicePortName != null && servicePortName.endsWith("https")) { + LOG.debug(() -> "Service port name ends with 'https'"); + return true; } - return (servicePortName != null && servicePortName.endsWith("https")) || servicePort.toString().endsWith("443"); + + if (servicePort != null && servicePort.toString().endsWith("443")) { + LOG.debug(() -> "Service port ends with '443'"); + return true; + } + + return false; } static Map getMapWithPrefixedKeys(Map map, String prefix) { - if (map == null) { - return new HashMap<>(); - } - if (!StringUtils.hasText(prefix)) { - return map; + return keysWithPrefix(map, prefix); + } + + private static boolean hasTrueSecuredValue(Map input) { + if (input != null) { + return "true".equals(input.get("secured")); } - final Map result = new HashMap<>(); - map.forEach((k, v) -> result.put(prefix + k, v)); - return result; + return false; } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServicesListSupplier.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServicesListSupplier.java index a9b485b3fd..33cd0eddcf 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServicesListSupplier.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServicesListSupplier.java @@ -32,15 +32,15 @@ * * @author Piotr Minkowski */ -public abstract class KubernetesServicesListSupplier implements ServiceInstanceListSupplier { +public abstract class KubernetesServicesListSupplier implements ServiceInstanceListSupplier { protected final Environment environment; protected final KubernetesDiscoveryProperties discoveryProperties; - protected final KubernetesServiceInstanceMapper mapper; + protected final KubernetesServiceInstanceMapper mapper; - public KubernetesServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper, + public KubernetesServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties) { this.environment = environment; this.discoveryProperties = discoveryProperties; diff --git a/spring-cloud-kubernetes-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-kubernetes-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6708f3f4bd..1b5dcabb6f 100644 --- a/spring-cloud-kubernetes-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-kubernetes-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,5 @@ org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration +org.springframework.cloud.kubernetes.commons.KubernetesCommonsSanitizeAutoConfiguration org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadAutoConfiguration org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadPropertiesAutoConfiguration org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfigurationTests.java new file mode 100644 index 0000000000..b7dd4a6f7a --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsSanitizeAutoConfigurationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesCommonsSanitizeAutoConfigurationTests { + + @Test + void sanitizeSecretsNotEnabled() { + contextRunner(false, false) + .run(context -> assertThat(context.getBeansOfType(SanitizingFunction.class)).isEmpty()); + } + + @Test + void sanitizeSecretsEnabled() { + contextRunner(true, false) + .run(context -> assertThat(context.getBeansOfType(SanitizingFunction.class)).hasSize(1)); + } + + @Test + void sanitizeSecretsEnabledSanitizedClassNotPresent() { + contextRunner(true, true) + .run(context -> assertThat(context.getBeansOfType(SanitizingFunction.class)).isEmpty()); + } + + private ApplicationContextRunner contextRunner(boolean enableSanitizeSecrets, boolean filterSanitizedDataClass) { + + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(KubernetesCommonsSanitizeAutoConfiguration.class)); + + if (enableSanitizeSecrets) { + contextRunner = contextRunner.withPropertyValues(ConditionalOnSanitizeSecrets.VALUE + "=true"); + } + + if (filterSanitizedDataClass) { + contextRunner = contextRunner.withClassLoader(new FilteredClassLoader(SanitizableData.class)); + } + + contextRunner = contextRunner.withPropertyValues("spring.main.cloud-platform=KUBERNETES"); + + return contextRunner; + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/SanitizeTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/SanitizeTests.java new file mode 100644 index 0000000000..ba87eac7c3 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/SanitizeTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.actuate.endpoint.Sanitizer; +import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; + +import static org.springframework.boot.actuate.endpoint.SanitizableData.SANITIZED_VALUE; + +/** + * @author wind57 + */ +class SanitizeTests { + + private static final boolean SHOW_UNSANITIZED = true; + + private static final List SANITIZING_FUNCTIONS = List + .of(new KubernetesCommonsSanitizeAutoConfiguration().secretsPropertySourceSanitizingFunction()); + + @Test + void bootstrapPropertySourceNotSecrets() { + + BootstrapPropertySource bootstrapPropertySource = new BootstrapPropertySource<>( + new EnumerablePropertySource<>("enumerable") { + @Override + public String[] getPropertyNames() { + return new String[0]; + } + + @Override + public Object getProperty(String name) { + return null; + } + }); + + Sanitizer sanitizer = new Sanitizer(SANITIZING_FUNCTIONS); + SanitizableData sanitizableData = new SanitizableData(bootstrapPropertySource, "secret", "xyz"); + + Assertions.assertEquals(sanitizer.sanitize(sanitizableData, SHOW_UNSANITIZED), "xyz"); + } + + @Test + void bootstrapPropertySourceSecrets() { + + BootstrapPropertySource bootstrapPropertySource = new BootstrapPropertySource<>( + new SecretsPropertySource(new SourceData("secret-source", Map.of()))); + + Sanitizer sanitizer = new Sanitizer(SANITIZING_FUNCTIONS); + SanitizableData sanitizableData = new SanitizableData(bootstrapPropertySource, "secret", "xyz"); + + Assertions.assertEquals(sanitizer.sanitize(sanitizableData, SHOW_UNSANITIZED), SANITIZED_VALUE); + } + + @Test + void notSecretsPropertySource() { + + BootstrapPropertySource bootstrapPropertySource = new BootstrapPropertySource<>( + new MountConfigMapPropertySource("mount-source", Map.of())); + + Sanitizer sanitizer = new Sanitizer(SANITIZING_FUNCTIONS); + SanitizableData sanitizableData = new SanitizableData(bootstrapPropertySource, "secret", "xyz"); + + Assertions.assertEquals(sanitizer.sanitize(sanitizableData, SHOW_UNSANITIZED), "xyz"); + } + + @Test + void secretsPropertySource() { + + BootstrapPropertySource bootstrapPropertySource = new BootstrapPropertySource<>( + new SecretsPropertySource(new SourceData("secret-source", Map.of()))); + + Sanitizer sanitizer = new Sanitizer(SANITIZING_FUNCTIONS); + SanitizableData sanitizableData = new SanitizableData(bootstrapPropertySource, "secret", "xyz"); + + Assertions.assertEquals(sanitizer.sanitize(sanitizableData, SHOW_UNSANITIZED), SANITIZED_VALUE); + } + + @Test + void compositeOneSecretOneMount() { + CompositePropertySource compositePropertySource = new CompositePropertySource("composite"); + compositePropertySource.addFirstPropertySource( + new SecretsPropertySource(new SourceData("secret-source", Map.of("secret", "xyz")))); + compositePropertySource + .addFirstPropertySource(new MountConfigMapPropertySource("mount-source", Map.of("mount", "abc"))); + + Sanitizer sanitizer = new Sanitizer(SANITIZING_FUNCTIONS); + SanitizableData sanitizableDataSecret = new SanitizableData(compositePropertySource, "secret", "xyz"); + SanitizableData sanitizableDataMount = new SanitizableData(compositePropertySource, "mount", "abc"); + + Assertions.assertEquals(sanitizer.sanitize(sanitizableDataSecret, SHOW_UNSANITIZED), SANITIZED_VALUE); + Assertions.assertEquals(sanitizer.sanitize(sanitizableDataMount, SHOW_UNSANITIZED), "abc"); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java new file mode 100644 index 0000000000..f902fa15b7 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +class SourceDataEntriesProcessorOrderedPropertiesTests { + + /** + *
+	 *     - a single property is present
+	 * 
+ */ + @Test + void testSingleNonFileProperty() { + Map map = new LinkedHashMap<>(); + map.put("my-key", "my-value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("my-key", "my-value"), result); + } + + /** + *
+	 *     - a single property from a properties file
+	 * 
+ */ + @Test + void testSingleFileProperty() { + Map map = new LinkedHashMap<>(); + map.put("application.properties", "my-key=from-app"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("my-key", "from-app"), result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	}
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that the property overrides the value from "application.properties".
+	 * 
+ */ + @Test + void testThree() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties"""); + map.put("firstKey", "abc"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromProperties"), result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	    thirdKey=thirdFromProperties
+	 *     	}
+	 *
+	 *     	- application-dev.properties contains:
+	 *     	  {
+	 *     	  	  firstKey=firstFromDevProperties
+	 *     	      secondKey=secondFromDevProperties
+	 *     	  }
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that profile specific properties override non-profile
+	 *     	  and plain properties override everything.
+	 * 
+ */ + @Test + void testFour() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties + thirdKey=thirdFromProperties"""); + map.put("application-dev.properties", """ + firstKey=firstFromDevProperties + secondKey=secondFromDevProperties"""); + map.put("firstKey", "abc"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals( + Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey", "thirdFromProperties"), + result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	    thirdKey=thirdFromProperties
+	 *     	}
+	 *
+	 *     	- application-dev.properties contains:
+	 *     	  {
+	 *     	  	  firstKey=firstFromDevProperties
+	 *     	      secondKey=secondFromDevProperties
+	 *     	  }
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that profile specific properties override non-profile
+	 *     	  and plain properties override everything.
+	 *     	  It also proves that non-active profile properties are ignored.
+	 * 
+ */ + @Test + void testFive() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties + thirdKey=thirdFromProperties"""); + map.put("application-dev.properties", """ + firstKey=firstFromDevProperties + secondKey=secondFromDevProperties"""); + map.put("application-k8s.properties", """ + firstKey=firstFromK8sProperties + secondKey=secondFromK8sProperties"""); + map.put("firstKey", "abc"); + map.put("fourthKey", "def"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey", + "thirdFromProperties", "fourthKey", "def"), result); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java new file mode 100644 index 0000000000..34106fc9cc --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +class SourceDataEntriesProcessorSortedTests { + + @Test + void testSingleNonFileProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple-property", "value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getKey(), "simple-property"); + Assertions.assertEquals(result.get(0).getValue(), "value"); + } + + @Test + void testTwoNonFileProperties() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("one", "1"); + k8sSource.put("two", "2"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "one"); + Assertions.assertEquals(result.get(0).getValue(), "1"); + + Assertions.assertEquals(result.get(1).getKey(), "two"); + Assertions.assertEquals(result.get(1).getValue(), "2"); + } + + @Test + void testSingleFileProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("application.properties", "key=value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + } + + @Test + void testApplicationAndSimpleProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("simple", "other_value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + } + + @Test + void testSimplePropertyAndApplication() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("application.properties", "key=value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + } + + @Test + void testSimplePropertyAndTwoApplications() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "application-dev.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-dev=value-dev"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + } + + @Test + void testComplex() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("application-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "application-k8s.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + + Assertions.assertEquals(result.get(3).getKey(), "second-simple"); + Assertions.assertEquals(result.get(3).getValue(), "second_other_value"); + } + + @Test + void testComplexWithNonDefaultApplicationName() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("application-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + + Assertions.assertEquals(result.get(0).getKey(), "simple"); + Assertions.assertEquals(result.get(0).getValue(), "other_value"); + + Assertions.assertEquals(result.get(1).getKey(), "second-simple"); + Assertions.assertEquals(result.get(1).getValue(), "second_other_value"); + } + + @Test + void testComplexWithNonDefaultApplicationNameMoreMatches() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("sorted.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result.get(0).getKey(), "sorted.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "sorted-k8s.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + + Assertions.assertEquals(result.get(3).getKey(), "second-simple"); + Assertions.assertEquals(result.get(3).getValue(), "second_other_value"); + } + + @Test + void testProfileBasedOnly() { + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result.get(0).getKey(), "sorted-k8s.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + + Assertions.assertEquals(result.get(2).getKey(), "second-simple"); + Assertions.assertEquals(result.get(2).getValue(), "second_other_value"); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java index f3d62f1cda..74e9f5e54e 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java @@ -54,6 +54,7 @@ void testBindingWhenNoPropertiesProvided() { assertThat(props.order()).isZero(); assertThat(props.useEndpointSlices()).isFalse(); assertThat(props.includeExternalNameServices()).isFalse(); + assertThat(props.discoveryServerUrl()).isNull(); }); } @@ -66,7 +67,8 @@ void testBindingWhenSomePropertiesProvided() { "spring.cloud.kubernetes.discovery.use-endpoint-slices=true", "spring.cloud.kubernetes.discovery.namespaces[0]=ns1", "spring.cloud.kubernetes.discovery.namespaces[1]=ns2", - "spring.cloud.kubernetes.discovery.include-external-name-services=true") + "spring.cloud.kubernetes.discovery.include-external-name-services=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://example") .run(context -> { KubernetesDiscoveryProperties props = context.getBean(KubernetesDiscoveryProperties.class); assertThat(props).isNotNull(); @@ -87,6 +89,7 @@ void testBindingWhenSomePropertiesProvided() { assertThat(props.order()).isZero(); assertThat(props.useEndpointSlices()).isTrue(); assertThat(props.includeExternalNameServices()).isTrue(); + assertThat(props.discoveryServerUrl()).isEqualTo("http://example"); }); } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesLoadBalancerPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesLoadBalancerPropertiesTests.java new file mode 100644 index 0000000000..5014a36a12 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesLoadBalancerPropertiesTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.loadbalancer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesLoadBalancerPropertiesTests { + + @Test + void testBindingWhenNoPropertiesProvided() { + new ApplicationContextRunner().withUserConfiguration(KubernetesLoadBalancerPropertiesTests.Config.class) + .run(context -> { + KubernetesLoadBalancerProperties props = context.getBean(KubernetesLoadBalancerProperties.class); + assertThat(props).isNotNull(); + assertThat(props.getEnabled()).isTrue(); + assertThat(props.getMode()).isEqualTo(KubernetesLoadBalancerMode.POD); + assertThat(props.getClusterDomain()).isEqualTo("cluster.local"); + assertThat(props.getPortName()).isEqualTo("http"); + }); + } + + @Test + void testBindingWhenSomePropertiesProvided() { + new ApplicationContextRunner().withUserConfiguration(KubernetesLoadBalancerPropertiesTests.Config.class) + .withPropertyValues("spring.cloud.kubernetes.loadbalancer.enabled=false", + "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", + "spring.cloud.kubernetes.loadbalancer.clusterDomain=clusterDomain", + "spring.cloud.kubernetes.loadbalancer.portName=portName") + .run(context -> { + KubernetesLoadBalancerProperties props = context.getBean(KubernetesLoadBalancerProperties.class); + assertThat(props).isNotNull(); + assertThat(props.getEnabled()).isFalse(); + assertThat(props.getMode()).isEqualTo(KubernetesLoadBalancerMode.SERVICE); + assertThat(props.getClusterDomain()).isEqualTo("clusterDomain"); + assertThat(props.getPortName()).isEqualTo("portName"); + }); + } + + @Configuration + @EnableConfigurationProperties(KubernetesLoadBalancerProperties.class) + static class Config { + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapperTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapperTests.java new file mode 100644 index 0000000000..8ae3c77aef --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/loadbalancer/KubernetesServiceInstanceMapperTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.loadbalancer; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +@ExtendWith(OutputCaptureExtension.class) +class KubernetesServiceInstanceMapperTests { + + private static final String SECURED_LABEL_MESSAGE = "Service has a true 'secured' label"; + + private static final String SECURED_ANNOTATION_MESSAGE = "Service has a true 'secured' annotation"; + + private static final String NAME_ENDS_IN_HTTPS_MESSAGE = "Service port name ends with 'https'"; + + private static final String PORT_ENDS_IN_443_MESSAGE = "Service port ends with '443'"; + + @Test + void testCreateHostWithNamespace() { + String namespace = "customNamespace"; + String host = KubernetesServiceInstanceMapper.createHost("serviceName", namespace, "clusterDomain"); + assertThat(host).isEqualTo("serviceName.customNamespace.svc.clusterDomain"); + } + + @Test + void testCreateHostWithEmptyNamespace() { + String host = KubernetesServiceInstanceMapper.createHost("serviceName", "", "clusterDomain"); + assertThat(host).isEqualTo("serviceName.default.svc.clusterDomain"); + } + + @Test + void testCreateHostWithNullNamespace() { + String host = KubernetesServiceInstanceMapper.createHost("serviceName", null, "clusterDomain"); + assertThat(host).isEqualTo("serviceName.default.svc.clusterDomain"); + } + + @Test + void testIsSecureWithTrueLabel(CapturedOutput output) { + Map labels = Map.of("secured", "true"); + Map annotations = Map.of(); + String servicePortName = null; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isTrue(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isTrue(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsSecureWithTrueAnnotation(CapturedOutput output) { + // empty labels + Map labels = Map.of(); + Map annotations = Map.of("secured", "true"); + String servicePortName = null; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isTrue(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isTrue(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsSecureWithTrueAnnotationNullLabels(CapturedOutput output) { + // null labels + Map labels = null; + Map annotations = Map.of("secured", "true"); + String servicePortName = null; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isTrue(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isTrue(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsNotSecureServicePortNameAndServicePortAreNull(CapturedOutput output) { + // null labels + Map labels = null; + // null annotations + Map annotations = null; + String servicePortName = null; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isFalse(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsNotSecureServicePortNameDoesNotMatch(CapturedOutput output) { + // null labels + Map labels = null; + // null annotations + Map annotations = null; + String servicePortName = "abc_https_def"; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isFalse(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsSecureServicePortNameMatches(CapturedOutput output) { + // null labels + Map labels = null; + // null annotations + Map annotations = null; + String servicePortName = "abc_https"; + Integer servicePort = null; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isTrue(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isTrue(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsNotSecureServicePortDoesNotMatch(CapturedOutput output) { + // null labels + Map labels = null; + // null annotations + Map annotations = null; + String servicePortName = null; + Integer servicePort = 444; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isFalse(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isFalse(); + } + + @Test + void testIsSecureServicePortMatches(CapturedOutput output) { + // null labels + Map labels = null; + // null annotations + Map annotations = null; + String servicePortName = null; + Integer servicePort = 443; + assertThat(KubernetesServiceInstanceMapper.isSecure(labels, annotations, servicePortName, servicePort)) + .isTrue(); + + assertThat(output.getOut().contains(SECURED_LABEL_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(SECURED_ANNOTATION_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(NAME_ENDS_IN_HTTPS_MESSAGE)).isFalse(); + assertThat(output.getOut().contains(PORT_ENDS_IN_443_MESSAGE)).isTrue(); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-commons/src/test/resources/logback-test.xml index 13afbf0cd2..3d389bcaeb 100644 --- a/spring-cloud-kubernetes-commons/src/test/resources/logback-test.xml +++ b/spring-cloud-kubernetes-commons/src/test/resources/logback-test.xml @@ -14,5 +14,6 @@ + diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml index f3452c1e9b..30c0b8799a 100644 --- a/spring-cloud-kubernetes-controllers/pom.xml +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 pom diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml index 5d9640212d..a9e93d679e 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-controllers org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -13,7 +13,7 @@ springcloud - 4.1.0 + 4.8.0 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java index aa9d4f80e7..fcb6e586f5 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -130,11 +130,11 @@ public static void before() { public void testApplicationCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default"); Environment environment = environmentRepository.findOne("application", "", ""); @@ -160,11 +160,11 @@ public void testApplicationCase() throws ApiException { public void testStoresCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default"); Environment environment = environmentRepository.findOne("stores", "", ""); @@ -210,11 +210,11 @@ public void testStoresCase() throws ApiException { public void testStoresProfileCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default"); Environment environment = environmentRepository.findOne("stores", "dev", ""); @@ -264,11 +264,11 @@ else if (propertySource.getName().equals("secret.stores.stores-dev.default")) { public void testApplicationPropertiesAnSecretsOverride() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default"); Environment environment = environmentRepository.findOne("stores-dev", "", ""); diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java index 51ded3f2fe..1b9188e4fa 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java @@ -69,18 +69,18 @@ class KubernetesPropertySourceSupplierTests { @BeforeAll public static void before() throws ApiException { when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); when(coreApi.listNamespacedConfigMap(eq("team-a"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_TEAM_A_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_TEAM_A_LIST); when(coreApi.listNamespacedConfigMap(eq("team-b"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_TEAM_B_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_TEAM_B_LIST); when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_DEFAULT_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_DEFAULT_LIST); when(coreApi.listNamespacedSecret(eq("team-a"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_TEAM_A_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_TEAM_A_LIST); when(coreApi.listNamespacedSecret(eq("team-b"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), - eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_TEAM_B_LIST); + eq(null), eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_TEAM_B_LIST); } @Test diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml index c105609951..6c104fb147 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-controllers - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -13,7 +13,7 @@ springcloud - 4.1.0 + 4.8.0 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml index 7b8b762050..83cc2eeca4 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-controllers org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -13,7 +13,7 @@ springcloud - 4.1.0 + 4.8.0 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherController.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherController.java new file mode 100644 index 0000000000..47410e66f6 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherController.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author wind57 + */ +@RestController +@ConditionalOnKubernetesCatalogEnabled +@ConditionalOnHttpDiscoveryCatalogWatcherEnabled +class DiscoveryCatalogWatcherController { + + private final HeartBeatListener heartBeatListener; + + DiscoveryCatalogWatcherController(HeartBeatListener heartBeatListener) { + this.heartBeatListener = heartBeatListener; + } + + @GetMapping("/state") + Mono> state() { + return Mono.defer(() -> Mono.just(heartBeatListener.lastState().get())); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java similarity index 86% rename from spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java rename to spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java index 84415f740c..74881029a2 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java @@ -14,15 +14,17 @@ * limitations under the License. */ -package org.springframewok.cloud.kubernetes.discoveryserver; +package org.springframework.cloud.kubernetes.discoveryserver; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableScheduling; /** * @author Ryan Baxter */ @SpringBootApplication +@EnableScheduling public class DiscoveryServerApplication { public static void main(String[] args) { diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerController.java similarity index 65% rename from spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java rename to spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerController.java index d7453096b4..e32fdef3bc 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframewok.cloud.kubernetes.discoveryserver; - -import java.util.List; +package org.springframework.cloud.kubernetes.discoveryserver; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -41,8 +41,10 @@ public DiscoveryServerController(KubernetesInformerReactiveDiscoveryClient react @GetMapping("/apps") public Flux apps() { - return reactiveDiscoveryClient.getServices().flatMap(service -> reactiveDiscoveryClient.getInstances(service) - .collectList().flatMap(serviceInstances -> Mono.just(new Service(service, serviceInstances)))); + return reactiveDiscoveryClient.getServices() + .flatMap(service -> reactiveDiscoveryClient.getInstances(service).collectList() + .flatMap(serviceInstances -> Mono.just(new Service(service, + serviceInstances.stream().map(x -> (DefaultKubernetesServiceInstance) x).toList())))); } @GetMapping("/apps/{name}") @@ -50,14 +52,23 @@ public Flux appInstances(@PathVariable String name) { return reactiveDiscoveryClient.getInstances(name); } + /** + * use the "appInstanceNonDeprecated" instead. + */ + @Deprecated(forRemoval = true) @GetMapping("/app/{name}/{instanceId}") public Mono appInstance(@PathVariable String name, @PathVariable String instanceId) { - return reactiveDiscoveryClient.getInstances(name) - .filter(serviceInstance -> serviceInstance.getInstanceId().equals(instanceId)).singleOrEmpty(); + return innerAppInstance(name, instanceId); } - private record Service(String name, List serviceInstances) { + @GetMapping("/apps/{name}/{instanceId}") + Mono appInstanceNonDeprecated(@PathVariable String name, @PathVariable String instanceId) { + return innerAppInstance(name, instanceId); + } + private Mono innerAppInstance(String name, String instanceId) { + return reactiveDiscoveryClient.getInstances(name) + .filter(serviceInstance -> serviceInstance.getInstanceId().equals(instanceId)).singleOrEmpty(); } } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/HeartBeatListener.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/HeartBeatListener.java new file mode 100644 index 0000000000..85aece0eae --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframework/cloud/kubernetes/discoveryserver/HeartBeatListener.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.Environment; +import org.springframework.core.log.LogAccessor; +import org.springframework.stereotype.Component; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCHER_DEFAULT_DELAY; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_NAME; + +/** + * Listener for a HeartbeatEvent that comes from KubernetesCatalogWatch. + * + * @author wind57 + */ +@Component +@ConditionalOnKubernetesCatalogEnabled +@ConditionalOnHttpDiscoveryCatalogWatcherEnabled +class HeartBeatListener implements ApplicationListener { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(HeartBeatListener.class)); + + private final AtomicReference> lastState = new AtomicReference<>(List.of()); + + HeartBeatListener(Environment environment) { + String watchDelay = environment.getProperty(CATALOG_WATCH_PROPERTY_NAME); + if (watchDelay != null) { + LOG.debug("using delay : " + watchDelay); + } + else { + LOG.debug("using default watch delay : " + CATALOG_WATCHER_DEFAULT_DELAY); + } + } + + @Override + @SuppressWarnings("unchecked") + public void onApplicationEvent(HeartbeatEvent event) { + LOG.debug(() -> "received heartbeat event"); + List state = (List) event.getValue(); + LOG.debug(() -> "state received : " + state); + lastState.set(state); + } + + AtomicReference> lastState() { + return lastState; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml index 58e7ca9849..8f649931a2 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml @@ -1,2 +1,20 @@ server: port: 8761 + +# needed to disable the publication of InstanceRegisteredEvent +# which in case of discovery server would not be needed. +spring: + cloud: + discovery: + client: + health-indicator: + enabled: false + +management: + endpoint: + health: + group: + liveness: + include: livenessState, kubernetes + readiness: + include: readinessState, kubernetes diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java deleted file mode 100644 index 4167a0fa8f..0000000000 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframewok.cloud.kubernetes.discoveryserver; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.client.WireMock; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.openapi.JSON; -import io.kubernetes.client.openapi.models.V1EndpointAddress; -import io.kubernetes.client.openapi.models.V1EndpointPort; -import io.kubernetes.client.openapi.models.V1EndpointSubset; -import io.kubernetes.client.openapi.models.V1Endpoints; -import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; -import io.kubernetes.client.openapi.models.V1ListMetaBuilder; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; -import io.kubernetes.client.openapi.models.V1Service; -import io.kubernetes.client.openapi.models.V1ServiceListBuilder; -import io.kubernetes.client.openapi.models.V1ServiceSpec; -import io.kubernetes.client.openapi.models.V1ServiceStatus; -import io.kubernetes.client.util.ClientBuilder; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; -import org.springframework.context.annotation.Bean; -import org.springframework.test.web.reactive.server.WebTestClient; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Ryan Baxter - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DiscoveryServerIntegrationTests.TestConfig.class, -properties = {"debug=true"}) -public class DiscoveryServerIntegrationTests { - - private static final V1Service testService1 = new V1Service() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1")) - .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus()); - private static final V1Endpoints testEndpoints1 = new V1Endpoints() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1")) - .addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080).name("http")) - .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2").targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build()))); - - private static final V1Service testService2 = new V1Service() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace2")) - .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus()); - - private static final V1Service testService3 = new V1Service() - .metadata(new V1ObjectMeta().name("test-svc-3").namespace("namespace1").putLabelsItem("spring", "true") - .putLabelsItem("k8s", "true")) - .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus()); - private static final V1Endpoints testEndpoints3 = new V1Endpoints() - .metadata(new V1ObjectMeta().name("test-svc-3").namespace("namespace1")) - .addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080).name("http")) - .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2").targetRef(new V1ObjectReferenceBuilder().withUid("uid2").build()))); - - - private static WireMockServer wireMockServer; - - @Autowired - WebTestClient webTestClient; - - @Test - void apps(){ - Map kubernetesServiceInstance1Metadata = new HashMap<>(); - kubernetesServiceInstance1Metadata.put(testEndpoints1.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints1.getSubsets().get(0).getPorts().get(0).getPort().toString()); - - Map kubernetesServiceInstance2Metadata = new HashMap<>(); - kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString()); - kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels()); - - KubernetesServiceInstance kubernetesServiceInstance1 = new KubernetesServiceInstance(testEndpoints1.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService1.getMetadata().getName(), testEndpoints1.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints1.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance1Metadata, false, testService1.getMetadata().getNamespace(), null); - KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null); - - webTestClient.get().uri("/apps").exchange().expectBodyList(KubernetesService.class).hasSize(2).contains(new KubernetesService(testService1.getMetadata().getName(), - Collections.singletonList(kubernetesServiceInstance1)), new KubernetesService(testService3.getMetadata().getName(), Collections.singletonList(kubernetesServiceInstance3))); - } - - @Test - void appsName() { - Map kubernetesServiceInstance2Metadata = new HashMap<>(); - kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString()); - kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels()); - KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null); - webTestClient.get().uri("/apps/test-svc-3").exchange().expectBodyList(KubernetesServiceInstance.class).hasSize(1).contains(kubernetesServiceInstance3); - } - - @Test - void instance() { - Map kubernetesServiceInstance2Metadata = new HashMap<>(); - kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString()); - kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels()); - KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null); - webTestClient.get().uri("/app/test-svc-3/uid2").exchange().expectBody(KubernetesServiceInstance.class).isEqualTo(kubernetesServiceInstance3); - } - - @SpringBootApplication - protected static class TestConfig { - - @Bean - public KubernetesNamespaceProvider kubernetesNamespaceProvider() { - KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); - when(provider.getNamespace()).thenReturn("namespace1"); - return provider; - } - - @Bean - public ApiClient apiClient() { - wireMockServer = new WireMockServer(options().dynamicPort()); - wireMockServer.start(); - WireMock.configureFor(wireMockServer.port()); - stubFor(get("/api/v1/namespaces/namespace1/endpoints?resourceVersion=0&watch=false") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1EndpointsListBuilder() - .withMetadata(new V1ListMetaBuilder().withNewResourceVersion("0").build()).addToItems(testEndpoints1, testEndpoints3).build())))); - stubFor(get("/api/v1/namespaces/namespace1/services?resourceVersion=0&watch=false") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1ServiceListBuilder() - .withMetadata(new V1ListMetaBuilder().withNewResourceVersion("0").build()).addToItems(testService1, testService2, testService3).build())))); - stubFor(get("/api/v1/namespaces/namespace1/endpoints?watch=true") - .willReturn(aResponse().withStatus(200))); - stubFor(get("/api/v1/namespaces/namespace1/services?watch=true") - .willReturn(aResponse().withStatus(200))); - ApiClient apiClient = new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build(); - return apiClient; - } - - } - - public static class KubernetesService { - - private String name; - - private List serviceInstances; - - public KubernetesService() { } - - public KubernetesService(String name, List serviceInstances) { - this.name = name; - this.serviceInstances = serviceInstances; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getServiceInstances() { - return serviceInstances; - } - - public void setServiceInstances(List serviceInstances) { - this.serviceInstances = serviceInstances; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - KubernetesService service = (KubernetesService) o; - return Objects.equals(getName(), service.getName()) && Objects.equals(getServiceInstances(), service.getServiceInstances()); - } - - @Override - public int hashCode() { - return Objects.hash(getName(), getServiceInstances()); - } - - @Override - public String toString() { - return "KubernetesService{" + - "name='" + name + '\'' + - ", serviceInstances=" + serviceInstances + - '}'; - } - } -} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/HandleToReactiveDiscoveryClient.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/HandleToReactiveDiscoveryClient.java new file mode 100644 index 0000000000..32c6b2d3f7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/HandleToReactiveDiscoveryClient.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery.reactive; + +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; + +/** + * A way to get the reactive discovery client instance that is package-private. This is + * only needed for tests. + * + * @author wind57 + */ +public class HandleToReactiveDiscoveryClient extends KubernetesInformerReactiveDiscoveryClient { + + public HandleToReactiveDiscoveryClient(KubernetesInformerDiscoveryClient kubernetesDiscoveryClient) { + super(kubernetesDiscoveryClient); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherControllerTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherControllerTests.java new file mode 100644 index 0000000000..7964f8e5f7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryCatalogWatcherControllerTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.test.StepVerifier; + +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; + +/** + * @author wind57 + */ +class DiscoveryCatalogWatcherControllerTests { + + private final HeartBeatListener heartBeatListener = Mockito.mock(HeartBeatListener.class); + + @Test + void test() { + Mockito.when(heartBeatListener.lastState()) + .thenReturn(new AtomicReference<>(List.of(new EndpointNameAndNamespace("one", "two")))); + + DiscoveryCatalogWatcherController catalogWatcherController = new DiscoveryCatalogWatcherController( + heartBeatListener); + + StepVerifier.create(catalogWatcherController.state()) + .expectNext(List.of(new EndpointNameAndNamespace("one", "two"))).verifyComplete(); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplicationContextTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplicationContextTests.java new file mode 100644 index 0000000000..99a2474387 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerApplicationContextTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.context.annotation.Bean; + +/** + * @author wind57 + */ +class DiscoveryServerApplicationContextTests { + + @Nested + @SpringBootTest(classes = TestConfig.class, + properties = { "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.health.livenessstate.enabled=true", + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) + class BothControllersPresent { + + @Autowired + private ObjectProvider discoveryServerController; + + @Autowired + private ObjectProvider discoveryCatalogWatcherController; + + @Autowired + private ObjectProvider heartBeatListener; + + @Test + void test() { + Assertions.assertNotNull(discoveryServerController.getIfAvailable()); + Assertions.assertNotNull(discoveryCatalogWatcherController.getIfAvailable()); + Assertions.assertNotNull(heartBeatListener.getIfAvailable()); + } + + } + + @Nested + @SpringBootTest(classes = TestConfig.class, + properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.health.livenessstate.enabled=true", + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) + class CatalogControllerNotPresentOne { + + @Autowired + private ObjectProvider discoveryServerController; + + @Autowired + private ObjectProvider discoveryCatalogWatcherController; + + @Autowired + private ObjectProvider heartBeatListener; + + @Test + void test() { + Assertions.assertNotNull(discoveryServerController.getIfAvailable()); + Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable()); + Assertions.assertNull(heartBeatListener.getIfAvailable()); + } + + } + + @Nested + @SpringBootTest(classes = TestConfig.class, + properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=true", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false", + /* disable kubernetes from liveness and readiness */ + "management.health.livenessstate.enabled=true", + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) + class CatalogControllerNotPresentTwo { + + @Autowired + private ObjectProvider discoveryServerController; + + @Autowired + private ObjectProvider discoveryCatalogWatcherController; + + @Autowired + private ObjectProvider heartBeatListener; + + @Test + void test() { + Assertions.assertNotNull(discoveryServerController.getIfAvailable()); + Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable()); + Assertions.assertNull(heartBeatListener.getIfAvailable()); + } + + } + + @Nested + @SpringBootTest(classes = TestConfig.class, + properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false", + /* disable kubernetes from liveness and readiness */ + "management.health.livenessstate.enabled=true", + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) + class CatalogControllerNotPresentThree { + + @Autowired + private ObjectProvider discoveryServerController; + + @Autowired + private ObjectProvider discoveryCatalogWatcherController; + + @Autowired + private ObjectProvider heartBeatListener; + + @Test + void test() { + Assertions.assertNotNull(discoveryServerController.getIfAvailable()); + Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable()); + Assertions.assertNull(heartBeatListener.getIfAvailable()); + } + + } + + @TestConfiguration + static class TestConfig { + + @Bean + KubernetesInformerReactiveDiscoveryClient discoveryClient() { + return Mockito.mock(KubernetesInformerReactiveDiscoveryClient.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java similarity index 55% rename from spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java rename to spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java index bb29224afa..abcf5f131d 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframewok.cloud.kubernetes.discoveryserver; +package org.springframework.cloud.kubernetes.discoveryserver; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -28,7 +28,8 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.Service; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -39,24 +40,26 @@ */ class DiscoveryServerControllerTests { - private static final KubernetesServiceInstance serviceAInstance1 = new KubernetesServiceInstance("serviceAInstance1", - "serviceAInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null); + private static final DefaultKubernetesServiceInstance SERVICE_A_INSTANCE_1 = new DefaultKubernetesServiceInstance( + "serviceAInstance1", "serviceAInstance1", "2.2.2.2", 8080, Map.of(), false, "namespace1", null); - private static final KubernetesServiceInstance serviceAInstance2 = new KubernetesServiceInstance("serviceAInstance2", - "serviceAInstance2", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null); + private static final DefaultKubernetesServiceInstance SERVICE_A_INSTANCE_2 = new DefaultKubernetesServiceInstance( + "serviceAInstance2", "serviceAInstance2", "2.2.2.2", 8080, Map.of(), false, "namespace1", null); - private static final KubernetesServiceInstance serviceAInstance3 = new KubernetesServiceInstance("serviceAInstance3", - "serviceAInstance3", "2.2.2.2", 8080, new HashMap<>(), false, "namespace2", null); + private static final DefaultKubernetesServiceInstance SERVICE_A_INSTANCE_3 = new DefaultKubernetesServiceInstance( + "serviceAInstance3", "serviceAInstance3", "2.2.2.2", 8080, Map.of(), false, "namespace2", null); - private static final KubernetesServiceInstance serviceBInstance1 = new KubernetesServiceInstance("serviceBInstance1", - "serviceBInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null); + private static final DefaultKubernetesServiceInstance SERVICE_B_INSTANCE_1 = new DefaultKubernetesServiceInstance( + "serviceBInstance1", "serviceBInstance1", "2.2.2.2", 8080, Map.of(), false, "namespace1", null); - private static final KubernetesServiceInstance serviceCInstance1 = new KubernetesServiceInstance("serviceCInstance1", - "serviceCInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace2", null); + private static final DefaultKubernetesServiceInstance SERVICE_C_INSTANCE_1 = new DefaultKubernetesServiceInstance( + "serviceCInstance1", "serviceCInstance1", "2.2.2.2", 8080, Map.of(), false, "namespace2", null); - private static DiscoveryServerController.Service serviceA = new DiscoveryServerController.Service(); - private static DiscoveryServerController.Service serviceB = new DiscoveryServerController.Service(); - private static DiscoveryServerController.Service serviceC = new DiscoveryServerController.Service(); + private static Service serviceA; + + private static Service serviceB; + + private static Service serviceC; private static KubernetesInformerReactiveDiscoveryClient discoveryClient; @@ -64,17 +67,17 @@ class DiscoveryServerControllerTests { static void beforeAll() { Flux services = Flux.just("serviceA", "serviceB", "serviceC"); - List serviceAInstanceList = new ArrayList<>(); - serviceAInstanceList.add(serviceAInstance1); - serviceAInstanceList.add(serviceAInstance2); - serviceAInstanceList.add(serviceAInstance3); + List serviceAInstanceList = new ArrayList<>(); + serviceAInstanceList.add(SERVICE_A_INSTANCE_1); + serviceAInstanceList.add(SERVICE_A_INSTANCE_2); + serviceAInstanceList.add(SERVICE_A_INSTANCE_3); Flux serviceAInstances = Flux.fromIterable(serviceAInstanceList); - List serviceBInstanceList = Collections.singletonList(serviceBInstance1); + List serviceBInstanceList = Collections.singletonList(SERVICE_B_INSTANCE_1); Flux serviceBInstances = Flux.fromIterable(serviceBInstanceList); - List serviceCInstanceList = Collections.singletonList(serviceCInstance1); + List serviceCInstanceList = Collections.singletonList(SERVICE_C_INSTANCE_1); Flux serviceCInstances = Flux.fromIterable(serviceCInstanceList); discoveryClient = mock(KubernetesInformerReactiveDiscoveryClient.class); @@ -84,18 +87,12 @@ static void beforeAll() { when(discoveryClient.getInstances(eq("serviceC"))).thenReturn(serviceCInstances); when(discoveryClient.getInstances(eq("serviceD"))).thenReturn(Flux.empty()); - serviceA.setName("serviceA"); - serviceA.setServiceInstances(serviceAInstanceList); - - serviceB.setName("serviceB"); - serviceB.setServiceInstances(serviceBInstanceList); + serviceA = new Service("serviceA", serviceAInstanceList); + serviceB = new Service("serviceB", serviceBInstanceList); + serviceC = new Service("serviceC", serviceCInstanceList); - serviceC.setName("serviceC"); - serviceC.setServiceInstances(serviceCInstanceList); } - - @Test void apps() { DiscoveryServerController controller = new DiscoveryServerController(discoveryClient); @@ -105,16 +102,19 @@ void apps() { @Test void appInstances() { DiscoveryServerController controller = new DiscoveryServerController(discoveryClient); - StepVerifier.create(controller.appInstances("serviceA")).expectNext(serviceAInstance1, serviceAInstance2, serviceAInstance3).verifyComplete(); - StepVerifier.create(controller.appInstances("serviceB")).expectNext(serviceBInstance1).verifyComplete(); - StepVerifier.create(controller.appInstances("serviceC")).expectNext(serviceCInstance1).verifyComplete(); + StepVerifier.create(controller.appInstances("serviceA")) + .expectNext(SERVICE_A_INSTANCE_1, SERVICE_A_INSTANCE_2, SERVICE_A_INSTANCE_3).verifyComplete(); + StepVerifier.create(controller.appInstances("serviceB")).expectNext(SERVICE_B_INSTANCE_1).verifyComplete(); + StepVerifier.create(controller.appInstances("serviceC")).expectNext(SERVICE_C_INSTANCE_1).verifyComplete(); StepVerifier.create(controller.appInstances("serviceD")).expectNextCount(0).verifyComplete(); } @Test void appInstance() { DiscoveryServerController controller = new DiscoveryServerController(discoveryClient); - StepVerifier.create(controller.appInstance("serviceA", "serviceAInstance2")).expectNext(serviceAInstance2).verifyComplete(); + StepVerifier.create(controller.appInstance("serviceA", "serviceAInstance2")).expectNext(SERVICE_A_INSTANCE_2) + .verifyComplete(); StepVerifier.create(controller.appInstance("serviceB", "doesnotexist")).expectNextCount(0).verifyComplete(); } + } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsEndpointTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsEndpointTest.java new file mode 100644 index 0000000000..0acd107f8c --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsEndpointTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceSpec; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.HandleToReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = DiscoveryServerIntegrationAppsEndpointTest.TestConfig.class, + properties = { "management.health.livenessstate.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) +class DiscoveryServerIntegrationAppsEndpointTest { + + private static final String NAMESPACE = "namespace"; + + private static final SharedInformerFactory SHARED_INFORMER_FACTORY = Mockito.mock(SharedInformerFactory.class); + + private static final V1Service TEST_SERVICE_A = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE)) + .spec(new V1ServiceSpec().type("ClusterIP")); + + private static final V1Service TEST_SERVICE_B = new V1Service().metadata(new V1ObjectMeta().name("test-svc-2") + .namespace(NAMESPACE).putLabelsItem("spring", "true").putLabelsItem("k8s", "true")) + .spec(new V1ServiceSpec().type("ClusterIP")); + + private static final V1Endpoints TEST_ENDPOINTS_A = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE)) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080).name("http")) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build()))); + + private static final V1Endpoints TEST_ENDPOINTS_B = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-2").namespace(NAMESPACE)) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080).name("http")) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid2").build()))); + + @Autowired + private WebTestClient webTestClient; + + @Test + void apps() { + Map metadataA = new HashMap<>(); + metadataA.put("type", "ClusterIP"); + metadataA.put("port.http", "8080"); + metadataA.put("k8s_namespace", "namespace"); + + Map metadataB = new HashMap<>(metadataA); + metadataB.put("spring", "true"); + metadataB.put("k8s", "true"); + + DefaultKubernetesServiceInstance kubernetesServiceInstance1 = new DefaultKubernetesServiceInstance( + TEST_ENDPOINTS_A.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), + TEST_SERVICE_A.getMetadata().getName(), + TEST_ENDPOINTS_A.getSubsets().get(0).getAddresses().get(0).getIp(), + TEST_ENDPOINTS_A.getSubsets().get(0).getPorts().get(0).getPort(), metadataA, false, + TEST_SERVICE_A.getMetadata().getNamespace(), null); + + DefaultKubernetesServiceInstance kubernetesServiceInstance2 = new DefaultKubernetesServiceInstance( + TEST_ENDPOINTS_B.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), + TEST_SERVICE_B.getMetadata().getName(), + TEST_ENDPOINTS_B.getSubsets().get(0).getAddresses().get(0).getIp(), + TEST_ENDPOINTS_B.getSubsets().get(0).getPorts().get(0).getPort(), metadataB, false, + TEST_SERVICE_B.getMetadata().getNamespace(), null); + + webTestClient.get().uri("/apps").exchange().expectBodyList(Util.InstanceForTest.class).hasSize(2).contains( + new Util.InstanceForTest(TEST_SERVICE_A.getMetadata().getName(), + Collections.singletonList(kubernetesServiceInstance1)), + new Util.InstanceForTest(TEST_SERVICE_B.getMetadata().getName(), + Collections.singletonList(kubernetesServiceInstance2))); + } + + @TestConfiguration + static class TestConfig { + + @Bean + KubernetesNamespaceProvider kubernetesNamespaceProvider() { + KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); + when(provider.getNamespace()).thenReturn(NAMESPACE); + return provider; + } + + @Bean + KubernetesInformerReactiveDiscoveryClient discoveryClient() { + return new HandleToReactiveDiscoveryClient(kubernetesInformerDiscoveryClient()); + } + + private KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + + Lister serviceLister = Util.setupServiceLister(TEST_SERVICE_A, TEST_SERVICE_B); + Lister endpointsLister = Util.setupEndpointsLister(TEST_ENDPOINTS_A, TEST_ENDPOINTS_B); + + return new KubernetesInformerDiscoveryClient(SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, + null, KubernetesDiscoveryProperties.DEFAULT); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsNameEndpointTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsNameEndpointTest.java new file mode 100644 index 0000000000..405a7882c4 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationAppsNameEndpointTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.HashMap; +import java.util.Map; + +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceStatus; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.HandleToReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = DiscoveryServerIntegrationAppsNameEndpointTest.TestConfig.class, + properties = { "management.health.livenessstate.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) +class DiscoveryServerIntegrationAppsNameEndpointTest { + + private static final String NAMESPACE = "namespace"; + + private static final SharedInformerFactory SHARED_INFORMER_FACTORY = Mockito.mock(SharedInformerFactory.class); + + private static final V1Service TEST_SERVICE = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-3").namespace(NAMESPACE).putLabelsItem("spring", "true") + .putLabelsItem("k8s", "true")) + .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1").type("ClusterIP")).status(new V1ServiceStatus()); + + private static final V1Endpoints TEST_ENDPOINTS = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-3").namespace(NAMESPACE)) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080).name("http")) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid2").build()))); + + @Autowired + private WebTestClient webTestClient; + + @Test + void appsName() { + Map metadata = new HashMap<>(); + metadata.put("spring", "true"); + metadata.put("port.http", "8080"); + metadata.put("k8s_namespace", "namespace"); + metadata.put("type", "ClusterIP"); + metadata.put("k8s", "true"); + + DefaultKubernetesServiceInstance kubernetesServiceInstance = new DefaultKubernetesServiceInstance( + TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), + TEST_SERVICE.getMetadata().getName(), TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getIp(), + TEST_ENDPOINTS.getSubsets().get(0).getPorts().get(0).getPort(), metadata, false, + TEST_SERVICE.getMetadata().getNamespace(), null); + + webTestClient.get().uri("/apps/test-svc-3").exchange().expectBodyList(DefaultKubernetesServiceInstance.class) + .hasSize(1).contains(kubernetesServiceInstance); + } + + @TestConfiguration + static class TestConfig { + + @Bean + KubernetesNamespaceProvider kubernetesNamespaceProvider() { + KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); + when(provider.getNamespace()).thenReturn(NAMESPACE); + return provider; + } + + @Bean + KubernetesInformerReactiveDiscoveryClient discoveryClient() { + return new HandleToReactiveDiscoveryClient(kubernetesInformerDiscoveryClient()); + } + + private KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + + Lister serviceLister = Util.setupServiceLister(TEST_SERVICE); + Lister endpointsLister = Util.setupEndpointsLister(TEST_ENDPOINTS); + + return new KubernetesInformerDiscoveryClient(SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, + null, KubernetesDiscoveryProperties.DEFAULT); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationInstanceEndpointTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationInstanceEndpointTest.java new file mode 100644 index 0000000000..957dee8462 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationInstanceEndpointTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.HashMap; +import java.util.Map; + +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceStatus; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.HandleToReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = DiscoveryServerIntegrationInstanceEndpointTest.TestConfig.class, + properties = { "management.health.livenessstate.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) +class DiscoveryServerIntegrationInstanceEndpointTest { + + private static final String NAMESPACE = "namespace"; + + private static final SharedInformerFactory SHARED_INFORMER_FACTORY = Mockito.mock(SharedInformerFactory.class); + + private static final V1Service TEST_SERVICE = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-3").namespace(NAMESPACE).putLabelsItem("spring", "true") + .putLabelsItem("k8s", "true")) + .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1").type("ClusterIP")).status(new V1ServiceStatus()); + + private static final V1Endpoints TEST_ENDPOINTS = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-3").namespace(NAMESPACE)) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080).name("http")) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid2").build()))); + + @Autowired + private WebTestClient webTestClient; + + @Test + void instanceDeprecated() { + Map metadata = new HashMap<>(); + metadata.put("spring", "true"); + metadata.put("port.http", "8080"); + metadata.put("k8s_namespace", "namespace"); + metadata.put("type", "ClusterIP"); + metadata.put("k8s", "true"); + + DefaultKubernetesServiceInstance kubernetesServiceInstance = new DefaultKubernetesServiceInstance( + TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), + TEST_SERVICE.getMetadata().getName(), TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getIp(), + TEST_ENDPOINTS.getSubsets().get(0).getPorts().get(0).getPort(), metadata, false, + TEST_SERVICE.getMetadata().getNamespace(), null); + webTestClient.get().uri("/app/test-svc-3/uid2").exchange().expectBody(DefaultKubernetesServiceInstance.class) + .isEqualTo(kubernetesServiceInstance); + } + + @Test + void instance() { + Map metadata = new HashMap<>(); + metadata.put("spring", "true"); + metadata.put("port.http", "8080"); + metadata.put("k8s_namespace", "namespace"); + metadata.put("type", "ClusterIP"); + metadata.put("k8s", "true"); + + DefaultKubernetesServiceInstance kubernetesServiceInstance = new DefaultKubernetesServiceInstance( + TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), + TEST_SERVICE.getMetadata().getName(), TEST_ENDPOINTS.getSubsets().get(0).getAddresses().get(0).getIp(), + TEST_ENDPOINTS.getSubsets().get(0).getPorts().get(0).getPort(), metadata, false, + TEST_SERVICE.getMetadata().getNamespace(), null); + webTestClient.get().uri("/apps/test-svc-3/uid2").exchange().expectBody(DefaultKubernetesServiceInstance.class) + .isEqualTo(kubernetesServiceInstance); + } + + @TestConfiguration + static class TestConfig { + + @Bean + KubernetesNamespaceProvider kubernetesNamespaceProvider() { + KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); + when(provider.getNamespace()).thenReturn(NAMESPACE); + return provider; + } + + @Bean + KubernetesInformerReactiveDiscoveryClient discoveryClient() { + return new HandleToReactiveDiscoveryClient(kubernetesInformerDiscoveryClient()); + } + + private KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + + Lister serviceLister = Util.setupServiceLister(TEST_SERVICE); + Lister endpointsLister = Util.setupEndpointsLister(TEST_ENDPOINTS); + + return new KubernetesInformerDiscoveryClient(SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, + null, KubernetesDiscoveryProperties.DEFAULT); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/HeartbeatTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/HeartbeatTest.java new file mode 100644 index 0000000000..1c5bb99b71 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/HeartbeatTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.annotation.Bean; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +@SpringBootTest(classes = HeartbeatTest.TestConfig.class, + properties = { "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true", + /* disable kubernetes from liveness and readiness */ + "management.health.livenessstate.enabled=true", + "management.endpoint.health.group.liveness.include=livenessState", + "management.health.readinessstate.enabled=true", + "management.endpoint.health.group.readiness.include=readinessState" }) +@AutoConfigureWebTestClient +class HeartbeatTest { + + @Autowired + private WebTestClient client; + + @Autowired + private ApplicationContext context; + + @Test + void testHeartbeat() { + Assertions.assertTrue(true); + client.get().uri("/state").exchange().expectStatus().is2xxSuccessful().expectBody().json("[]"); + + context.getBean(HeartbeatPublisher.class).publishEvent(); + client.get().uri("/state").exchange().expectStatus().is2xxSuccessful().expectBody().json(""" + [ + { + "endpointName":"endpoint-name", + "namespace":"namespaceA" + } + ] + """); + } + + @TestConfiguration + static class TestConfig { + + @Bean + KubernetesInformerReactiveDiscoveryClient client() { + return Mockito.mock(KubernetesInformerReactiveDiscoveryClient.class); + } + + @Bean + HeartbeatPublisher heartbeatPublisher() { + return new HeartbeatPublisher(); + } + + } + + static class HeartbeatPublisher implements ApplicationEventPublisherAware { + + private ApplicationEventPublisher publisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + void publishEvent() { + publisher.publishEvent( + new HeartbeatEvent("test", List.of(new EndpointNameAndNamespace("endpoint-name", "namespaceA")))); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/Util.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/Util.java new file mode 100644 index 0000000000..a1312220e3 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/test/java/org/springframework/cloud/kubernetes/discoveryserver/Util.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryserver; + +import java.util.List; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1Service; + +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; + +/** + * @author wind57 + */ +final class Util { + + private Util() { + + } + + static Lister setupServiceLister(V1Service... services) { + Cache serviceCache = new Cache<>(); + Lister serviceLister = new Lister<>(serviceCache); + for (V1Service svc : services) { + serviceCache.add(svc); + } + return serviceLister; + } + + static Lister setupEndpointsLister(V1Endpoints... endpoints) { + Cache endpointsCache = new Cache<>(); + Lister endpointsLister = new Lister<>(endpointsCache); + for (V1Endpoints ep : endpoints) { + endpointsCache.add(ep); + } + return endpointsLister; + } + + record InstanceForTest(String name, List serviceInstances) { + + } + +} diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index 12a069f51c..32523ec6fd 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -23,18 +23,18 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.1.0-SNAPSHOT + 4.1.1-SNAPSHOT spring-cloud-kubernetes-dependencies - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT pom Spring Cloud Kubernetes :: Dependencies Spring Cloud Kubernetes Dependencies 0.13.0 - 6.7.2 - 17.0.2 + 6.9.2 + 19.0.0 2.35.1 4.4 diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index 945d6ee881..10ae3e6208 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java index a68d89b85f..88b9c2c254 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java @@ -29,11 +29,9 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; -import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.util.ClassUtils; /** * @author Ryan Baxter @@ -42,7 +40,7 @@ class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { @Override public void initialize(BootstrapRegistry registry) { - if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + if (hasConfigServerInstanceProvider()) { return; } // We need to pass a lambda here rather than create a new instance of @@ -54,14 +52,11 @@ public void initialize(BootstrapRegistry registry) { final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { - private final BootstrapContext context; - - private KubernetesFunction(BootstrapContext context) { - this.context = context; + private KubernetesFunction() { } static KubernetesFunction create(BootstrapContext context) { - return new KubernetesFunction(context); + return new KubernetesFunction(); } @Override @@ -74,23 +69,17 @@ public List apply(String serviceId, Binder binder, BindHandler // Kubernetes DiscoveryClient return Collections.emptyList(); } - KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, - bindHandler); - KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); - return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) - .getInstances(serviceId); + return getInstanceProvider(binder, bindHandler).getInstances(serviceId); } - private KubernetesConfigServerInstanceProvider getInstanceProvider( - KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, - BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { - KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder - .bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class), + private KubernetesConfigServerInstanceProvider getInstanceProvider(Binder binder, BindHandler bindHandler) { + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = binder + .bind(KubernetesDiscoveryProperties.PREFIX, Bindable.of(KubernetesDiscoveryProperties.class), bindHandler) - .orElseGet(KubernetesDiscoveryClientProperties::new); - KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet(); + .orElseGet(() -> KubernetesDiscoveryProperties.DEFAULT); + KubernetesDiscoveryClientBlockingAutoConfiguration autoConfiguration = new KubernetesDiscoveryClientBlockingAutoConfiguration(); DiscoveryClient discoveryClient = autoConfiguration - .kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties); + .kubernetesDiscoveryClient(autoConfiguration.restTemplateBuilder(), kubernetesDiscoveryProperties); return discoveryClient::getInstances; } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java index e1629d5218..c6b79b3cc0 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java @@ -22,7 +22,7 @@ public class DiscoveryServerUrlInvalidException extends RuntimeException { public DiscoveryServerUrlInvalidException() { - super("spring.cloud.kubernetes.discovery.discovery-server-url must be specified and a valid URL."); + super("'spring.cloud.kubernetes.discovery.discovery-server-url' must be specified and be a valid URL."); } } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java new file mode 100644 index 0000000000..05a63bb1a3 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.log.LogAccessor; +import org.springframework.http.HttpMethod; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.client.RestTemplate; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE; + +/** + * @author wind57 + */ +final class KubernetesCatalogWatch implements ApplicationEventPublisherAware { + + private static final ParameterizedTypeReference> TYPE = new ParameterizedTypeReference<>() { + + }; + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesCatalogWatch.class)); + + private final AtomicReference> catalogState = new AtomicReference<>(List.of()); + + private final RestTemplate restTemplate; + + private ApplicationEventPublisher publisher; + + KubernetesCatalogWatch(RestTemplateBuilder builder, KubernetesDiscoveryProperties properties) { + this.restTemplate = builder.rootUri(properties.discoveryServerUrl()).build(); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}") + public void catalogServicesWatch() { + try { + List currentState = restTemplate.exchange("/state", HttpMethod.GET, null, TYPE) + .getBody(); + + if (!catalogState.get().equals(currentState)) { + LOG.debug(() -> "Received update from kubernetes discovery http client: " + currentState); + publisher.publishEvent(new HeartbeatEvent(this, currentState)); + } + + catalogState.set(currentState); + } + catch (Exception e) { + LOG.error(e, () -> "Error watching Kubernetes Services"); + } + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfiguration.java new file mode 100644 index 0000000000..a0a4e740a8 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogWatcherEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.log.LogAccessor; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCHER_DEFAULT_DELAY; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_NAME; + +/** + * @author wind57 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnKubernetesCatalogWatcherEnabled +@ConditionalOnHttpDiscoveryCatalogWatcherEnabled +@EnableConfigurationProperties(KubernetesDiscoveryProperties.class) +class KubernetesCatalogWatchAutoConfiguration { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesCatalogWatchAutoConfiguration.class)); + + // this has to be a RestTemplateBuilder and not a WebClientBuilder, otherwise + // we need the webflux dependency, and it might leak into client's dependencies + // which is not always desirable. + @Bean + @ConditionalOnMissingBean + RestTemplateBuilder restTemplateBuilder() { + return new RestTemplateBuilder(); + } + + @Bean + @ConditionalOnMissingBean + KubernetesCatalogWatch kubernetesCatalogWatch(RestTemplateBuilder builder, KubernetesDiscoveryProperties properties, + Environment environment) { + + String watchDelay = environment.getProperty(CATALOG_WATCH_PROPERTY_NAME); + if (watchDelay != null) { + LOG.debug("using delay : " + watchDelay); + } + else { + LOG.debug("using default watch delay : " + CATALOG_WATCHER_DEFAULT_DELAY); + } + + return new KubernetesCatalogWatch(builder, properties); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java index 62d11665da..815ac43e18 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java @@ -17,13 +17,15 @@ package org.springframework.cloud.kubernetes.discovery; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.util.CollectionUtils; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; @@ -32,16 +34,33 @@ */ public class KubernetesDiscoveryClient implements DiscoveryClient { - private RestTemplate rest; + private final RestTemplate rest; - private KubernetesDiscoveryClientProperties properties; + private final boolean emptyNamespaces; + private final Set namespaces; + + private final String discoveryServerUrl; + + @Deprecated(forRemoval = true) public KubernetesDiscoveryClient(RestTemplate rest, KubernetesDiscoveryClientProperties properties) { if (!StringUtils.hasText(properties.getDiscoveryServerUrl())) { throw new DiscoveryServerUrlInvalidException(); } this.rest = rest; - this.properties = properties; + this.emptyNamespaces = properties.getNamespaces().isEmpty(); + this.namespaces = properties.getNamespaces(); + this.discoveryServerUrl = properties.getDiscoveryServerUrl(); + } + + KubernetesDiscoveryClient(RestTemplate rest, KubernetesDiscoveryProperties kubernetesDiscoveryProperties) { + if (!StringUtils.hasText(kubernetesDiscoveryProperties.discoveryServerUrl())) { + throw new DiscoveryServerUrlInvalidException(); + } + this.rest = rest; + this.emptyNamespaces = kubernetesDiscoveryProperties.namespaces().isEmpty(); + this.namespaces = kubernetesDiscoveryProperties.namespaces(); + this.discoveryServerUrl = kubernetesDiscoveryProperties.discoveryServerUrl(); } @Override @@ -51,38 +70,31 @@ public String description() { @Override public List getInstances(String serviceId) { - List response = Collections.emptyList(); - KubernetesServiceInstance[] responseBody = rest.getForEntity( - properties.getDiscoveryServerUrl() + "/apps/" + serviceId, KubernetesServiceInstance[].class).getBody(); + DefaultKubernetesServiceInstance[] responseBody = rest + .getForEntity(discoveryServerUrl + "/apps/" + serviceId, DefaultKubernetesServiceInstance[].class) + .getBody(); if (responseBody != null && responseBody.length > 0) { - response = Arrays.stream(responseBody).filter(this::matchNamespaces).collect(Collectors.toList()); + return Arrays.stream(responseBody).filter(this::matchNamespaces).collect(Collectors.toList()); } - return response; + return List.of(); } @Override public List getServices() { - List response = Collections.emptyList(); - Service[] services = rest.getForEntity(properties.getDiscoveryServerUrl() + "/apps", Service[].class).getBody(); + Service[] services = rest.getForEntity(discoveryServerUrl + "/apps", Service[].class).getBody(); if (services != null && services.length > 0) { - response = Arrays.stream(services).filter(this::matchNamespaces).map(Service::getName) - .collect(Collectors.toList()); + return Arrays.stream(services).filter(this::matchNamespaces).map(Service::name).toList(); } - return response; + return List.of(); } - private boolean matchNamespaces(KubernetesServiceInstance kubernetesServiceInstance) { - if (CollectionUtils.isEmpty(properties.getNamespaces())) { - return true; - } - return properties.getNamespaces().contains(kubernetesServiceInstance.getNamespace()); + private boolean matchNamespaces(DefaultKubernetesServiceInstance kubernetesServiceInstance) { + return emptyNamespaces || namespaces.contains(kubernetesServiceInstance.getNamespace()); } private boolean matchNamespaces(Service service) { - if (CollectionUtils.isEmpty(service.getServiceInstances())) { - return true; - } - return service.getServiceInstances().stream().anyMatch(this::matchNamespaces); + return service.serviceInstances().isEmpty() + || service.serviceInstances().stream().anyMatch(this::matchNamespaces); } } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java index ecc47b7fc3..a6b2aa73d7 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -22,7 +22,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -34,6 +33,7 @@ import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -43,13 +43,16 @@ /** * @author Ryan Baxter + * @deprecated in favor of {@link KubernetesDiscoveryClientBlockingAutoConfiguration} and + * {@link KubernetesDiscoveryClientReactiveAutoConfiguration} */ @Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) -@ConditionalOnProperty(value = "spring.cloud.kubernetes.discovery.enabled", matchIfMissing = true) +@ConditionalOnKubernetesDiscoveryEnabled @EnableConfigurationProperties({ DiscoveryClientHealthIndicatorProperties.class, KubernetesDiscoveryClientProperties.class }) +@Deprecated(forRemoval = true) public class KubernetesDiscoveryClientAutoConfiguration { @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientBlockingAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientBlockingAutoConfiguration.java new file mode 100644 index 0000000000..6865cd7c59 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientBlockingAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.cloud.kubernetes.commons.PodUtils; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author wind57 + */ + +@Configuration(proxyBeanMethods = false) +@ConditionalOnSpringCloudKubernetesBlockingDiscovery +@EnableConfigurationProperties({ DiscoveryClientHealthIndicatorProperties.class, KubernetesDiscoveryProperties.class }) +class KubernetesDiscoveryClientBlockingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + RestTemplateBuilder restTemplateBuilder() { + return new RestTemplateBuilder(); + } + + @Bean + @ConditionalOnMissingBean + KubernetesDiscoveryClient kubernetesDiscoveryClient(RestTemplateBuilder restTemplateBuilder, + KubernetesDiscoveryProperties properties) { + return new KubernetesDiscoveryClient(restTemplateBuilder.build(), properties); + } + + @Bean + @ConditionalOnMissingBean + PodUtils kubernetesDiscoveryPodUtils() { + return new KubernetesDiscoveryPodUtils(); + } + + @Bean + @ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer + KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer(PodUtils podUtils, + ApplicationEventPublisher applicationEventPublisher) { + return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java index 2f944f7600..c93de0d1e9 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java @@ -16,14 +16,17 @@ package org.springframework.cloud.kubernetes.discovery; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author Ryan Baxter + * @deprecated use + * {@link org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties} + * instead. */ +@Deprecated(forRemoval = true) @ConfigurationProperties("spring.cloud.kubernetes.discovery") public class KubernetesDiscoveryClientProperties { @@ -35,7 +38,7 @@ public class KubernetesDiscoveryClientProperties { * If set then only the services and endpoints matching these namespaces will be * fetched from the Kubernetes API server. */ - private List namespaces = new ArrayList<>(); + private Set namespaces = Set.of(); public String getDiscoveryServerUrl() { return discoveryServerUrl; @@ -53,11 +56,11 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - List getNamespaces() { + Set getNamespaces() { return namespaces; } - void setNamespaces(List namespaces) { + void setNamespaces(Set namespaces) { this.namespaces = namespaces; } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientReactiveAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientReactiveAutoConfiguration.java new file mode 100644 index 0000000000..d6f297213c --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientReactiveAutoConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.kubernetes.commons.PodUtils; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSpringCloudKubernetesReactiveDiscovery +@EnableConfigurationProperties({ DiscoveryClientHealthIndicatorProperties.class, KubernetesDiscoveryProperties.class }) +class KubernetesDiscoveryClientReactiveAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } + + @Bean + @ConditionalOnMissingBean + KubernetesReactiveDiscoveryClient kubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder, + KubernetesDiscoveryProperties properties) { + return new KubernetesReactiveDiscoveryClient(webClientBuilder, properties); + } + + @Bean + @ConditionalOnMissingBean + PodUtils kubernetesDiscoveryPodUtils() { + return new KubernetesDiscoveryPodUtils(); + } + + /** + * Post an event so that health indicator is initialized. + */ + @Bean + @ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer + KubernetesDiscoveryClientHealthIndicatorInitializer reactiveIndicatorInitializer( + ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { + return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); + } + + /** + * unlike the blocking implementation, we need to register the health indicator. + */ + @Bean + @ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer + ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( + KubernetesReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties) { + return new ReactiveDiscoveryClientHealthIndicator(client, properties); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryPodUtils.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryPodUtils.java new file mode 100644 index 0000000000..d21af26e74 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryPodUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.function.Supplier; + +import org.springframework.cloud.kubernetes.commons.PodUtils; + +/** + * @author wind57 + */ +final class KubernetesDiscoveryPodUtils implements PodUtils { + + @Override + public Supplier currentPod() { + // we don't really have a way to get the pod here + return () -> null; + } + + @Override + public boolean isInsideKubernetes() { + // this bean is used in a config that is annotated + // with @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES), + // so safe to return true here. + return true; + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java index e0e830b3bf..a5d346e65a 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java @@ -21,6 +21,9 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.Service; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; @@ -29,14 +32,22 @@ */ public class KubernetesReactiveDiscoveryClient implements ReactiveDiscoveryClient { - private WebClient webClient; + private final WebClient webClient; + @Deprecated(forRemoval = true) public KubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder, KubernetesDiscoveryClientProperties properties) { if (!StringUtils.hasText(properties.getDiscoveryServerUrl())) { throw new DiscoveryServerUrlInvalidException(); } - this.webClient = webClientBuilder.baseUrl(properties.getDiscoveryServerUrl()).build(); + webClient = webClientBuilder.baseUrl(properties.getDiscoveryServerUrl()).build(); + } + + KubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder, KubernetesDiscoveryProperties properties) { + if (!StringUtils.hasText(properties.discoveryServerUrl())) { + throw new DiscoveryServerUrlInvalidException(); + } + webClient = webClientBuilder.baseUrl(properties.discoveryServerUrl()).build(); } @Override @@ -48,14 +59,14 @@ public String description() { @Cacheable("serviceinstances") public Flux getInstances(String serviceId) { return webClient.get().uri("/apps/" + serviceId) - .exchangeToFlux(clientResponse -> clientResponse.bodyToFlux(KubernetesServiceInstance.class)); + .exchangeToFlux(clientResponse -> clientResponse.bodyToFlux(DefaultKubernetesServiceInstance.class)); } @Override @Cacheable("services") public Flux getServices() { - return webClient.get().uri("/apps").exchangeToFlux( - clientResponse -> clientResponse.bodyToFlux(Service.class).map(service -> service.getName())); + return webClient.get().uri("/apps") + .exchangeToFlux(clientResponse -> clientResponse.bodyToFlux(Service.class).map(Service::name)); } } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java index f397da609c..370d8c0210 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java @@ -25,6 +25,7 @@ /** * @author Ryan Baxter */ +@Deprecated(forRemoval = true) public class KubernetesServiceInstance implements ServiceInstance { private String instanceId; diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java index 81619e215e..43c97c87f7 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java @@ -21,11 +21,12 @@ /** * @author Ryan Baxter */ +@Deprecated(forRemoval = true) public class Service { private String name; - private List serviceInstances; + private List serviceInstances = List.of(); public Service() { } diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d9d4dd270d..5b99b7c50b 100644 --- a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,3 @@ -org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration +org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientBlockingAutoConfiguration +org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientReactiveAutoConfiguration +org.springframework.cloud.kubernetes.discovery.KubernetesCatalogWatchAutoConfiguration diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/App.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/App.java new file mode 100644 index 0000000000..2519bb7316 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/App.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; + +/** + * @author wind57 + */ +@SpringBootConfiguration +@EnableAutoConfiguration +class App { + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/BlockingDiscoveryHealthPublishedEventTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/BlockingDiscoveryHealthPublishedEventTest.java new file mode 100644 index 0000000000..416f480ba1 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/BlockingDiscoveryHealthPublishedEventTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +/** + * test that asserts the type of published event for blocking discovery. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://example", + "spring.cloud.discovery.reactive.enabled=false" }, + classes = { HealthEventListenerConfiguration.class, App.class }) +class BlockingDiscoveryHealthPublishedEventTest { + + @AfterEach + void afterEach() { + HealthEventListenerConfiguration.caught = false; + } + + @Test + void test() { + Assertions.assertTrue(HealthEventListenerConfiguration.caught); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java index d911c0cce0..421afc4be6 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java @@ -16,9 +16,9 @@ package org.springframework.cloud.kubernetes.discovery; -import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashSet; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -30,9 +30,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; import org.springframework.context.ConfigurableApplicationContext; @@ -68,11 +67,23 @@ void beforeAll() throws JsonProcessingException { wireMockServer = new WireMockServer(options().dynamicPort()); wireMockServer.start(); WireMock.configureFor(wireMockServer.port()); - String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"spring-cloud-kubernetes-configserver\",\"host\":\"localhost\",\"port\":" - + wireMockServer.port() + ",\"uri\":\"" + wireMockServer.baseUrl() - + "\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]"; + String appsName = """ + [{ + "instanceId": "uid2", + "serviceId": "spring-cloud-kubernetes-configserver", + "host": "localhost", + "port": "%s", + "uri": "%s", + "secure":false, + "metadata":{"spring": "true", "http": "8080", "k8s": "true"}, + "namespace": "namespace1", + "cluster": null, + "scheme":"http" + }] + """.formatted(wireMockServer.port(), wireMockServer.baseUrl()); + stubFor(get("/apps/spring-cloud-kubernetes-configserver").willReturn( - aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json"))); + aResponse().withStatus(200).withBody(appsName).withHeader("content-type", "application/json"))); Environment environment = new Environment("test", "default"); Map properties = new HashMap<>(); properties.put("hello", "world"); @@ -84,11 +95,16 @@ void beforeAll() throws JsonProcessingException { .withHeader("content-type", "application/json"))); } + @AfterEach + void afterEach() { + context.close(); + } + @Test void testBootstrapper() { - this.context = setup().run(); + context = setup().run(); verify(1, getRequestedFor(urlEqualTo("/apps/spring-cloud-kubernetes-configserver"))); - assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + assertThat(context.getEnvironment().getProperty("hello")).isEqualTo("world"); } SpringApplicationBuilder setup(String... env) { @@ -99,20 +115,20 @@ SpringApplicationBuilder setup(String... env) { } private String[] addDefaultEnv(String[] env) { - Set set = new LinkedHashSet<>(); - if (env != null && env.length > 0) { - set.addAll(Arrays.asList(env)); + Set set = new HashSet<>(); + if (env != null) { + set.addAll(List.of(env)); } set.add("server.port=0"); set.add("spring.cloud.config.discovery.enabled=true"); set.add("spring.config.import=optional:configserver:"); set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); set.add("spring.cloud.kubernetes.discovery.discoveryServerUrl=" + wireMockServer.baseUrl()); + set.add("spring.main.web-application-type=NONE"); return set.toArray(new String[0]); } - @SpringBootConfiguration - @EnableAutoConfiguration + @TestConfiguration static class TestConfig { } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/HealthEventListenerConfiguration.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/HealthEventListenerConfiguration.java new file mode 100644 index 0000000000..2a5f2d16bf --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/HealthEventListenerConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.junit.jupiter.api.Assertions; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer.RegisteredEventSource; + +/** + * @author wind57 + */ +@TestConfiguration +class HealthEventListenerConfiguration { + + static boolean caught = false; + + @Bean + HealthEventListener healthEventListener() { + return new HealthEventListener(); + } + + private static class HealthEventListener implements ApplicationListener> { + + @Override + public void onApplicationEvent(InstanceRegisteredEvent event) { + caught = true; + Assertions.assertInstanceOf(RegisteredEventSource.class, event.getSource()); + RegisteredEventSource registeredEventSource = (RegisteredEventSource) event.getSource(); + Assertions.assertTrue(registeredEventSource.inside()); + Assertions.assertNull(registeredEventSource.pod()); + Assertions.assertEquals(registeredEventSource.cloudPlatform(), "kubernetes"); + } + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfigurationTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfigurationTests.java new file mode 100644 index 0000000000..92fd404956 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchAutoConfigurationTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesCatalogWatchAutoConfigurationTests { + + private ApplicationContextRunner applicationContextRunner; + + @Test + void discoveryEnabledDefault() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); + } + + @Test + void discoveryEnabled() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); + } + + @Test + void discoveryDisabled() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com"); + applicationContextRunner.run(context -> assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class)); + } + + @Test + void kubernetesDiscoveryEnabled() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=true", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com"); + applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); + } + + // disabling discovery, disabled catalog watcher. + @Test + void kubernetesDiscoveryDisabled() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class)); + } + + /** + * both blocking and reactive configs are disabled, catalog watcher is disabled. + */ + @Test + void disableBlockingAndReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=false", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + /** + * blocking is disabled, reactive is enabled, catalog watcher is enabled. + */ + @Test + void disableBlockingEnableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + }); + } + + /** + * blocking is enabled, reactive is disabled, catalog watcher is enabled. + */ + @Test + void enableBlockingDisableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=true", "spring.cloud.discovery.reactive.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=example.com", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + }); + } + + /** + * spring.cloud.kubernetes.discovery.enabled is false, catalog watcher is disabled. + */ + @Test + void disableKubernetesDiscovery() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=false", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + /** + * spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled is false, as + * such catalog watcher is not present. + */ + @Test + void disableHttpDiscoveryClientCatalogWatcher() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=true", + "spring.cloud.kubernetes.discovery.enabled=false", + "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + private void setup(String... properties) { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(KubernetesCatalogWatchAutoConfiguration.class, + KubernetesDiscoveryClientBlockingAutoConfiguration.class, + KubernetesDiscoveryClientReactiveAutoConfiguration.class)) + .withPropertyValues(properties); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTests.java new file mode 100644 index 0000000000..689a07e137 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.ApplicationEventPublisher; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.verify; + +/** + * @author wind57 + */ +class KubernetesCatalogWatchTests { + + private WireMockServer wireMockServer; + + private static final ArgumentCaptor HEARTBEAT_EVENT_ARGUMENT_CAPTOR = ArgumentCaptor + .forClass(HeartbeatEvent.class); + + private static final ApplicationEventPublisher APPLICATION_EVENT_PUBLISHER = Mockito + .mock(ApplicationEventPublisher.class); + + @AfterEach + void afterEach() { + Mockito.reset(APPLICATION_EVENT_PUBLISHER); + } + + @Test + void testSingleCycleSameAsCurrentState() { + + String body = "[]"; + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + stubFor(get("/state") + .willReturn(aResponse().withStatus(200).withBody(body).withHeader("content-type", "application/json"))); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); + + KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties); + catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + + catalogWatch.catalogServicesWatch(); + + Mockito.verifyNoInteractions(APPLICATION_EVENT_PUBLISHER); + + } + + @Test + @SuppressWarnings("unchecked") + void testSingleCycleDifferentCurrentState() { + + String body = """ + [ + { + "endpointName":"endpoint-name", + "namespace":"namespaceA" + } + ] + """; + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + stubFor(get("/state") + .willReturn(aResponse().withStatus(200).withBody(body).withHeader("content-type", "application/json"))); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); + + KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties); + catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + + catalogWatch.catalogServicesWatch(); + + verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); + HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); + Assertions.assertEquals(event.getSource().getClass(), KubernetesCatalogWatch.class); + + List state = (List) event.getValue(); + + Assertions.assertEquals(state.size(), 1); + Assertions.assertEquals(state.get(0).namespace(), "namespaceA"); + + } + + @Test + @SuppressWarnings("unchecked") + void testTwoCyclesDifferentStates() { + + String bodyOne = """ + [ + { + "endpointName":"endpoint-name", + "namespace":"namespaceA" + } + ] + """; + + // namespace differs + String bodyTwo = """ + [ + { + "endpointName":"endpoint-name", + "namespace":"namespaceB" + } + ] + """; + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + stubFor(get("/state").willReturn( + aResponse().withStatus(200).withBody(bodyOne).withHeader("content-type", "application/json"))); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); + + KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties); + catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + + catalogWatch.catalogServicesWatch(); + + verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); + HeartbeatEvent eventOne = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); + Assertions.assertEquals(eventOne.getSource().getClass(), KubernetesCatalogWatch.class); + + List stateOne = (List) eventOne.getValue(); + + Assertions.assertEquals(stateOne.size(), 1); + Assertions.assertEquals(stateOne.get(0).namespace(), "namespaceA"); + + // second call + stubFor(get("/state").willReturn( + aResponse().withStatus(200).withBody(bodyTwo).withHeader("content-type", "application/json"))); + + catalogWatch.catalogServicesWatch(); + + verify(APPLICATION_EVENT_PUBLISHER, Mockito.times(2)).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); + HeartbeatEvent eventTwo = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); + Assertions.assertEquals(eventTwo.getSource().getClass(), KubernetesCatalogWatch.class); + + List stateTwo = (List) eventTwo.getValue(); + + Assertions.assertEquals(stateTwo.size(), 1); + Assertions.assertEquals(stateTwo.get(0).namespace(), "namespaceB"); + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryAutoConfigurationTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryAutoConfigurationTests.java new file mode 100644 index 0000000000..b0f9ebdd18 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryAutoConfigurationTests.java @@ -0,0 +1,288 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesDiscoveryAutoConfigurationTests { + + /** + *
+	 *     '@ConditionalOnDiscoveryEnabled' is not matched, thus no beans are created
+	 *     from either blocking or reactive configurations.
+	 * 
+ */ + @Test + void discoveryDisabled() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(RestTemplate.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     '@ConditionalOnKubernetesDiscoveryEnabled' is not matched, thus no beans are created
+	 *     from either blocking or reactive configurations.
+	 * 
+ */ + @Test + void kubernetesDiscoveryDisabled() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(RestTemplate.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     '@ConditionalOnCloudPlatform' does not match 'KUBERNETES', thus no beans are created
+	 *     from either blocking or reactive configurations.
+	 * 
+ */ + @Test + void cloudPlatformDisabled() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=none"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(RestTemplate.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     - reactive config is disabled, blocking is defaulted.
+	 *     - WebClient class is not present
+	 * 
+ */ + @Test + void reactiveDisabledBlockingEnabledWebClientPresent() { + setupWithFilteredClassLoader(WebClient.class, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(RestTemplateBuilder.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).getBean("indicatorInitializer").isNotNull(); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     - reactive config is disabled, blocking is defaulted.
+	 *     - WebClient class is present
+	 * 
+ */ + @Test + void reactiveDisabledBlockingEnabledWebClientMissing() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=false", "spring.cloud.discovery.blocking.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(RestTemplate.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).getBean("indicatorInitializer").isNull(); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     - reactive config is disabled, blocking is defaulted.
+	 *     - WebClient class is present
+	 * 
+ */ + @Test + void reactiveDisabledBlockingEnabledWebClientMissingHealthIndicatorMissing() { + setupWithFilteredClassLoader(HealthIndicator.class, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(RestTemplateBuilder.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean("indicatorInitializer"); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     '@ConditionalOnDiscoveryHealthIndicatorEnabled' not matched.
+	 * 
+ */ + @Test + void blockingHealthIndicatorDisabled() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver", + "spring.cloud.discovery.client.health-indicator.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(RestTemplateBuilder.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean("indicatorInitializer"); + + assertThat(context).hasSingleBean(WebClient.Builder.class); + assertThat(context).hasSingleBean(KubernetesReactiveDiscoveryClient.class); + + }); + } + + /** + *
+	 *     - reactive config is enabled, blocking is defaulted.
+	 *     - WebClient class is present
+	 * 
+ */ + @Test + void reactiveEnabledBlockingEnabledWebClientPresent() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(RestTemplateBuilder.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).getBean("indicatorInitializer").isNotNull(); + + assertThat(context).hasSingleBean(WebClient.Builder.class); + assertThat(context).hasSingleBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).getBean("kubernetesReactiveDiscoveryClientHealthIndicator").isNotNull(); + }); + } + + /** + *
+	 *     - reactive config is enabled, blocking is defaulted.
+	 *     - WebClient class is missing
+	 * 
+ */ + @Test + void reactiveEnabledBlockingEnabledWebClientMissing() { + setupWithFilteredClassLoader(WebClient.class, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(RestTemplateBuilder.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).getBean("indicatorInitializer").isNotNull(); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).getBean("kubernetesReactiveDiscoveryClientHealthIndicator").isNull(); + }); + } + + /** + *
+	 *     '@ConditionalOnBlockingDiscoveryEnabled' is not matched.
+	 * 
+ */ + @Test + void testBlockingDisabled() { + setupWithFilteredClassLoader(WebClient.class, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=true", "spring.cloud.discovery.blocking.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(RestTemplate.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).getBean("indicatorInitializer").isNull(); + + assertThat(context).doesNotHaveBean(WebClient.Builder.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).getBean("kubernetesReactiveDiscoveryClientHealthIndicator").isNull(); + }); + } + + /** + *
+	 *     - WebClient is on the classpath (this is asserted via the presence of beans that come
+	 *       from the reactive auto-configuration)
+	 *     - This has no impact of the creation of the blocking discovery client
+	 * 
+ */ + @Test + void testFor1426Issue() { + setupWithFilteredClassLoader(null, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.reactive.enabled=true", "spring.cloud.discovery.blocking.enabled=true", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver"); + applicationContextRunner.run(context -> { + // blocking client is present + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + + // reactive client is present + assertThat(context).hasSingleBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + private ApplicationContextRunner applicationContextRunner; + + private void setupWithFilteredClassLoader(Class cls, String... properties) { + + if (cls != null) { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(KubernetesDiscoveryClientBlockingAutoConfiguration.class, + KubernetesDiscoveryClientReactiveAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(cls)).withPropertyValues(properties); + } + else { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(KubernetesDiscoveryClientBlockingAutoConfiguration.class, + KubernetesDiscoveryClientReactiveAutoConfiguration.class)) + .withPropertyValues(properties); + } + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java index e634d9e953..54fb77860e 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java @@ -22,8 +22,6 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; import org.springframework.cloud.commons.util.UtilAutoConfiguration; @@ -34,81 +32,83 @@ */ class KubernetesDiscoveryClientAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, - ReactiveCommonsClientAutoConfiguration.class, KubernetesDiscoveryClientAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(UtilAutoConfiguration.class, ReactiveCommonsClientAutoConfiguration.class, + KubernetesDiscoveryClientReactiveAutoConfiguration.class, + KubernetesDiscoveryClientBlockingAutoConfiguration.class)); @Test - public void shouldWorkWithDefaults() { + void shouldWorkWithDefaults() { contextRunner .withPropertyValues("spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver") .withClassLoader(new FilteredClassLoader("org.springframework.web.reactive")).run(context -> { - assertThat(context).hasSingleBean(DiscoveryClient.class); - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() { + void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() { contextRunner .withPropertyValues("spring.cloud.discovery.enabled=false", "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver") .run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); - assertThat(context).doesNotHaveBean(DiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() { + void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() { contextRunner .withPropertyValues("spring.cloud.kubernetes.discovery.enabled=false", "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver") .run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); - assertThat(context).doesNotHaveBean(DiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void shouldHaveReactiveDiscoveryClient() { + void shouldHaveReactiveDiscoveryClient() { contextRunner .withPropertyValues("spring.main.cloud-platform=KUBERNETES", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver") .run(context -> { - assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class); - assertThat(context).doesNotHaveBean(DiscoveryClient.class); + assertThat(context).hasSingleBean(KubernetesReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); assertThat(context).hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() { + void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() { contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false").run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() { + void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() { contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } @Test - public void worksWithoutActuator() { + void worksWithoutActuator() { contextRunner .withPropertyValues("spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver") .withClassLoader(new FilteredClassLoader("org.springframework.boot.actuate")).run(context -> { - assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class); + assertThat(context).hasSingleBean(KubernetesReactiveDiscoveryClient.class); assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); }); } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java index 16c17404f7..2f97f56e2c 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java @@ -16,10 +16,10 @@ package org.springframework.cloud.kubernetes.discovery; -import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import com.github.tomakehurst.wiremock.WireMockServer; @@ -32,6 +32,8 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.web.client.RestTemplate; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -45,9 +47,55 @@ */ class KubernetesDiscoveryClientTests { - private static final String APPS = "[{\"name\":\"test-svc-1\",\"serviceInstances\":[{\"instanceId\":\"uid1\",\"serviceId\":\"test-svc-1\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"http\":\"8080\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]},{\"name\":\"test-svc-3\",\"serviceInstances\":[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace2\",\"cluster\":null,\"scheme\":\"http\"}]}]"; - - private static final String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace2\",\"cluster\":null,\"scheme\":\"http\"}]"; + private static final String APPS = """ + [{ + "name": "test-svc-1", + "serviceInstances": + [{ + "instanceId": "uid1", + "serviceId": "test-svc-1", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata":{"http": "8080"}, + "namespace": "namespace1", + "cluster": null, + "scheme": "http" + }] + }, + { + "name": "test-svc-3", + "serviceInstances": + [{ + "instanceId": "uid2", + "serviceId": "test-svc-3", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata": {"spring": "true", "http": "8080", "k8s": "true"}, + "namespace": "namespace2", + "cluster":null, + "scheme":"http" + }] + }] + """; + + private static final String APPS_NAME = """ + [{ + "instanceId": "uid2", + "serviceId": "test-svc-3", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata": {"spring": "true", "http": "8080", "k8s": "true"}, + "namespace": "namespace2", + "cluster": null, + "scheme": "http" + }] + """; private static WireMockServer wireMockServer; @@ -67,8 +115,9 @@ static void beforeAll() { @Test void getInstances() { RestTemplate rest = new RestTemplateBuilder().build(); - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties); assertThat(discoveryClient.getServices()).contains("test-svc-1", "test-svc-3"); } @@ -76,53 +125,53 @@ void getInstances() { @Test void getServices() { RestTemplate rest = new RestTemplateBuilder().build(); - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties); Map metadata = new HashMap<>(); metadata.put("spring", "true"); metadata.put("http", "8080"); metadata.put("k8s", "true"); - assertThat(discoveryClient.getInstances("test-svc-3")) - .contains(new KubernetesServiceInstance("uid2", "test-svc-3", "2.2.2.2", 8080, false, - URI.create("http://2.2.2.2:8080"), metadata, "http", "namespace2")); + assertThat(discoveryClient.getInstances("test-svc-3")).contains(new DefaultKubernetesServiceInstance("uid2", + "test-svc-3", "2.2.2.2", 8080, metadata, false, "namespace2", null, null)); assertThat(discoveryClient.getInstances("does-not-exist")).isEmpty(); } @ParameterizedTest @MethodSource("servicesFilteredByNamespacesSource") - void getServicesFilteredByNamespaces(List namespaces, List expectedServices) { + void getServicesFilteredByNamespaces(Set namespaces, List expectedServices) { RestTemplate rest = new RestTemplateBuilder().build(); - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setNamespaces(namespaces); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, namespaces, true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties); assertThat(discoveryClient.getServices()).containsExactlyInAnyOrderElementsOf(expectedServices); } - static Stream servicesFilteredByNamespacesSource() { - return Stream.of(Arguments.of(List.of(), List.of("test-svc-1", "test-svc-3")), - Arguments.of(List.of("namespace1", "namespace2"), List.of("test-svc-1", "test-svc-3")), - Arguments.of(List.of("namespace1"), List.of("test-svc-1")), - Arguments.of(List.of("namespace2", "does-not-exist"), List.of("test-svc-3"))); - } - @ParameterizedTest @MethodSource("instancesFilteredByNamespacesSource") - void getInstancesFilteredByNamespaces(List namespaces, String serviceId, List expectedInstances) { + void getInstancesFilteredByNamespaces(Set namespaces, String serviceId, List expectedInstances) { RestTemplate rest = new RestTemplateBuilder().build(); - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setNamespaces(namespaces); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, namespaces, true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties); assertThat(discoveryClient.getInstances(serviceId)).map(ServiceInstance::getInstanceId) .containsExactlyInAnyOrderElementsOf(expectedInstances); } - static Stream instancesFilteredByNamespacesSource() { - return Stream.of(Arguments.of(List.of(), "test-svc-3", List.of("uid2")), - Arguments.of(List.of("namespace1"), "test-svc-3", List.of()), - Arguments.of(List.of("namespace2"), "test-svc-3", List.of("uid2"))); + private static Stream servicesFilteredByNamespacesSource() { + return Stream.of(Arguments.of(Set.of(), List.of("test-svc-1", "test-svc-3")), + Arguments.of(Set.of("namespace1", "namespace2"), List.of("test-svc-1", "test-svc-3")), + Arguments.of(Set.of("namespace1"), List.of("test-svc-1")), + Arguments.of(Set.of("namespace2", "does-not-exist"), List.of("test-svc-3"))); + } + + private static Stream instancesFilteredByNamespacesSource() { + return Stream.of(Arguments.of(Set.of(), "test-svc-3", List.of("uid2")), + Arguments.of(Set.of("namespace1"), "test-svc-3", List.of()), + Arguments.of(Set.of("namespace2"), "test-svc-3", List.of("uid2"))); } } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java index 9f6adf6b4d..91b509baca 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java @@ -16,9 +16,9 @@ package org.springframework.cloud.kubernetes.discovery; -import java.net.URI; import java.util.HashMap; import java.util.Map; +import java.util.Set; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.web.reactive.function.client.WebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -38,9 +40,55 @@ */ class KubernetesReactiveDiscoveryClientTests { - private static final String APPS = "[{\"name\":\"test-svc-1\",\"serviceInstances\":[{\"instanceId\":\"uid1\",\"serviceId\":\"test-svc-1\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"http\":\"8080\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]},{\"name\":\"test-svc-3\",\"serviceInstances\":[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]}]"; + private static final String APPS = """ + [{ + "name": "test-svc-1", + "serviceInstances": + [{ + "instanceId": "uid1", + "serviceId": "test-svc-1", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata": {"http":"8080"}, + "namespace": "namespace1", + "cluster": null, + "scheme": "http" + }] + }, + { + "name": "test-svc-3", + "serviceInstances": + [{ + "instanceId": "uid2", + "serviceId": "test-svc-3", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata": {"spring": "true", "http": "8080", "k8s": "true"}, + "namespace": "namespace1", + "cluster": null, + "scheme": "http" + }] + }] + """; - private static final String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]"; + private static final String APPS_NAME = """ + [{ + "instanceId": "uid2", + "serviceId": "test-svc-3", + "host": "2.2.2.2", + "port": 8080, + "uri": "http://2.2.2.2:8080", + "secure": false, + "metadata": {"spring": "true", "http": "8080", "k8s": "true"}, + "namespace": "namespace1", + "cluster": null, + "scheme": "http" + }] + """; private static WireMockServer wireMockServer; @@ -59,8 +107,10 @@ static void beforeAll() { @Test void getInstances() { - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesReactiveDiscoveryClient discoveryClient = new KubernetesReactiveDiscoveryClient(WebClient.builder(), properties); StepVerifier.create(discoveryClient.getServices()).expectNext("test-svc-1", "test-svc-3").verifyComplete(); @@ -68,19 +118,20 @@ void getInstances() { @Test void getServices() { - KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties(); - properties.setDiscoveryServerUrl(wireMockServer.baseUrl()); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, + wireMockServer.baseUrl()); KubernetesReactiveDiscoveryClient discoveryClient = new KubernetesReactiveDiscoveryClient(WebClient.builder(), properties); Map metadata = new HashMap<>(); metadata.put("spring", "true"); metadata.put("http", "8080"); metadata.put("k8s", "true"); + StepVerifier.create(discoveryClient.getInstances("test-svc-3")) - .expectNext(new KubernetesServiceInstance("uid2", "test-svc-3", "2.2.2.2", 8080, false, - URI.create("http://2.2.2.2:8080"), metadata, "http", "namespace1")) + .expectNext(new DefaultKubernetesServiceInstance("uid2", "test-svc-3", "2.2.2.2", 8080, metadata, false, + "namespace1", null, null)) .verifyComplete(); - StepVerifier.create(discoveryClient.getInstances("test-svc-3")).expectNextCount(0); } } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ReactiveDiscoveryHealthPublishedEventTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ReactiveDiscoveryHealthPublishedEventTest.java new file mode 100644 index 0000000000..72ca89a83a --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ReactiveDiscoveryHealthPublishedEventTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * test that asserts the type of published event for reactive discovery. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.discovery-server-url=http://example", + // disable blocking implementation + "spring.cloud.discovery.blocking.enabled=false" }, + classes = { HealthEventListenerConfiguration.class, App.class }) +class ReactiveDiscoveryHealthPublishedEventTest { + + // blocking client is not present, as such the blocking auto-configuration was not + // picked up, therefor the health event comes from the reactive one. + @Autowired + private ObjectProvider discoveryClients; + + @AfterEach + void afterEach() { + HealthEventListenerConfiguration.caught = false; + } + + @Test + void test() { + Assertions.assertTrue(HealthEventListenerConfiguration.caught); + Assertions.assertNull(discoveryClients.getIfAvailable()); + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/README.md b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/README.md index 1cd7e941e1..dabb0d2e1d 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/README.md +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/README.md @@ -83,17 +83,18 @@ items: - apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - namespace: default - name: namespace-reader + namespace: default + name: namespace-reader rules: - - apiGroups: ["", "extensions", "apps"] - resources: ["configmaps", "pods", "services", "endpoints", "secrets"] - verbs: ["get", "list", "watch"] + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "pods", "services", "endpoints", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiVersion: apps/v1 kind: Deployment metadata: name: kubernetes-leader-election-example spec: + replicas: 4 selector: matchLabels: app: kubernetes-leader-election-example @@ -148,17 +149,10 @@ And check the leadership information again: curl $SERVICE_URL ``` -Now you should receive a message like this: -``` -I am 'kubernetes-leader-election-example-1234567890-abcde' but I am not a leader of the 'world' -``` - -If you wouldn't do anything for a few seconds, the same instance will become a leader again because it only yielded its leadership but stayed in the cluster. +If another instance was able to acquire the leadership you should see a different instance is now the leadership. You may +have to try this a few times depending on how the service is load balanced. -Now scale the application to two instances and try all the steps again: -``` -kubectl scale --replicas=2 deployment.apps/kubernetes-leader-election-example -``` +`DEBUG` logging is enabled so you can view the logs of the instances in order to see which instance is acquiring leadership. > Note: with multiple replicas in the cluster, `curl` command will access one of them depending on the Kubernetes load balancing configuration. Thus, when trying to yield the leadership, request might go to a non-leader node first. Just execute command again until it reaches the correct node. diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index cef615c882..0472189834 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes-examples - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT kubernetes-leader-election-example @@ -75,11 +75,11 @@ maven-surefire-plugin - 2.22.2 + 3.1.2 maven-failsafe-plugin - 2.22.2 + 3.1.2 diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/java/org/springframework/cloud/kubernetes/examples/LeaderController.java b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/java/org/springframework/cloud/kubernetes/examples/LeaderController.java index 39b142ac55..674aa10685 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/java/org/springframework/cloud/kubernetes/examples/LeaderController.java +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/java/org/springframework/cloud/kubernetes/examples/LeaderController.java @@ -47,7 +47,7 @@ public LeaderController() throws UnknownHostException { * Return a message whether this instance is a leader or not. * @return info */ - @GetMapping + @GetMapping("/") public String getInfo() { if (this.context == null) { return String.format("I am '%s' but I am not a leader of the '%s'", this.host, this.role); @@ -63,7 +63,7 @@ public String getInfo() { * to give up the leadership. * @return info about leadership */ - @PutMapping + @PutMapping("/") public ResponseEntity revokeLeadership() { if (this.context == null) { String message = String.format("Cannot revoke leadership because '%s' is not a leader", this.host); diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/resources/application.properties b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/resources/application.properties index 6c665afdb1..faf8c085c4 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/resources/application.properties +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/main/resources/application.properties @@ -2,3 +2,5 @@ spring.cloud.kubernetes.leader.role=world # Configmap to which leader election metadata will be saved spring.cloud.kubernetes.leader.config-map-name=leader +logging.level.org.springframework.cloud.kubernetes.fabric8.leader=DEBUG +logging.level.org.springframework.cloud.kubernetes.commons.leader=DEBUG diff --git a/spring-cloud-kubernetes-examples/pom.xml b/spring-cloud-kubernetes-examples/pom.xml index 7a0598f2a3..a07e43c966 100644 --- a/spring-cloud-kubernetes-examples/pom.xml +++ b/spring-cloud-kubernetes-examples/pom.xml @@ -23,7 +23,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT spring-cloud-kubernetes-examples diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/.jdk8 b/spring-cloud-kubernetes-fabric8-autoconfig/.jdk8 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml index 897eed634a..b36dac09a2 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml @@ -23,7 +23,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8ActuatorConfiguration.java b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8ActuatorConfiguration.java index 6241835682..890c9b6101 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8ActuatorConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8ActuatorConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,8 @@ import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.cloud.CloudPlatform; import org.springframework.cloud.kubernetes.commons.PodUtils; +import org.springframework.cloud.kubernetes.commons.autoconfig.ConditionalOnKubernetesHealthIndicatorEnabled; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,8 +29,7 @@ * @author wind57 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(HealthIndicator.class) -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnKubernetesHealthIndicatorEnabled public class Fabric8ActuatorConfiguration { @Bean diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/src/test/java/org/springframework/cloud/kubernetes/fabric8/Fabric8UserAgentDefaultConfigurationTests.java b/spring-cloud-kubernetes-fabric8-autoconfig/src/test/java/org/springframework/cloud/kubernetes/fabric8/Fabric8UserAgentDefaultConfigurationTests.java index 43e7069091..69002846ef 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/src/test/java/org/springframework/cloud/kubernetes/fabric8/Fabric8UserAgentDefaultConfigurationTests.java +++ b/spring-cloud-kubernetes-fabric8-autoconfig/src/test/java/org/springframework/cloud/kubernetes/fabric8/Fabric8UserAgentDefaultConfigurationTests.java @@ -37,7 +37,7 @@ class Fabric8UserAgentDefaultConfigurationTests { @Test void testUserAgent() { String userAgent = client.getConfiguration().getUserAgent(); - assertThat(userAgent).isEqualTo("fabric8-kubernetes-client/6.7.2"); + assertThat(userAgent).isEqualTo("fabric8-kubernetes-client/6.9.2"); } } diff --git a/spring-cloud-kubernetes-fabric8-config/.jdk8 b/spring-cloud-kubernetes-fabric8-config/.jdk8 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/spring-cloud-kubernetes-fabric8-config/pom.xml b/spring-cloud-kubernetes-fabric8-config/pom.xml index 0767d79f4a..66f37f90cc 100644 --- a/spring-cloud-kubernetes-fabric8-config/pom.xml +++ b/spring-cloud-kubernetes-fabric8-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java index a0c72da351..69687f6f6d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java @@ -46,11 +46,11 @@ * @author Ioannis Canellos */ @Configuration(proxyBeanMethods = false) -@Import({ KubernetesCommonsAutoConfiguration.class, Fabric8AutoConfiguration.class }) -@ConditionalOnClass({ ConfigMap.class, Secret.class }) -@AutoConfigureAfter(KubernetesBootstrapConfiguration.class) @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @ConditionalOnBootstrapEnabled +@ConditionalOnClass({ ConfigMap.class, Secret.class }) +@Import({ KubernetesCommonsAutoConfiguration.class, Fabric8AutoConfiguration.class }) +@AutoConfigureAfter(KubernetesBootstrapConfiguration.class) public class Fabric8BootstrapConfiguration { @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeConfigpropsEndpointTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeConfigpropsEndpointTests.java new file mode 100644 index 0000000000..b6c5aa516e --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeConfigpropsEndpointTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class BootstrapFabric8SanitizeConfigpropsEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class DefaultSettingsTest extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.configprops.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.configprops.show-values=NEVER" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class ExplicitNever extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=false" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithoutSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize-two", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=true" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.['sanitize-1'].beans.[*].properties.sanitizeSecretNameTwo") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeEnvEndpointTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeEnvEndpointTests.java new file mode 100644 index 0000000000..fe7fe397b7 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/BootstrapFabric8SanitizeEnvEndpointTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class BootstrapFabric8SanitizeEnvEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class DefaultSettingsTest extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.env.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.env.show-values=NEVER" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class ExplicitNever extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=false" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithoutSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.bootstrap.name=sanitize-two", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=true" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretNameTwo'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java new file mode 100644 index 0000000000..52a002630a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8ConfigpropsEndpointTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class ConfigDataFabric8ConfigpropsEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class DefaultSettingsTest extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.configprops.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=NEVER", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class ExplicitNever extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=false", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithoutSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.configprops.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "management.endpoint.configprops.show-values=ALWAYS", + "spring.cloud.kubernetes.sanitize.secrets=true", + "spring.config.import=kubernetes:,classpath:./sanitize-two.yaml" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeConfigMapName") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretName") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/configprops", this.port) + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() + .jsonPath("contexts.sanitize.beans.[*].properties.sanitizeSecretNameTwo") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8SanitizeEnvEndpointTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8SanitizeEnvEndpointTests.java new file mode 100644 index 0000000000..4230604bb6 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/ConfigDataFabric8SanitizeEnvEndpointTests.java @@ -0,0 +1,248 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.SanitizableData; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +class ConfigDataFabric8SanitizeEnvEndpointTests { + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoints.web.exposure.include=*" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class DefaultSettingsTest extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + // management.endpoint.env.show-values = NEVER + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoint.env.show-values=NEVER" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class ExplicitNever extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = false
+	 *
+	 *     Sanitizing functions must apply, but we have none registered, as such
+	 *     everything is visible in plain text, both from configmaps and secrets.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize.yaml", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=false" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithoutSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // secret is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo("sanitizeSecretValue"); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + + /** + *
+	 *     - management.endpoint.env.show-values = ALWAYS
+	 *     - spring.cloud.kubernetes.sanitize.secrets = true
+	 *
+	 *     Sanitizing functions must apply, and we have one registered, as such
+	 *     configmap is visible in plain text, but secrets are sanitized.
+	 *
+	 * 
+ */ + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SanitizeApp.class, + properties = { "spring.main.cloud-platform=KUBERNETES", "management.endpoints.web.exposure.include=*", + "spring.config.import=kubernetes:,classpath:./sanitize-two.yaml", + "management.endpoint.env.show-values=ALWAYS", "spring.cloud.kubernetes.sanitize.secrets=true" }) + @EnableKubernetesMockClient(crud = true, https = false) + @Nested + class AlwaysWithSanitizingFunction extends Fabric8SecretsSanitize { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @LocalManagementPort + private int port; + + @BeforeAll + static void setUpBeforeClass() { + setUpBeforeClass(mockClient); + } + + @Test + void test() { + // configmap is not sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeConfigMapName'].value") + .isEqualTo("sanitizeConfigMapValue"); + + // first secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretName'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // second secret is sanitized + webClient.get().uri("http://localhost:{port}/actuator/env", this.port).accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("propertySources.[*].properties.['sanitize.sanitizeSecretNameTwo'].value") + .isEqualTo(SanitizableData.SANITIZED_VALUE); + + // secret is usable from configuration properties + webClient.get().uri("http://localhost:{port}/secret", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeSecretValue"); + + // configmap is usable from configuration properties + webClient.get().uri("http://localhost:{port}/configmap", this.port).exchange().expectStatus().isOk() + .expectBody().jsonPath("$").isEqualTo("sanitizeConfigMapValue"); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/Fabric8SecretsSanitize.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/Fabric8SecretsSanitize.java new file mode 100644 index 0000000000..ec8c389d95 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/Fabric8SecretsSanitize.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import java.util.Base64; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; + +/** + * @author wind57 + */ +abstract class Fabric8SecretsSanitize { + + private static final String NAMESPACE = "test"; + + static void setUpBeforeClass(KubernetesClient mockClient) { + + // Configure kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, NAMESPACE); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + Secret secret = new SecretBuilder().withNewMetadata().withName("sanitize-secret").endMetadata() + .addToData("sanitize.sanitizeSecretName", + Base64.getEncoder().encodeToString("sanitizeSecretValue".getBytes())) + .build(); + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + Secret secretTwo = new SecretBuilder().withNewMetadata().withName("sanitize-secret-two").endMetadata() + .addToData("sanitize.sanitizeSecretNameTwo", + Base64.getEncoder().encodeToString("sanitizeSecretValueTwo".getBytes())) + .build(); + mockClient.secrets().inNamespace(NAMESPACE).resource(secretTwo).create(); + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("sanitize-configmap").endMetadata() + .addToData("sanitize.sanitizeConfigMapName", "sanitizeConfigMapValue").build(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapApp.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeApp.java similarity index 76% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapApp.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeApp.java index fa5fc62914..c7a48b7c00 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapApp.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeApp.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -23,13 +23,12 @@ /** * @author wind57 */ - +@EnableConfigurationProperties(SanitizeProperties.class) @SpringBootApplication -@EnableConfigurationProperties(ConfigMapProperties.class) -public class ConfigMapApp { +class SanitizeApp { public static void main(String[] args) { - SpringApplication.run(ConfigMapApp.class, args); + SpringApplication.run(SanitizeApp.class, args); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeController.java similarity index 58% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeController.java index c59aa7fc93..0f609e2fc9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,17 +23,22 @@ * @author wind57 */ @RestController -public class ConfigMapController { +class SanitizeController { - private final ConfigMapProperties properties; + private final SanitizeProperties sanitizeProperties; - public ConfigMapController(ConfigMapProperties properties) { - this.properties = properties; + SanitizeController(SanitizeProperties sanitizeProperties) { + this.sanitizeProperties = sanitizeProperties; } - @GetMapping("/key") - public String key() { - return properties.getKey(); + @GetMapping("/secret") + String secret() { + return sanitizeProperties.getSanitizeSecretName(); + } + + @GetMapping("/configmap") + String configmap() { + return sanitizeProperties.getSanitizeConfigMapName(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeProperties.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeProperties.java new file mode 100644 index 0000000000..83f0c350c0 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/sanitize_secrets/SanitizeProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.sanitize_secrets; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author wind57 + */ +@ConfigurationProperties("sanitize") +class SanitizeProperties { + + private String sanitizeSecretName; + + private String sanitizeSecretNameTwo; + + private String sanitizeConfigMapName; + + public String getSanitizeSecretName() { + return sanitizeSecretName; + } + + public void setSanitizeSecretName(String sanitizeSecretName) { + this.sanitizeSecretName = sanitizeSecretName; + } + + public String getSanitizeConfigMapName() { + return sanitizeConfigMapName; + } + + public void setSanitizeConfigMapName(String sanitizeConfigMapName) { + this.sanitizeConfigMapName = sanitizeConfigMapName; + } + + public String getSanitizeSecretNameTwo() { + return sanitizeSecretNameTwo; + } + + public void setSanitizeSecretNameTwo(String sanitizeSecretNameTwo) { + this.sanitizeSecretNameTwo = sanitizeSecretNameTwo; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/BoostrapFabric8SecretsPropertySourceTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/BoostrapFabric8SecretsPropertySourceTest.java similarity index 94% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/BoostrapFabric8SecretsPropertySourceTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/BoostrapFabric8SecretsPropertySourceTest.java index fb75d00582..067647f6b5 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/BoostrapFabric8SecretsPropertySourceTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/BoostrapFabric8SecretsPropertySourceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config; +package org.springframework.cloud.kubernetes.fabric8.config.secrets_property_source; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigDataFabric8SecretsPropertySourceTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/ConfigDataFabric8SecretsPropertySourceTest.java similarity index 87% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigDataFabric8SecretsPropertySourceTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/ConfigDataFabric8SecretsPropertySourceTest.java index acc286796d..9ff3a136e8 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigDataFabric8SecretsPropertySourceTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/ConfigDataFabric8SecretsPropertySourceTest.java @@ -14,19 +14,16 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config; +package org.springframework.cloud.kubernetes.fabric8.config.secrets_property_source; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.kubernetes.fabric8.config.example.App; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class, properties = { "spring.main.cloud-platform=KUBERNETES", "spring.config.import=kubernetes:" }) @TestPropertySource("classpath:/application-secrets.properties") diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/Fabric8SecretsPropertySourceTest.java similarity index 93% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/Fabric8SecretsPropertySourceTest.java index e9d43ffa1a..3c109e7081 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/secrets_property_source/Fabric8SecretsPropertySourceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config; +package org.springframework.cloud.kubernetes.fabric8.config.secrets_property_source; import java.util.Base64; @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySourceLocator; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize-two.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize-two.yaml new file mode 100644 index 0000000000..7037403969 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize-two.yaml @@ -0,0 +1,16 @@ +spring: + application: + name: sanitize + cloud: + kubernetes: + secrets: + enableApi: true + sources: + - name: sanitize-secret + namespaces: test + - name: sanitize-secret-two + namespaces: test + config: + sources: + - name: sanitize-configmap + namespace: test diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize.yaml new file mode 100644 index 0000000000..f91869d894 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/sanitize.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: sanitize + cloud: + kubernetes: + secrets: + enableApi: true + sources: + - name: sanitize-secret + namespaces: test + config: + sources: + - name: sanitize-configmap + namespace: test diff --git a/spring-cloud-kubernetes-fabric8-discovery/.jdk8 b/spring-cloud-kubernetes-fabric8-discovery/.jdk8 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/spring-cloud-kubernetes-fabric8-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml index 2a93d3f48e..0ed2f61626 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml +++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContext.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContext.java index 9d3fc70f8b..9a9fadf8f1 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContext.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContext.java @@ -16,7 +16,6 @@ package org.springframework.cloud.kubernetes.fabric8.discovery; -import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -28,6 +27,9 @@ import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import static java.util.Comparator.comparing; +import static java.util.Comparator.nullsLast; + /** * A simple holder for some instances needed for either Endpoints or EndpointSlice catalog * implementations. @@ -39,7 +41,7 @@ record Fabric8CatalogWatchContext(KubernetesClient kubernetesClient, KubernetesD static List state(Stream references) { return references.filter(Objects::nonNull).map(x -> new EndpointNameAndNamespace(x.getName(), x.getNamespace())) - .sorted(Comparator.comparing(EndpointNameAndNamespace::endpointName, String::compareTo)).toList(); + .sorted(comparing(EndpointNameAndNamespace::endpointName, nullsLast(String::compareTo))).toList(); } } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java index dc2d6528c5..79b99aad2a 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java @@ -17,25 +17,19 @@ package org.springframework.cloud.kubernetes.fabric8.discovery; import java.util.Collections; -import java.util.List; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; -import org.apache.commons.logging.Log; -import org.springframework.boot.BootstrapContext; import org.springframework.boot.BootstrapRegistry; -import org.springframework.boot.context.properties.bind.BindHandler; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerConfigDataLocationResolver.PropertyResolver; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; -import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.ServicePortSecureResolver; import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; -import org.springframework.util.ClassUtils; /** * @author Ryan Baxter @@ -44,71 +38,49 @@ class Fabric8ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper @Override public void initialize(BootstrapRegistry registry) { - if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + if (hasConfigServerInstanceProvider()) { return; } - // We need to pass a lambda here rather than create a new instance of - // ConfigServerInstanceProvider.Function - // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on - // the classpath - registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); - } - - final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { - private final BootstrapContext context; - - private KubernetesFunction(BootstrapContext context) { - this.context = context; - } - - static KubernetesFunction create(BootstrapContext context) { - return new KubernetesFunction(context); - } + registry.registerIfAbsent(KubernetesDiscoveryProperties.class, context -> { + if (!getDiscoveryEnabled(context)) { + return null; + } + return createKubernetesDiscoveryProperties(context); + }); - @Override - public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { - if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { - // If we don't have the Binder or BinderHandler from the - // ConfigDataLocationResolverContext - // we won't be able to create the necessary configuration - // properties to configure the - // Kubernetes DiscoveryClient - return Collections.emptyList(); + registry.registerIfAbsent(KubernetesClientProperties.class, context -> { + if (!getDiscoveryEnabled(context)) { + return null; } - KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, - bindHandler); - KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); - return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler) - .getInstances(serviceId); - } + return createKubernetesClientProperties(context); + }); - private KubernetesConfigServerInstanceProvider getInstanceProvider( - KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, - BootstrapContext context, Binder binder, BindHandler bindHandler) { + // create instance provider + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> { + if (!getDiscoveryEnabled(context)) { + return (id) -> Collections.emptyList(); + } if (context.isRegistered(KubernetesDiscoveryClient.class)) { KubernetesDiscoveryClient client = context.get(KubernetesDiscoveryClient.class); return client::getInstances; } else { + PropertyResolver propertyResolver = getPropertyResolver(context); Fabric8AutoConfiguration fabric8AutoConfiguration = new Fabric8AutoConfiguration(); - Config config = fabric8AutoConfiguration.kubernetesClientConfig(clientProperties); + Config config = fabric8AutoConfiguration + .kubernetesClientConfig(context.get(KubernetesClientProperties.class)); KubernetesClient kubernetesClient = fabric8AutoConfiguration.kubernetesClient(config); + KubernetesDiscoveryProperties discoveryProperties = context.get(KubernetesDiscoveryProperties.class); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient, - discoveryProperties, KubernetesClientServicesFunctionProvider - .servicesFunction(discoveryProperties, binder, bindHandler), + discoveryProperties, + KubernetesClientServicesFunctionProvider.servicesFunction(discoveryProperties, + new KubernetesNamespaceProvider(propertyResolver + .get(KubernetesNamespaceProvider.NAMESPACE_PROPERTY, String.class, null))), null, new ServicePortSecureResolver(discoveryProperties)); return discoveryClient::getInstances; } - } - - // This method should never be called, but is there for backward - // compatibility purposes - @Override - public List apply(String serviceId) { - return apply(serviceId, null, null, null); - } - + }); } } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java index d58931f000..7895f8cf99 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java @@ -65,27 +65,23 @@ private Fabric8KubernetesDiscoveryClientUtils() { } - static EndpointSubsetNS subsetsFromEndpoints(Endpoints endpoints) { - return new EndpointSubsetNS(endpoints.getMetadata().getNamespace(), endpoints.getSubsets()); - } - static List endpoints(KubernetesDiscoveryProperties properties, KubernetesClient client, KubernetesNamespaceProvider namespaceProvider, String target, @Nullable String serviceName, Predicate filter) { List endpoints; - if (properties.allNamespaces()) { - LOG.debug(() -> "discovering endpoints in all namespaces"); - endpoints = filteredEndpoints(client.endpoints().inAnyNamespace().withNewFilter(), properties, serviceName); - } - else if (!properties.namespaces().isEmpty()) { + if (!properties.namespaces().isEmpty()) { LOG.debug(() -> "discovering endpoints in namespaces : " + properties.namespaces()); List inner = new ArrayList<>(properties.namespaces().size()); properties.namespaces().forEach(namespace -> inner.addAll(filteredEndpoints( client.endpoints().inNamespace(namespace).withNewFilter(), properties, serviceName))); endpoints = inner; } + else if (properties.allNamespaces()) { + LOG.debug(() -> "discovering endpoints in all namespaces"); + endpoints = filteredEndpoints(client.endpoints().inAnyNamespace().withNewFilter(), properties, serviceName); + } else { String namespace = Fabric8Utils.getApplicationNamespace(client, null, target, namespaceProvider); LOG.debug(() -> "discovering endpoints in namespace : " + namespace); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java index 7ab4e56d23..805623c22e 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java @@ -35,6 +35,7 @@ import org.springframework.core.log.LogAccessor; import org.springframework.scheduling.annotation.Scheduled; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_GROUP; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_VERSION; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.ENDPOINT_SLICE; @@ -66,7 +67,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } - @Scheduled(fixedDelayString = "${spring.cloud.kubernetes.discovery.catalogServicesWatchDelay:30000}") + @Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}") public void catalogServicesWatch() { try { diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfiguration.java index 2d7b940b13..1c7080f29b 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfiguration.java @@ -19,12 +19,9 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogWatcherEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; @@ -38,9 +35,7 @@ * @author Tim Ysewyn */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesCatalogEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnKubernetesCatalogWatcherEnabled @AutoConfigureAfter({ Fabric8AutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) public class KubernetesCatalogWatchAutoConfiguration { diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java index 820d3a3f72..07cbf1a8de 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java @@ -48,6 +48,11 @@ public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscov public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties, Binder binder, BindHandler bindHandler) { + return servicesFunction(properties, new KubernetesNamespaceProvider(binder, bindHandler)); + } + + public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties, + KubernetesNamespaceProvider namespaceProvider) { if (properties.allNamespaces()) { return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels()); @@ -55,7 +60,7 @@ public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscov return client -> { String namespace = Fabric8Utils.getApplicationNamespace(client, null, "discovery-service", - new KubernetesNamespaceProvider(binder, bindHandler)); + namespaceProvider); return client.services().inNamespace(namespace).withLabels(properties.serviceLabels()); }; diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java index a18d70e202..6a5a786cf2 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java @@ -113,13 +113,12 @@ public String description() { public List getInstances(String serviceId) { Objects.requireNonNull(serviceId); - List subsetsNS = getEndPointsList(serviceId).stream() - .map(Fabric8KubernetesDiscoveryClientUtils::subsetsFromEndpoints).toList(); + List allEndpoints = getEndPointsList(serviceId).stream().toList(); List instances = new ArrayList<>(); - for (EndpointSubsetNS es : subsetsNS) { - // subsetsNS are only those that matched the serviceId - instances.addAll(serviceInstances(es, serviceId)); + for (Endpoints endpoints : allEndpoints) { + // endpoints are only those that matched the serviceId + instances.addAll(serviceInstances(endpoints, serviceId)); } if (properties.includeExternalNameServices()) { @@ -148,15 +147,15 @@ public List getEndPointsList(String serviceId) { return endpoints(properties, client, namespaceProvider, "fabric8-discovery", serviceId, adapter.filter()); } - private List serviceInstances(EndpointSubsetNS es, String serviceId) { + private List serviceInstances(Endpoints endpoints, String serviceId) { - List subsets = es.endpointSubset(); + List subsets = endpoints.getSubsets(); if (subsets.isEmpty()) { LOG.debug(() -> "serviceId : " + serviceId + " does not have any subsets"); return List.of(); } - String namespace = es.namespace(); + String namespace = endpoints.getMetadata().getNamespace(); List instances = new ArrayList<>(); Service service = client.services().inNamespace(namespace).withName(serviceId).get(); @@ -187,7 +186,9 @@ private List serviceInstances(EndpointSubsetNS es, String servi @Override public List getServices() { - return adapter.apply(client).stream().map(s -> s.getMetadata().getName()).distinct().toList(); + List services = adapter.apply(client).stream().map(s -> s.getMetadata().getName()).distinct().toList(); + LOG.debug(() -> "will return services : " + services); + return services; } @Deprecated(forRemoval = true) diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java index 27d7297da3..fdf7d7b90b 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -19,20 +19,14 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; import org.springframework.cloud.client.CommonsClientAutoConfiguration; -import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; -import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.PodUtils; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -51,10 +45,7 @@ * @author Tim Ysewyn */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnBlockingDiscoveryEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnSpringCloudKubernetesBlockingDiscovery @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ Fabric8AutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) public class KubernetesDiscoveryClientAutoConfiguration { @@ -79,8 +70,7 @@ public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient clie } @Bean - @ConditionalOnClass({ HealthIndicator.class }) - @ConditionalOnDiscoveryHealthIndicatorEnabled + @ConditionalOnSpringCloudKubernetesBlockingDiscoveryHealthInitializer public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer( ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { LOG.debug(() -> "Will publish InstanceRegisteredEvent from blocking implementation"); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java index 948d37bf85..bed427f010 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java @@ -22,19 +22,16 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; -import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; import org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration; import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.commons.PodUtils; -import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscovery; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -53,10 +50,7 @@ * @author Tim Ysewyn */ @Configuration(proxyBeanMethods = false) -@ConditionalOnDiscoveryEnabled -@ConditionalOnKubernetesDiscoveryEnabled -@ConditionalOnReactiveDiscoveryEnabled -@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@ConditionalOnSpringCloudKubernetesReactiveDiscovery @AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class, ReactiveCommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class, @@ -94,8 +88,7 @@ KubernetesDiscoveryClientHealthIndicatorInitializer reactiveIndicatorInitializer } @Bean - @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") - @ConditionalOnDiscoveryHealthIndicatorEnabled + @ConditionalOnSpringCloudKubernetesReactiveDiscoveryHealthInitializer public ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( KubernetesReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties) { return new ReactiveDiscoveryClientHealthIndicator(client, properties); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContextTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContextTests.java new file mode 100644 index 0000000000..3324baaba1 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8CatalogWatchContextTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.List; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.ObjectReference; +import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; + +/** + * @author wind57 + */ +class Fabric8CatalogWatchContextTests { + + @Test + void stateWithASingleElementNameNotNull() { + + Stream referenceStream = Stream + .of(new ObjectReferenceBuilder().withName("a").withNamespace("default").build()); + + List result = Fabric8CatalogWatchContext.state(referenceStream); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).endpointName(), "a"); + Assertions.assertEquals(result.get(0).namespace(), "default"); + + } + + @Test + void stateWithASingleElementNameNull() { + + Stream referenceStream = Stream + .of(new ObjectReferenceBuilder().withName(null).withNamespace("default").build()); + + List result = Fabric8CatalogWatchContext.state(referenceStream); + Assertions.assertEquals(result.size(), 1); + Assertions.assertNull(result.get(0).endpointName()); + Assertions.assertEquals(result.get(0).namespace(), "default"); + + } + + @Test + void stateWithTwoElementsNameNull() { + + Stream referenceStream = Stream.of( + new ObjectReferenceBuilder().withName(null).withNamespace("defaultNull").build(), + new ObjectReferenceBuilder().withName("a").withNamespace("defaultA").build()); + + List result = Fabric8CatalogWatchContext.state(referenceStream); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).endpointName(), "a"); + Assertions.assertEquals(result.get(0).namespace(), "defaultA"); + Assertions.assertNull(result.get(1).endpointName()); + Assertions.assertEquals(result.get(1).namespace(), "defaultNull"); + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java index 92eda531a0..07267b4031 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java @@ -143,7 +143,7 @@ void testInAllNamespacesWithoutServiceLabels() { @Override void testAllNamespacesTrueOtherBranchesNotCalled() { - KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of("B"), + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of(), ENDPOINT_SLICES); endpoints("namespaceA", Map.of(), "podA"); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java index 16e0bc53a5..fcf276a4fe 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java @@ -375,7 +375,7 @@ void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() { } @Test - void instanceWithoutPortsShouldBeSkipped() { + void instanceWithoutSubsetsShouldBeSkipped() { Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint1").withNamespace("test") .withLabels(Collections.emptyMap()).endMetadata().build(); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java index 5dcb172a83..a19db4f5e3 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java @@ -292,7 +292,7 @@ void testPortsDataOne() { new EndpointSubsetBuilder() .withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build()); - Map portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets); + Map portsData = endpointSubsetsPortData(endpointSubsets); Assertions.assertEquals(portsData.size(), 2); Assertions.assertEquals(portsData.get("https"), 8080); Assertions.assertEquals(portsData.get(""), 8081); @@ -306,7 +306,7 @@ void testPortsDataTwo() { new EndpointSubsetBuilder() .withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build()); - Map portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets); + Map portsData = endpointSubsetsPortData(endpointSubsets); Assertions.assertEquals(portsData.size(), 2); Assertions.assertEquals(portsData.get("https"), 8080); Assertions.assertEquals(portsData.get("http"), 8081); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java index fb13b9fdca..0978a6b68b 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchAutoConfigurationApplicationContextTests.java @@ -61,22 +61,36 @@ void kubernetesDiscoveryEnabled() { applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); } - // disabling discovery has no impact on the catalog watch. + // disabling discovery should disable catalog watcher. @Test void kubernetesDiscoveryDisabled() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); - applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class)); + applicationContextRunner.run(context -> assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class)); } /** - * both blocking and reactive configs are disabled, should not influence catalog - * watcher in any way. + * both blocking and reactive configs are disabled, catalog watcher is disabled too. */ @Test void disableBlockingAndReactive() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesClientServicesFunction.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + /** + * blocking is disabled, reactive is enabled, catalog watcher is enabled. + */ + @Test + void disableBlockingEnableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=true"); applicationContextRunner.run(context -> { assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); assertThat(context).doesNotHaveBean(KubernetesClientServicesFunction.class); @@ -86,15 +100,30 @@ void disableBlockingAndReactive() { } /** - * spring.cloud.kubernetes.discovery.enabled is false, but does not influence catalog - * watcher. + * blocking is enabled, reactive is disabled, catalog watcher is enabled. + */ + @Test + void enableBlockingDisableReactive() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=true", "spring.cloud.discovery.reactive.enabled=false"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesClientServicesFunction.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); + }); + } + + /** + * spring.cloud.kubernetes.discovery.enabled is false, catalog watcher is disabled + * also. */ @Test void disableKubernetesDiscovery() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesCatalogWatch.class); + assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class); assertThat(context).doesNotHaveBean(KubernetesClientServicesFunction.class); assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java index 96d9c76302..f7abe74f7c 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java @@ -22,49 +22,18 @@ import io.fabric8.kubernetes.api.model.EndpointAddress; import io.fabric8.kubernetes.api.model.EndpointAddressBuilder; -import io.fabric8.kubernetes.api.model.EndpointPortBuilder; import io.fabric8.kubernetes.api.model.EndpointSubset; import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; -import io.fabric8.kubernetes.api.model.Endpoints; -import io.fabric8.kubernetes.api.model.EndpointsBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; /** * @author wind57 */ -@ExtendWith(OutputCaptureExtension.class) class KubernetesDiscoveryClientUtilsTests { - @Test - void testSubsetsFromEndpointsEmptySubsets() { - Endpoints endpoints = new EndpointsBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("non-default").build()).build(); - EndpointSubsetNS result = Fabric8KubernetesDiscoveryClientUtils.subsetsFromEndpoints(endpoints); - Assertions.assertNotNull(result); - Assertions.assertEquals(result.endpointSubset(), List.of()); - Assertions.assertEquals(result.namespace(), "non-default"); - } - - @Test - void testSubsetsFromEndpointsNonEmptySubsets() { - Endpoints endpoints = new EndpointsBuilder().withSubsets((List) null) - .withMetadata(new ObjectMetaBuilder().withNamespace("default").build()) - .withSubsets( - new EndpointSubsetBuilder().withPorts(new EndpointPortBuilder().withPort(8080).build()).build()) - .build(); - EndpointSubsetNS result = Fabric8KubernetesDiscoveryClientUtils.subsetsFromEndpoints(endpoints); - Assertions.assertNotNull(result); - Assertions.assertEquals(result.endpointSubset().size(), 1); - Assertions.assertEquals(result.endpointSubset().get(0).getPorts().get(0).getPort(), 8080); - Assertions.assertEquals(result.namespace(), "default"); - } - /** *
 	 *      - ready addresses are empty
diff --git a/spring-cloud-kubernetes-fabric8-istio/pom.xml b/spring-cloud-kubernetes-fabric8-istio/pom.xml
index 53a74cff24..5b07fd306c 100644
--- a/spring-cloud-kubernetes-fabric8-istio/pom.xml
+++ b/spring-cloud-kubernetes-fabric8-istio/pom.xml
@@ -5,7 +5,7 @@
 	
 		spring-cloud-kubernetes
 		org.springframework.cloud
-		3.1.0-SNAPSHOT
+		3.1.1-SNAPSHOT
 	
 	4.0.0
 
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfiguration.java
similarity index 95%
rename from spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfiguration.java
rename to spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfiguration.java
index 036bb2db0b..074b1f7d04 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfiguration.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfiguration.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import io.fabric8.istio.client.DefaultIstioClient;
 import io.fabric8.istio.client.IstioClient;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioBootstrapConfiguration.java b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioBootstrapConfiguration.java
similarity index 94%
rename from spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioBootstrapConfiguration.java
rename to spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioBootstrapConfiguration.java
index c379a1dab9..fc6d3cd69c 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioBootstrapConfiguration.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioBootstrapConfiguration.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import java.util.Arrays;
 
@@ -25,7 +25,7 @@
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cloud.kubernetes.fabric8.istio.utils.MeshUtils;
+import org.springframework.cloud.kubernetes.fabric8.client.istio.utils.MeshUtils;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.ConfigurableEnvironment;
@@ -69,7 +69,7 @@ public void detectIstio() {
 		}
 
 		void addIstioProfile(ConfigurableEnvironment environment) {
-			if (this.utils.isIstioEnabled()) {
+			if (utils.isIstioEnabled()) {
 				if (hasIstioProfile(environment)) {
 					if (LOG.isDebugEnabled()) {
 						LOG.debug("'istio' already in list of active profiles");
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioClientProperties.java b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioClientProperties.java
similarity index 95%
rename from spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioClientProperties.java
rename to spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioClientProperties.java
index 44725eea8a..526f8b7fc2 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioClientProperties.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioClientProperties.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/utils/MeshUtils.java b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/utils/MeshUtils.java
similarity index 91%
rename from spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/utils/MeshUtils.java
rename to spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/utils/MeshUtils.java
index 912ab9c497..39090ece43 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/utils/MeshUtils.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/utils/MeshUtils.java
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio.utils;
+package org.springframework.cloud.kubernetes.fabric8.client.istio.utils;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.springframework.boot.web.client.RestTemplateBuilder;
-import org.springframework.cloud.kubernetes.fabric8.istio.IstioClientProperties;
+import org.springframework.cloud.kubernetes.fabric8.client.istio.IstioClientProperties;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.client.RestTemplate;
 
@@ -35,7 +35,7 @@ public class MeshUtils {
 
 	private final IstioClientProperties istioClientProperties;
 
-	private RestTemplate restTemplate = new RestTemplateBuilder().build();
+	private final RestTemplate restTemplate = new RestTemplateBuilder().build();
 
 	public MeshUtils(IstioClientProperties istioClientProperties) {
 		this.istioClientProperties = istioClientProperties;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring.factories
index a0f5ea2091..ecffe4449f 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring.factories
@@ -1,2 +1,2 @@
 org.springframework.cloud.bootstrap.BootstrapConfiguration=\
-org.springframework.cloud.kubernetes.fabric8.istio.IstioBootstrapConfiguration
+org.springframework.cloud.kubernetes.fabric8.client.istio.IstioBootstrapConfiguration
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index e1a8b91294..28e556d309 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-cloud-kubernetes-fabric8-istio/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1 +1 @@
-org.springframework.cloud.kubernetes.fabric8.istio.IstioAutoConfiguration
+org.springframework.cloud.kubernetes.fabric8.client.istio.IstioAutoConfiguration
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/App.java b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/App.java
similarity index 91%
rename from spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/App.java
rename to spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/App.java
index 2a63891789..50d6e1b25a 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/App.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/App.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabled.java b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabledTest.java
similarity index 90%
rename from spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabled.java
rename to spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabledTest.java
index e8eb563951..29ebadb348 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabled.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenIstioDisabledTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import io.fabric8.istio.client.IstioClient;
 import org.junit.jupiter.api.Test;
@@ -30,7 +30,7 @@
  */
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class,
 		properties = { "spring.cloud.istio.enabled=false" })
-class IstioAutoConfigurationClientNotPresentWhenIstioDisabled {
+class IstioAutoConfigurationClientNotPresentWhenIstioDisabledTest {
 
 	@Autowired
 	private ConfigurableApplicationContext context;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabled.java b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabledTest.java
similarity index 94%
rename from spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabled.java
rename to spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabledTest.java
index 688c2766c6..b11eb1d45e 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabled.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientNotPresentWhenKubernetesDisabledTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import io.fabric8.istio.client.IstioClient;
 import org.junit.jupiter.api.Test;
@@ -29,7 +29,7 @@
  * @author wind57
  */
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class)
-class IstioAutoConfigurationClientNotPresentWhenKubernetesDisabled {
+class IstioAutoConfigurationClientNotPresentWhenKubernetesDisabledTest {
 
 	@Autowired
 	private ConfigurableApplicationContext context;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentByDefault.java b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentByDefaultTest.java
similarity index 91%
rename from spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentByDefault.java
rename to spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentByDefaultTest.java
index d6a9df60e9..df133c2e83 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentByDefault.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentByDefaultTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import io.fabric8.istio.client.IstioClient;
 import org.junit.jupiter.api.Test;
@@ -30,7 +30,7 @@
  */
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class,
 		properties = "spring.main.cloud-platform=KUBERNETES")
-class IstioAutoConfigurationClientPresentByDefault {
+class IstioAutoConfigurationClientPresentByDefaultTest {
 
 	@Autowired
 	private ConfigurableApplicationContext context;
diff --git a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentWhenIstioEnabled.java b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentWhenIstioEnabledTest.java
similarity index 91%
rename from spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentWhenIstioEnabled.java
rename to spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentWhenIstioEnabledTest.java
index f8c898e93d..ae34274c2e 100644
--- a/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioAutoConfigurationClientPresentWhenIstioEnabled.java
+++ b/spring-cloud-kubernetes-fabric8-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioAutoConfigurationClientPresentWhenIstioEnabledTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.cloud.kubernetes.fabric8.istio;
+package org.springframework.cloud.kubernetes.fabric8.client.istio;
 
 import io.fabric8.istio.client.IstioClient;
 import org.junit.jupiter.api.Test;
@@ -30,7 +30,7 @@
  */
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class,
 		properties = { "spring.cloud.istio.enabled=true", "spring.main.cloud-platform=KUBERNETES" })
-class IstioAutoConfigurationClientPresentWhenIstioEnabled {
+class IstioAutoConfigurationClientPresentWhenIstioEnabledTest {
 
 	@Autowired
 	private ConfigurableApplicationContext context;
diff --git a/spring-cloud-kubernetes-fabric8-leader/pom.xml b/spring-cloud-kubernetes-fabric8-leader/pom.xml
index f0d4aa061c..5a0d48d1ed 100644
--- a/spring-cloud-kubernetes-fabric8-leader/pom.xml
+++ b/spring-cloud-kubernetes-fabric8-leader/pom.xml
@@ -22,7 +22,7 @@
 	
 		org.springframework.cloud
 		spring-cloud-kubernetes
-		3.1.0-SNAPSHOT
+		3.1.1-SNAPSHOT
 	
 
 	spring-cloud-kubernetes-fabric8-leader
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml
index 01ea247ba8..5e2e3fc1ca 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml
@@ -5,7 +5,7 @@
 	
 		org.springframework.cloud
 		spring-cloud-kubernetes
-		3.1.0-SNAPSHOT
+		3.1.1-SNAPSHOT
 	
 	4.0.0
 
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfiguration.java
index 5a55951e74..2d91a46e97 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfiguration.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,10 +17,10 @@
 package org.springframework.cloud.kubernetes.fabric8.loadbalancer;
 
 import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.cloud.CloudPlatform;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
+import org.springframework.cloud.kubernetes.commons.loadbalancer.ConditionalOnKubernetesLoadBalancerEnabled;
 import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
 import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
 import org.springframework.context.annotation.Bean;
@@ -34,7 +34,7 @@
 @Configuration(proxyBeanMethods = false)
 @EnableConfigurationProperties(KubernetesLoadBalancerProperties.class)
 @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
-@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true)
+@ConditionalOnKubernetesLoadBalancerEnabled
 @LoadBalancerClients(defaultConfiguration = Fabric8LoadBalancerClientConfiguration.class)
 public class Fabric8LoadBalancerAutoConfiguration {
 
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerClientConfiguration.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerClientConfiguration.java
index 7d73a76814..09ca17f6e1 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerClientConfiguration.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerClientConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,8 +18,8 @@
 
 import io.fabric8.kubernetes.client.KubernetesClient;
 
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
+import org.springframework.cloud.kubernetes.commons.loadbalancer.ConditionalOnKubernetesLoadBalancerServiceModeEnabled;
 import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.Bean;
@@ -33,7 +33,7 @@
 public class Fabric8LoadBalancerClientConfiguration {
 
 	@Bean
-	@ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE")
+	@ConditionalOnKubernetesLoadBalancerServiceModeEnabled
 	ServiceInstanceListSupplier kubernetesServicesListSupplier(Environment environment,
 			KubernetesClient kubernetesClient, Fabric8ServiceInstanceMapper mapper,
 			KubernetesDiscoveryProperties discoveryProperties, ConfigurableApplicationContext context) {
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java
index 1fb1948e2f..9c4d2eda8d 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java
@@ -51,8 +51,8 @@ public class Fabric8ServiceInstanceMapper implements KubernetesServiceInstanceMa
 
 	@Override
 	public KubernetesServiceInstance map(Service service) {
-		final ObjectMeta meta = service.getMetadata();
-		final List ports = service.getSpec().getPorts();
+		ObjectMeta meta = service.getMetadata();
+		List ports = service.getSpec().getPorts();
 		ServicePort port = null;
 		if (ports.size() == 1) {
 			port = ports.get(0);
@@ -67,16 +67,16 @@ else if (ports.size() > 1 && Utils.isNotNullOrEmpty(this.properties.getPortName(
 		if (port == null) {
 			return null;
 		}
-		final String host = KubernetesServiceInstanceMapper.createHost(service.getMetadata().getName(),
+		String host = KubernetesServiceInstanceMapper.createHost(service.getMetadata().getName(),
 				service.getMetadata().getNamespace(), properties.getClusterDomain());
-		final boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(),
+		boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(),
 				service.getMetadata().getAnnotations(), port.getName(), port.getPort());
 		return new DefaultKubernetesServiceInstance(meta.getUid(), meta.getName(), host, port.getPort(),
 				getServiceMetadata(service), secure);
 	}
 
 	private Map getServiceMetadata(Service service) {
-		final Map serviceMetadata = new HashMap<>();
+		Map serviceMetadata = new HashMap<>();
 		KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties.metadata();
 		if (metadataProps.addLabels()) {
 			Map labelMetadata = KubernetesServiceInstanceMapper
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java
index 00e0a6fe9c..0972271c56 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java
@@ -36,7 +36,7 @@
  *
  * @author Piotr Minkowski
  */
-public class Fabric8ServicesListSupplier extends KubernetesServicesListSupplier {
+public class Fabric8ServicesListSupplier extends KubernetesServicesListSupplier {
 
 	private final KubernetesClient kubernetesClient;
 
@@ -50,15 +50,14 @@ public class Fabric8ServicesListSupplier extends KubernetesServicesListSupplier
 	public Flux> get() {
 		List result = new ArrayList<>();
 		if (discoveryProperties.allNamespaces()) {
-			List services = this.kubernetesClient.services().inAnyNamespace()
-					.withField("metadata.name", this.getServiceId()).list().getItems();
+			List services = kubernetesClient.services().inAnyNamespace()
+					.withField("metadata.name", getServiceId()).list().getItems();
 			services.forEach(service -> result.add(mapper.map(service)));
 		}
 		else {
-			Service service = StringUtils.hasText(this.kubernetesClient.getNamespace())
-					? this.kubernetesClient.services().inNamespace(this.kubernetesClient.getNamespace())
-							.withName(this.getServiceId()).get()
-					: this.kubernetesClient.services().withName(this.getServiceId()).get();
+			Service service = StringUtils.hasText(kubernetesClient.getNamespace()) ? kubernetesClient.services()
+					.inNamespace(kubernetesClient.getNamespace()).withName(getServiceId()).get()
+					: kubernetesClient.services().withName(getServiceId()).get();
 			if (service != null) {
 				result.add(mapper.map(service));
 			}
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfigurationTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfigurationTests.java
index 52bc6bdfc5..0994bee637 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfigurationTests.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8LoadBalancerAutoConfigurationTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,13 +16,13 @@
 
 package org.springframework.cloud.kubernetes.fabric8.loadbalancer;
 
-import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
-import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
 import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
-import org.springframework.context.ConfigurableApplicationContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -31,48 +31,53 @@
  */
 class Fabric8LoadBalancerAutoConfigurationTests {
 
-	private ConfigurableApplicationContext context;
-
-	@AfterEach
-	public void close() {
-		if (this.context != null) {
-			this.context.close();
-		}
-	}
-
 	@Test
 	void kubernetesLoadBalancerWhenKubernetesDisabledAndLoadBalancerDisabled() {
-		setup("spring.cloud.kubernetes.loadbalancer.enabled=false");
-		assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
+		new ApplicationContextRunner().withUserConfiguration(Fabric8LoadBalancerAutoConfigurationTests.Config.class)
+				.withConfiguration(AutoConfigurations.of(Fabric8LoadBalancerAutoConfiguration.class))
+				.withPropertyValues("spring.cloud.kubernetes.loadbalancer.enabled=false")
+				.run(this::assertInstanceMapperMissing);
 	}
 
 	@Test
 	void kubernetesLoadBalancerWhenKubernetesDisabledAndLoadBalancerEnabled() {
-		setup("spring.cloud.kubernetes.loadbalancer.enabled=true");
-		assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
+		new ApplicationContextRunner().withUserConfiguration(Fabric8LoadBalancerAutoConfigurationTests.Config.class)
+				.withConfiguration(AutoConfigurations.of(Fabric8LoadBalancerAutoConfiguration.class))
+				.withPropertyValues("spring.cloud.kubernetes.loadbalancer.enabled=true")
+				.run(this::assertInstanceMapperMissing);
 	}
 
 	@Test
 	void kubernetesLoadBalancerWhenKubernetesEnabledAndLoadBalancerEnabled() {
-		setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.loadbalancer.enabled=true");
-		assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).hasSize(1);
+		new ApplicationContextRunner().withUserConfiguration(Fabric8LoadBalancerAutoConfigurationTests.Config.class)
+				.withConfiguration(AutoConfigurations.of(Fabric8LoadBalancerAutoConfiguration.class))
+				.withPropertyValues("spring.cloud.kubernetes.loadbalancer.enabled=true",
+						"spring.main.cloud-platform=KUBERNETES")
+				.run(this::assertInstanceMapperPresent);
 	}
 
 	@Test
 	void kubernetesLoadBalancerWhenKubernetesEnabledAndLoadBalancerDisabled() {
-		setup("spring.cloud.kubernetes.loadbalancer.enabled=false");
-		assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
+		new ApplicationContextRunner().withUserConfiguration(Fabric8LoadBalancerAutoConfigurationTests.Config.class)
+				.withConfiguration(AutoConfigurations.of(Fabric8LoadBalancerAutoConfiguration.class))
+				.withPropertyValues("spring.cloud.kubernetes.loadbalancer.enabled=false",
+						"spring.main.cloud-platform=KUBERNETES")
+				.run(this::assertInstanceMapperMissing);
 	}
 
 	@Test
 	void kubernetesLoadBalancerWhenDefaultProperties() {
-		setup("spring.main.cloud-platform=KUBERNETES");
-		assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).hasSize(1);
+		new ApplicationContextRunner().withUserConfiguration(Fabric8LoadBalancerAutoConfigurationTests.Config.class)
+				.withConfiguration(AutoConfigurations.of(Fabric8LoadBalancerAutoConfiguration.class))
+				.withPropertyValues("spring.main.cloud-platform=KUBERNETES").run(this::assertInstanceMapperPresent);
+	}
+
+	private void assertInstanceMapperMissing(AssertableApplicationContext context) {
+		assertThat(context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
 	}
 
-	private void setup(String... env) {
-		this.context = new SpringApplicationBuilder(Fabric8LoadBalancerAutoConfiguration.class, Config.class)
-				.web(org.springframework.boot.WebApplicationType.NONE).properties(env).run();
+	private void assertInstanceMapperPresent(AssertableApplicationContext context) {
+		assertThat(context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).hasSize(1);
 	}
 
 	@EnableConfigurationProperties(KubernetesDiscoveryProperties.class)
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java
index e8b18dce43..14b1b945db 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -37,9 +36,9 @@
 class Fabric8ServiceInstanceMapperTests {
 
 	@Test
-	public void testMapperSimple() {
+	void testMapperSimple() {
 		KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
-		Service service = buildService("test", "abc", 8080, null, new HashMap<>());
+		Service service = buildService("test", "abc", 8080, null, Map.of());
 		KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties,
 				KubernetesDiscoveryProperties.DEFAULT).map(service);
 		Assertions.assertNotNull(instance);
@@ -54,7 +53,7 @@ void testMapperMultiplePorts() {
 		List ports = new ArrayList<>();
 		ports.add(new ServicePortBuilder().withPort(8080).withName("web").build());
 		ports.add(new ServicePortBuilder().withPort(9000).withName("http").build());
-		Service service = buildService("test", "abc", ports, new HashMap<>());
+		Service service = buildService("test", "abc", ports, Map.of());
 		KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties,
 				KubernetesDiscoveryProperties.DEFAULT).map(service);
 		Assertions.assertNotNull(instance);
@@ -66,7 +65,7 @@ void testMapperMultiplePorts() {
 	@Test
 	void testMapperSecure() {
 		KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
-		Service service = buildService("test", "abc", 443, null, new HashMap<>());
+		Service service = buildService("test", "abc", 443, null, Map.of());
 		KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties,
 				KubernetesDiscoveryProperties.DEFAULT).map(service);
 		Assertions.assertNotNull(instance);
@@ -95,9 +94,7 @@ void testMapperSecureNullLabelsAndAnnotations() {
 	@Test
 	void testMapperSecureWithLabels() {
 		KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
-		HashMap labels = new HashMap<>();
-		labels.put("secured", "true");
-		labels.put("label1", "123");
+		Map labels = Map.of("secured", "true", "label1", "123");
 		Service service = buildService("test", "abc", 8080, null, labels);
 		KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties,
 				KubernetesDiscoveryProperties.DEFAULT).map(service);
@@ -120,7 +117,7 @@ private Service buildService(String name, String uid, List ports, M
 	}
 
 	private Service buildService(String name, String uid, List ports, Map labels) {
-		return buildService(name, uid, ports, labels, new HashMap<>(0));
+		return buildService(name, uid, ports, labels, Map.of());
 	}
 
 }
diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/KubernetesServiceListSupplierTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java
similarity index 72%
rename from spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/KubernetesServiceListSupplierTests.java
rename to spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java
index 9e46346688..1e41a432ab 100644
--- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/KubernetesServiceListSupplierTests.java
+++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,46 +30,44 @@
 import io.fabric8.kubernetes.client.dsl.ServiceResource;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mockito;
 
 import org.springframework.cloud.client.ServiceInstance;
 import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
 import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
 import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
 import org.springframework.core.env.Environment;
+import org.springframework.mock.env.MockEnvironment;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
 
-@ExtendWith(MockitoExtension.class)
-class KubernetesServiceListSupplierTests {
+class Fabric8ServiceListSupplierTests {
 
-	@Mock
-	Environment environment;
+	private final Environment environment = new MockEnvironment().withProperty("loadbalancer.client.name",
+			"test-service");
 
-	@Mock
-	Fabric8ServiceInstanceMapper mapper;
+	private final Fabric8ServiceInstanceMapper mapper = Mockito.mock(Fabric8ServiceInstanceMapper.class);
 
-	@Mock
-	KubernetesClient client;
+	private final KubernetesClient client = Mockito.mock(KubernetesClient.class);
 
-	@Mock
-	MixedOperation> serviceOperation;
+	@SuppressWarnings("unchecked")
+	private final MixedOperation> serviceOperation = Mockito
+			.mock(MixedOperation.class);
 
-	@Mock
-	NonNamespaceOperation> namespaceOperation;
+	@SuppressWarnings("unchecked")
+	private final NonNamespaceOperation> namespaceOperation = Mockito
+			.mock(NonNamespaceOperation.class);
 
-	@Mock
-	ServiceResource serviceResource;
+	@SuppressWarnings("unchecked")
+	private final ServiceResource serviceResource = Mockito.mock(ServiceResource.class);
 
-	@Mock
-	AnyNamespaceOperation> multiDeletable;
+	@SuppressWarnings("unchecked")
+	private final AnyNamespaceOperation> multiDeletable = Mockito
+			.mock(AnyNamespaceOperation.class);
 
 	@Test
 	void testPositiveMatch() {
-		when(environment.getProperty("loadbalancer.client.name")).thenReturn("test-service");
 		when(mapper.map(any(Service.class)))
 				.thenReturn(new DefaultKubernetesServiceInstance("", "", "", 0, null, false));
 		when(this.client.getNamespace()).thenReturn("test");
@@ -77,7 +75,7 @@ void testPositiveMatch() {
 		when(this.serviceOperation.inNamespace("test")).thenReturn(namespaceOperation);
 		when(this.namespaceOperation.withName("test-service")).thenReturn(this.serviceResource);
 		when(this.serviceResource.get()).thenReturn(buildService("test-service", 8080));
-		KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
+		KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
 				KubernetesDiscoveryProperties.DEFAULT);
 		List instances = supplier.get().blockFirst();
 		assert instances != null;
@@ -86,7 +84,6 @@ void testPositiveMatch() {
 
 	@Test
 	void testPositiveMatchAllNamespaces() {
-		when(environment.getProperty("loadbalancer.client.name")).thenReturn("test-service");
 		when(mapper.map(any(Service.class)))
 				.thenReturn(new DefaultKubernetesServiceInstance("", "", "", 0, null, false));
 		when(this.client.services()).thenReturn(this.serviceOperation);
@@ -98,7 +95,7 @@ void testPositiveMatchAllNamespaces() {
 		KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, true, Set.of(),
 				true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0,
 				false);
-		KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
+		KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
 				discoveryProperties);
 		List instances = supplier.get().blockFirst();
 		assert instances != null;
diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml
index a19f3bcaa9..4e47756483 100644
--- a/spring-cloud-kubernetes-integration-tests/pom.xml
+++ b/spring-cloud-kubernetes-integration-tests/pom.xml
@@ -6,7 +6,7 @@
 	
 		org.springframework.cloud
 		spring-cloud-kubernetes
-		3.1.0-SNAPSHOT
+		3.1.1-SNAPSHOT
 	
 
 	spring-cloud-kubernetes-integration-tests
@@ -31,7 +31,65 @@
 					true
 				
 			
+
+			
+			
+			
+				org.springframework.boot
+				spring-boot-maven-plugin
+				
+					docker.io/springcloud/${project.artifactId}:${project.version}
+				
+				
+					
+						build-image
+						
+							${skip.build.image}
+						
+						package
+						
+							build-image
+						
+					
+					
+						repackage
+						package
+						
+							repackage
+						
+					
+				
+			
+
+			
+			
+				org.apache.maven.plugins
+				maven-surefire-plugin
+				
+					true
+				
+			
+
+			
+			
+				org.apache.maven.plugins
+				maven-failsafe-plugin
+				
+					
+						
+							integration-test
+						
+					
+				
+				
+					
+						${testsToRun}
+					
+				
+			
+
 		
+
 	
 
 	
@@ -47,29 +105,37 @@
 	
 
 	
-		spring-cloud-kubernetes-fabric8-client-simple-core
-		spring-cloud-kubernetes-fabric8-client-configmap
-		spring-cloud-kubernetes-fabric8-istio-it
+
+		
+		spring-cloud-kubernetes-fabric8-client-catalog-watcher
+		spring-cloud-kubernetes-k8s-client-catalog-watcher
+
+		
 		spring-cloud-kubernetes-fabric8-client-discovery
-		spring-cloud-kubernetes-fabric8-client-loadbalancer
-		spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
-		spring-cloud-kubernetes-fabric8-client-secrets-event-reload
-		spring-cloud-kubernetes-fabric8-client-configmap-event-reload
-
-		spring-cloud-kubernetes-discoveryclient-it
-		spring-cloud-kubernetes-client-config-it
-		spring-cloud-kubernetes-client-loadbalancer-it
-		spring-cloud-kubernetes-client-reactive-discoveryclient-it
-		spring-cloud-kubernetes-configuration-watcher-it
-		spring-cloud-kubernetes-core-k8s-client-it
-        spring-cloud-kubernetes-client-secrets-event-reload
-        spring-cloud-kubernetes-client-configmap-event-reload
-        spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps
-        spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps
-        spring-cloud-kubernetes-fabric8-client-catalog-watcher
-        spring-cloud-kubernetes-client-catalog-watcher
-		spring-cloud-kubernetes-client-discovery-it
-		spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap
-        spring-cloud-kubernetes-client-configmap-polling-reload
+		spring-cloud-kubernetes-k8s-client-discovery
+
+		
+		spring-cloud-kubernetes-fabric8-client-reload
+		spring-cloud-kubernetes-k8s-client-reload
+
+		
+		spring-cloud-kubernetes-fabric8-client-istio
+
+		
+		
+		spring-cloud-kubernetes-k8s-client-discovery-server
+
+		
+		spring-cloud-kubernetes-k8s-client-configuration-watcher
+
+		
+		spring-cloud-kubernetes-k8s-client-loadbalancer
+
+		
+        spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps
+
+		
+        spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps
+
     
 
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/pom.xml
deleted file mode 100644
index 9c4b02569e..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/pom.xml
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-	
-		spring-cloud-kubernetes-integration-tests
-		org.springframework.cloud
-		3.1.0-SNAPSHOT
-	
-	4.0.0
-
-	spring-cloud-kubernetes-client-catalog-watcher
-
-	
-		
-			org.springframework.cloud
-			spring-cloud-kubernetes-client-discovery
-		
-		
-			org.springframework.boot
-			spring-boot-starter-webflux
-		
-		
-			org.springframework.boot
-			spring-boot-starter-actuator
-		
-
-		
-			org.springframework.cloud
-			spring-cloud-kubernetes-test-support
-		
-
-	
-	
-		
-			
-				../src/main/resources
-				true
-			
-			
-				src/main/resources
-				true
-			
-		
-
-		
-			
-			
-			
-				org.springframework.boot
-				spring-boot-maven-plugin
-				
-					docker.io/springcloud/${project.artifactId}:${project.version}
-				
-				
-					
-						build-image
-						
-							${skip.build.image}
-						
-						package
-						
-							build-image
-						
-					
-					
-						repackage
-						package
-						
-							repackage
-						
-					
-				
-			
-
-			
-			
-				org.apache.maven.plugins
-				maven-surefire-plugin
-				
-					true
-				
-			
-
-			
-			
-				org.apache.maven.plugins
-				maven-failsafe-plugin
-				
-					
-						
-							integration-test
-						
-					
-				
-				
-					
-						${testsToRun}
-					
-				
-			
-		
-	
-
-
-
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml
deleted file mode 100644
index cd01c1c29a..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
-  name: spring-cloud-kubernetes-client-catalog-watcher-ingress
-  namespace: default
-spec:
-  rules:
-    - http:
-        paths:
-          - path: /
-            pathType: Prefix
-            backend:
-              service:
-                name: spring-cloud-kubernetes-client-catalog-watcher-service
-                port:
-                  number: 8080
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-service.yaml
deleted file mode 100644
index 53ea6aec71..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-service.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app: spring-cloud-kubernetes-client-catalog-watcher-service
-  name: spring-cloud-kubernetes-client-catalog-watcher-service
-spec:
-  ports:
-    - name: http
-      port: 8080
-      targetPort: 8080
-  selector:
-    app: spring-cloud-kubernetes-client-catalog-watcher
-  type: ClusterIP
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/pom.xml
deleted file mode 100644
index 57b4a74158..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/pom.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-	
-		org.springframework.cloud
-		spring-cloud-kubernetes-integration-tests
-		3.1.0-SNAPSHOT
-	
-
-	4.0.0
-
-	spring-cloud-kubernetes-client-config-it
-
-	
-		
-			org.springframework.cloud
-			spring-cloud-starter-kubernetes-client-config
-		
-		
-			org.springframework.cloud
-			spring-cloud-starter-bootstrap
-		
-		
-			org.springframework.boot
-			spring-boot-starter-webflux
-		
-		
-			org.springframework.boot
-			spring-boot-starter-actuator
-		
-
-		
-			org.springframework.cloud
-			spring-cloud-kubernetes-test-support
-		
-
-	
-
-	
-		
-			
-				../src/main/resources
-				true
-			
-			
-				src/main/resources
-				true
-			
-		
-
-		
-			
-			
-			
-				org.springframework.boot
-				spring-boot-maven-plugin
-				
-					docker.io/springcloud/${project.artifactId}:${project.version}
-				
-				
-					
-						build-image
-						
-							${skip.build.image}
-						
-						package
-						
-							build-image
-						
-					
-					
-						repackage
-						package
-						
-							repackage
-						
-					
-				
-			
-
-			
-			
-				org.apache.maven.plugins
-				maven-surefire-plugin
-				
-					true
-				
-			
-
-			
-			
-				org.apache.maven.plugins
-				maven-failsafe-plugin
-				
-					
-						
-							integration-test
-						
-						
-							
-								${testsToRun}
-							
-						
-					
-				
-			
-		
-	
-
-
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/KubernetesConfigClientApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/KubernetesConfigClientApplicationIt.java
deleted file mode 100644
index a1094e32c9..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/KubernetesConfigClientApplicationIt.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2013-2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.cloud.kubernetes.client.config.it;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * @author Ryan Baxter
- */
-@SpringBootApplication
-@RestController
-public class KubernetesConfigClientApplicationIt {
-
-	private final MyConfigurationProperties configurationProperties;
-
-	public KubernetesConfigClientApplicationIt(MyConfigurationProperties configurationProperties) {
-		this.configurationProperties = configurationProperties;
-	}
-
-	@GetMapping("/myProperty")
-	public String myProperty() {
-		return configurationProperties.getMyProperty();
-	}
-
-	@GetMapping("/mySecret")
-	public String mySecret() {
-		return configurationProperties.getMySecret();
-	}
-
-	public static void main(String[] args) {
-		SpringApplication.run(KubernetesConfigClientApplicationIt.class, args);
-	}
-
-	@Configuration
-	@EnableConfigurationProperties(MyConfigurationProperties.class)
-	static class MyConfig {
-
-	}
-
-}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/MyConfigurationProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/MyConfigurationProperties.java
deleted file mode 100644
index ac43f4a21f..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/java/org/springframework/cloud/kubernetes/client/config/it/MyConfigurationProperties.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2013-2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.cloud.kubernetes.client.config.it;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-/**
- * @author Ryan Baxter
- */
-@ConfigurationProperties("my.config")
-public class MyConfigurationProperties {
-
-	private String myProperty = "default-value";
-
-	private String mySecret = "default-secret-value";
-
-	public MyConfigurationProperties() {
-	}
-
-	public String getMyProperty() {
-		return myProperty;
-	}
-
-	public void setMyProperty(String myProperty) {
-		this.myProperty = myProperty;
-	}
-
-	public String getMySecret() {
-		return mySecret;
-	}
-
-	public void setMySecret(String mySecret) {
-		this.mySecret = mySecret;
-	}
-
-}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/application.yaml
deleted file mode 100644
index 7ea719a7a6..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/application.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-management:
-  endpoints:
-    web:
-      exposure:
-        include: "*"
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap-kubernetes.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap-kubernetes.yaml
deleted file mode 100644
index 7df1beb56c..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap-kubernetes.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-spring:
-  cloud:
-    kubernetes:
-      secrets:
-        enable-api: true
-      reload:
-        enabled: true
-        monitoring-secrets: true
-logging:
-  level:
-    org:
-      springframework:
-        cloud:
-          kubernetes: DEBUG
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap.yaml
deleted file mode 100644
index 1a857f32ae..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/main/resources/bootstrap.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-spring:
-  application:
-    name: spring-cloud-kubernetes-client-config-it
-
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/java/org/springframework/cloud/kubernetes/client/config/it/ConfigMapAndSecretIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/java/org/springframework/cloud/kubernetes/client/config/it/ConfigMapAndSecretIT.java
deleted file mode 100644
index 1137969afa..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/java/org/springframework/cloud/kubernetes/client/config/it/ConfigMapAndSecretIT.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2013-2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.cloud.kubernetes.client.config.it;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import io.kubernetes.client.openapi.ApiException;
-import io.kubernetes.client.openapi.apis.CoreV1Api;
-import io.kubernetes.client.openapi.models.V1ConfigMap;
-import io.kubernetes.client.openapi.models.V1Deployment;
-import io.kubernetes.client.openapi.models.V1EnvVar;
-import io.kubernetes.client.openapi.models.V1EnvVarBuilder;
-import io.kubernetes.client.openapi.models.V1Ingress;
-import io.kubernetes.client.openapi.models.V1Secret;
-import io.kubernetes.client.openapi.models.V1Service;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.testcontainers.k3s.K3sContainer;
-import reactor.netty.http.client.HttpClient;
-import reactor.util.retry.Retry;
-import reactor.util.retry.RetryBackoffSpec;
-
-import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
-import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
-import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import static org.awaitility.Awaitility.await;
-
-/**
- * @author Ryan Baxter
- */
-class ConfigMapAndSecretIT {
-
-	private static final String PROPERTY_URL = "http://localhost:80/myProperty";
-
-	private static final String SECRET_URL = "http://localhost:80/mySecret";
-
-	private static final String K8S_CONFIG_CLIENT_IT_SERVICE_NAME = "spring-cloud-kubernetes-client-config-it";
-
-	private static final String NAMESPACE = "default";
-
-	private static final String APP_NAME = "spring-cloud-kubernetes-client-config-it";
-
-	private static final K3sContainer K3S = Commons.container();
-
-	private static Util util;
-
-	private static CoreV1Api coreV1Api;
-
-	@BeforeAll
-	static void setup() throws Exception {
-		K3S.start();
-		Commons.validateImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
-		Commons.loadSpringCloudKubernetesImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
-		util = new Util(K3S);
-		coreV1Api = new CoreV1Api();
-		util.setUp(NAMESPACE);
-	}
-
-	@AfterAll
-	static void afterAll() throws Exception {
-		Commons.cleanUp(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
-		Commons.systemPrune();
-	}
-
-	@AfterEach
-	void after() {
-		configK8sClientIt(false, Phase.DELETE);
-	}
-
-	@Test
-	void testConfigMapAndSecretWatchRefresh() {
-		configK8sClientIt(false, Phase.CREATE);
-		testConfigMapAndSecretRefresh();
-	}
-
-	@Test
-	void testConfigMapAndSecretPollingRefresh() {
-		configK8sClientIt(true, Phase.CREATE);
-		testConfigMapAndSecretRefresh();
-	}
-
-	/**
-	 * 
-	 *     - read configmap/secrets the way we initially build them and assert their values
-	 *     - replace the above and assert we get the new values.
-	 * 
- */ - void testConfigMapAndSecretRefresh() { - - WebClient.Builder builder = builder(); - WebClient propertyClient = builder.baseUrl(PROPERTY_URL).build(); - - await().timeout(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2)).until(() -> propertyClient - .method(HttpMethod.GET).retrieve().bodyToMono(String.class).block().equals("from-config-map")); - - WebClient secretClient = builder.baseUrl(SECRET_URL).build(); - String secret = secretClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - Assertions.assertEquals(secret, "p455w0rd"); - - V1ConfigMap configMap = (V1ConfigMap) util.yaml("spring-cloud-kubernetes-client-config-it-configmap.yaml"); - Map data = configMap.getData(); - data.replace("application.yaml", data.get("application.yaml").replace("from-config-map", "from-unit-test")); - configMap.data(data); - try { - coreV1Api.replaceNamespacedConfigMap(APP_NAME, NAMESPACE, configMap, null, null, null, null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - await().timeout(Duration.ofSeconds(60)).pollInterval(Duration.ofSeconds(2)).until(() -> propertyClient - .method(HttpMethod.GET).retrieve().bodyToMono(String.class).block().equals("from-unit-test")); - V1Secret v1Secret = (V1Secret) util.yaml("spring-cloud-kubernetes-client-config-it-secret.yaml"); - Map secretData = v1Secret.getData(); - secretData.replace("my.config.mySecret", "p455w1rd".getBytes()); - v1Secret.setData(secretData); - try { - coreV1Api.replaceNamespacedSecret(APP_NAME, NAMESPACE, v1Secret, null, null, null, null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - await().timeout(Duration.ofSeconds(60)).pollInterval(Duration.ofSeconds(2)).until(() -> secretClient - .method(HttpMethod.GET).retrieve().bodyToMono(String.class).block().equals("p455w1rd")); - } - - private static void configK8sClientIt(boolean pooling, Phase phase) { - V1Deployment deployment = (V1Deployment) util.yaml("spring-cloud-kubernetes-client-config-it-deployment.yaml"); - - if (pooling) { - V1EnvVar one = new V1EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_RELOAD_MODE").withValue("polling") - .build(); - List existing = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - existing.add(one); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - } - - V1Service service = (V1Service) util.yaml("spring-cloud-kubernetes-client-config-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("spring-cloud-kubernetes-client-config-it-ingress.yaml"); - - V1ConfigMap configMap = (V1ConfigMap) util.yaml("spring-cloud-kubernetes-client-config-it-configmap.yaml"); - V1Secret secret = (V1Secret) util.yaml("spring-cloud-kubernetes-client-config-it-secret.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - util.createAndWait(NAMESPACE, configMap, secret); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - util.deleteAndWait(NAMESPACE, configMap, secret); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-configmap.yaml deleted file mode 100644 index 6a4e128c9c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-configmap.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -data: - application.yaml: |- - my: - config: - myProperty: from-config-map -kind: ConfigMap -metadata: - name: spring-cloud-kubernetes-client-config-it diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-deployment.yaml deleted file mode 100644 index ebe120005d..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-config-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-config-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-config-it - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-config-it - image: docker.io/springcloud/spring-cloud-kubernetes-client-config-it - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - env: - - name: SPRING_PROFILES_ACTIVE - value: kubernetes - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-secret.yaml deleted file mode 100644 index 9b68d9734b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-secret.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -data: - my.config.mySecret: cDQ1NXcwcmQ= -kind: Secret -metadata: - name: spring-cloud-kubernetes-client-config-it -type: Opaque diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-service.yaml deleted file mode 100644 index 10aaed5090..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-config-it - name: spring-cloud-kubernetes-client-config-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-config-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/pom.xml deleted file mode 100644 index 5617c5617c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - jar - - spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app - - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.boot - spring-boot-starter-webflux - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/pom.xml deleted file mode 100644 index 8537ed5898..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/pom.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - spring-cloud-kubernetes-integration-tests - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-client-configmap-event-reload - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.cloud - spring-cloud-starter-bootstrap - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/DataChangesInConfigMapReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/DataChangesInConfigMapReloadIT.java deleted file mode 100644 index 3189e69b22..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/DataChangesInConfigMapReloadIT.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.configmap.event.reload; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -class DataChangesInConfigMapReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-configmap-event-reload"; - - private static final String NAMESPACE = "default"; - - private static final String LEFT_NAMESPACE = "left"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static CoreV1Api api; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - api = new CoreV1Api(); - - util.createNamespace(LEFT_NAMESPACE); - util.setUpClusterWide(NAMESPACE, Set.of(LEFT_NAMESPACE)); - } - - @AfterAll - static void afterAll() throws Exception { - util.deleteNamespace(LEFT_NAMESPACE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - configMap with no labels and data: left.value = left-initial exists in namespace left
-	 *     - we assert that we can read it correctly first, by invoking localhost/left
-	 *
-	 *     - then we change the configmap by adding a label, this in turn does not
-	 *       change the result of localhost/left, because the data has not changed.
-	 *
-	 *     - then we change data inside the config map, and we must see the updated value
-	 * 
- */ - @Test - void testSimple() { - manifests(Phase.CREATE); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); - - WebClient webClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the left-configmap - Assertions.assertEquals("left-initial", result); - - // then deploy a new version of left-configmap, but without changing its data, - // only add a label - V1ConfigMap configMap = new V1ConfigMapBuilder().withMetadata(new V1ObjectMetaBuilder() - .withLabels(Map.of("new-label", "abc")).withNamespace("left").withName("left-configmap").build()) - .withData(Map.of("left.value", "left-initial")).build(); - - replaceConfigMap(configMap, "left-configmap"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "left-initial".equals(innerResult); - }); - - String logs = logs(); - Assertions.assertTrue(logs.contains("ConfigMap left-configmap was updated in namespace left")); - Assertions.assertTrue(logs.contains("data in configmap has not changed, will not reload")); - - // change data - configMap = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace("left") - .withName("left-configmap").build()) - .withData(Map.of("left.value", "left-after-change")).build(); - - replaceConfigMap(configMap, "left-configmap"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "left-after-change".equals(innerResult); - }); - - manifests(Phase.DELETE); - } - - private static void manifests(Phase phase) { - - try { - - V1ConfigMap leftConfigMap = (V1ConfigMap) util.yaml("left-configmap.yaml"); - - V1Deployment deployment = (V1Deployment) util.yaml("one/deployment.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - - List envVars = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(List.of())); - - V1EnvVar secretsDisabledEnvVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED") - .value("FALSE"); - envVars.add(secretsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(LEFT_NAMESPACE, leftConfigMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - - if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(LEFT_NAMESPACE, leftConfigMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - catch (Exception e) { - throw new RuntimeException(e); - } - - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - - private static void replaceConfigMap(V1ConfigMap configMap, String name) { - try { - api.replaceNamespacedConfigMap(name, LEFT_NAMESPACE, configMap, null, null, null, null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/ingress.yaml deleted file mode 100644 index c571ad9ad6..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-client-configmap-ingress-event-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-client-configmap-event-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/service.yaml deleted file mode 100644 index 096e213069..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-configmap-event-reload - name: spring-cloud-kubernetes-client-configmap-event-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-configmap-event-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/three/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/three/deployment.yaml deleted file mode 100644 index f2f457750c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/three/deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-configmap-deployment-event-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-configmap-event-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-configmap-event-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-configmap-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-client-configmap-event-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 - env: - - name: SPRING_PROFILES_ACTIVE - value: three - - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD - value: DEBUG diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/two/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/two/deployment.yaml deleted file mode 100644 index caa0d04139..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/two/deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-configmap-deployment-event-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-configmap-event-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-configmap-event-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-configmap-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-client-configmap-event-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 - env: - - name: SPRING_PROFILES_ACTIVE - value: two - - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD - value: DEBUG diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/pom.xml deleted file mode 100644 index cbcb9b0a78..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/pom.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - 4.0.0 - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - - spring-cloud-kubernetes-client-configmap-polling-reload - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - - - - org.springframework.cloud - spring-cloud-starter - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java deleted file mode 100644 index 091179ee89..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.configmap.polling.reload; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class BootstrapEnabledPollingReloadConfigMapMountIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-configmap-polling-reload"; - - private static final String NAMESPACE = "default"; - - private static Util util; - - private static CoreV1Api coreV1Api; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - coreV1Api = new CoreV1Api(); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - we have bootstrap enabled, which means we will 'locate' property sources
-	 *       from config maps.
-	 *     - there are no explicit config maps to search for, but what we will also read,
-	 *     	 is 'spring.cloud.kubernetes.config.paths', which we have set to
-	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
-	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
-	 *
-	 *     - we then change the config map content, wait for k8s to pick it up and replace them
-	 *     - our polling will then detect that change, and trigger a reload.
-	 * 
- */ - @Test - void test() throws Exception { - String logs = logs(); - // (1) - Assertions.assertTrue(logs.contains("paths property sources : [/tmp/application.properties]")); - // (2) - Assertions.assertTrue(logs.contains("will add file-based property source : /tmp/application.properties")); - // (3) - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); - - // replace data in configmap and wait for k8s to pick it up - // our polling will detect that and restart the app - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); - coreV1Api.replaceNamespacedConfigMap("poll-reload-as-mount", NAMESPACE, configMap, null, null, null, null); - - await().timeout(Duration.ofSeconds(180)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("as-mount-changed")); - - } - - private static void manifests(Phase phase) { - - V1Deployment deployment = (V1Deployment) util.yaml("deployment-mount.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - - List existing = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(new ArrayList<>())); - - // bootstrap is enabled, which means that in 'application-with-bootstrap.yaml', - // config-data support is disabled. - V1EnvVar mountActiveProfile = new V1EnvVar().name("SPRING_PROFILES_ACTIVE").value("with-bootstrap"); - V1EnvVar disableBootstrap = new V1EnvVar().name("SPRING_CLOUD_BOOTSTRAP_ENABLED").value("TRUE"); - - V1EnvVar debugLevelReloadCommons = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD").value("DEBUG"); - V1EnvVar debugLevelConfig = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG").value("DEBUG"); - V1EnvVar debugLevelCommons = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS") - .value("DEBUG"); - - existing.add(mountActiveProfile); - existing.add(disableBootstrap); - existing.add(debugLevelReloadCommons); - existing.add(debugLevelCommons); - existing.add(debugLevelConfig); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/PollingReloadConfigMapMountIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/PollingReloadConfigMapMountIT.java deleted file mode 100644 index 34c9e4f468..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/PollingReloadConfigMapMountIT.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.configmap.polling.reload; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class PollingReloadConfigMapMountIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-configmap-polling-reload"; - - private static final String NAMESPACE = "default"; - - private static Util util; - - private static CoreV1Api coreV1Api; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - coreV1Api = new CoreV1Api(); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - we have "spring.config.import: kubernetes", which means we will 'locate' property sources
-	 *       from config maps.
-	 *     - the property above means that at the moment we will be searching for config maps that only
-	 *       match the application name, in this specific test there is no such config map.
-	 *     - what we will also read, is 'spring.cloud.kubernetes.config.paths', which we have set to
-	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
-	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
-	 *
-	 *     - we then change the config map content, wait for k8s to pick it up and replace them
-	 *     - our polling will then detect that change, and trigger a reload.
-	 * 
- */ - @Test - void test() throws Exception { - String logs = logs(); - // (1) - Assertions.assertTrue(logs.contains("paths property sources : [/tmp/application.properties]")); - // (2) - Assertions.assertTrue(logs.contains("will add file-based property source : /tmp/application.properties")); - // (3) - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); - - // replace data in configmap and wait for k8s to pick it up - // our polling will detect that and restart the app - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); - coreV1Api.replaceNamespacedConfigMap("poll-reload-as-mount", NAMESPACE, configMap, null, null, null, null); - - await().timeout(Duration.ofSeconds(180)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("as-mount-changed")); - - } - - private static void manifests(Phase phase) { - - V1Deployment deployment = (V1Deployment) util.yaml("deployment-mount.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - - List existing = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(new ArrayList<>())); - - // bootstrap is disabled, which means that in 'application-mount.yaml', - // config-data support is enabled. - V1EnvVar mountActiveProfile = new V1EnvVar().name("SPRING_PROFILES_ACTIVE").value("mount"); - V1EnvVar disableBootstrap = new V1EnvVar().name("SPRING_CLOUD_BOOTSTRAP_ENABLED").value("FALSE"); - - V1EnvVar debugLevelReloadCommons = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD").value("DEBUG"); - V1EnvVar debugLevelConfig = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG").value("DEBUG"); - V1EnvVar debugLevelCommons = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS") - .value("DEBUG"); - - existing.add(mountActiveProfile); - existing.add(disableBootstrap); - existing.add(debugLevelReloadCommons); - existing.add(debugLevelCommons); - existing.add(debugLevelConfig); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/deployment-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/deployment-mount.yaml deleted file mode 100644 index 9f3f472467..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/deployment-mount.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-configmap-deployment-polling-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-configmap-polling-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-configmap-polling-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-configmap-polling-reload - image: docker.io/springcloud/spring-cloud-kubernetes-client-configmap-polling-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - initialDelaySeconds: 10 - periodSeconds: 2 - failureThreshold: 3 - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - initialDelaySeconds: 10 - periodSeconds: 2 - failureThreshold: 3 - ports: - - containerPort: 8080 - - volumeMounts: - - name: config-map-volume - mountPath: /tmp - - volumes: - - name: config-map-volume - configMap: - name: poll-reload-as-mount diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/ingress.yaml deleted file mode 100644 index 64f5bf4c02..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-client-configmap-ingress-polling-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-client-configmap-polling-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/service.yaml deleted file mode 100644 index 19d5acda8d..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-configmap-polling-reload - name: spring-cloud-kubernetes-client-configmap-polling-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-configmap-polling-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml deleted file mode 100644 index 3e6ca6f617..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - spring-cloud-kubernetes-integration-tests - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-client-discovery-it - - - - org.springframework.cloud - spring-cloud-kubernetes-client-discovery - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml deleted file mode 100644 index 8aefe278b1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml +++ /dev/null @@ -1,110 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-client-loadbalancer-it - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-loadbalancer - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-webflux - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java deleted file mode 100644 index f9cdee80fe..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.loadbalancer.it; - -import java.util.List; -import java.util.Map; - -import reactor.netty.http.client.HttpClient; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Ryan Baxter - */ - -@SpringBootApplication -@RestController -public class KubernetesClientLoadBalancerApplicationIt { - - private static final String URL = "http://service-wiremock/__admin/mappings"; - - private final DiscoveryClient discoveryClient; - - public KubernetesClientLoadBalancerApplicationIt(DiscoveryClient discoveryClien) { - this.discoveryClient = discoveryClien; - } - - public static void main(String[] args) { - SpringApplication.run(KubernetesClientLoadBalancerApplicationIt.class, args); - } - - @Bean - @LoadBalanced - WebClient.Builder client() { - return WebClient.builder(); - } - - @GetMapping("/loadbalancer-it/service") - @SuppressWarnings("unchecked") - public Map greeting() { - return (Map) client().clientConnector(new ReactorClientHttpConnector(HttpClient.create())) - .baseUrl(URL).build().method(HttpMethod.GET).retrieve().bodyToMono(Map.class).block(); - } - - @GetMapping("/services") - public List services() { - return discoveryClient.getServices(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml deleted file mode 100644 index a2ee8e365d..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-loadbalancer-it - name: spring-cloud-kubernetes-client-loadbalancer-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-loadbalancer-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml deleted file mode 100644 index 3024a4fe82..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-loadbalancer-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-loadbalancer-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-loadbalancer-it - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-loadbalancer-it - env: - - name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE - value: SERVICE - image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/pom.xml deleted file mode 100644 index 14f1a50744..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/pom.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-client-reactive-discoveryclient-it - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-all - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/resources/application.yaml deleted file mode 100644 index 978e0cded1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/resources/application.yaml +++ /dev/null @@ -1,12 +0,0 @@ -spring: - webflux: - base-path: /reactive-discovery-it - -management: - endpoint: - health: - show-details: always - endpoints: - web: - exposure: - include: "*" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/ReactiveDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/ReactiveDiscoveryClientIT.java deleted file mode 100644 index 2c077b6d2e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/ReactiveDiscoveryClientIT.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.reactive.discovery.it; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; -import java.util.Objects; - -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Ryan Baxter - */ -class ReactiveDiscoveryClientIT { - - private static final String HEALTH_URL = "http://localhost:80/reactive-discovery-it/actuator/health"; - - private static final String SERVICES_URL = "http://localhost:80/reactive-discovery-it/services"; - - private static final String SPRING_CLOUD_K8S_REACTIVE_DISCOVERY_APP_NAME = "spring-cloud-kubernetes-client-reactive-discoveryclient-it"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(SPRING_CLOUD_K8S_REACTIVE_DISCOVERY_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_REACTIVE_DISCOVERY_APP_NAME, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(SPRING_CLOUD_K8S_REACTIVE_DISCOVERY_APP_NAME, K3S); - Commons.systemPrune(); - } - - @BeforeEach - void setup() { - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); - } - - @AfterEach - void after() { - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); - reactiveDiscoveryIt(Phase.DELETE); - } - - @Test - void testReactiveDiscoveryClient() { - reactiveDiscoveryIt(Phase.CREATE); - testLoadBalancer(); - testHealth(); - } - - @SuppressWarnings("unchecked") - private void testHealth() { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl(HEALTH_URL).build(); - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map health = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Map components = (Map) health.get("components"); - - Assertions.assertTrue(components.containsKey("reactiveDiscoveryClients")); - Map discoveryComposite = (Map) components.get("discoveryComposite"); - Assertions.assertEquals(discoveryComposite.get("status"), "UP"); - } - - private void testLoadBalancer() { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl(SERVICES_URL).build(); - String servicesResponse = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - - Assertions - .assertTrue(Arrays.stream(servicesResponse.split(",")).anyMatch("service-wiremock"::equalsIgnoreCase)); - } - - private void reactiveDiscoveryIt(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("spring-cloud-kubernetes-client-reactive-discoveryclient-it-deployment.yaml"); - V1Service service = (V1Service) util - .yaml("spring-cloud-kubernetes-client-reactive-discoveryclient-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util - .yaml("spring-cloud-kubernetes-client-reactive-discoveryclient-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-deployment.yaml deleted file mode 100644 index 4d1ce28ef1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-client-reactive-discoveryclient-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-client-reactive-discoveryclient-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-client-reactive-discoveryclient-it - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-client-reactive-discoveryclient-it - image: docker.io/springcloud/spring-cloud-kubernetes-client-reactive-discoveryclient-it - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /reactive-discovery-it/actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /reactive-discovery-it/actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-ingress.yaml deleted file mode 100644 index 6f361e0e41..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: it-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: /reactive-discovery-it - pathType: ImplementationSpecific - backend: - service: - name: spring-cloud-kubernetes-client-reactive-discoveryclient-it - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-service.yaml deleted file mode 100644 index 8d2e27d520..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-client-reactive-discoveryclient-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-reactive-discoveryclient-it - name: spring-cloud-kubernetes-client-reactive-discoveryclient-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-reactive-discoveryclient-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml deleted file mode 100644 index 3057b98ff7..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-client-secrets-event-reload - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java deleted file mode 100644 index 72056a2300..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.secrets.event.reload; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -/** - * @author wind57 - */ - -@SpringBootApplication -@EnableConfigurationProperties(SecretsProperties.class) -public class SecretsApp { - - public static void main(String[] args) { - SpringApplication.run(SecretsApp.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/DataChangesInSecretsReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/DataChangesInSecretsReloadIT.java deleted file mode 100644 index bec495be06..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/DataChangesInSecretsReloadIT.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.secrets.event.reload; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.openapi.models.V1SecretBuilder; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -class DataChangesInSecretsReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-secrets-event-reload"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static CoreV1Api api; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - api = new CoreV1Api(); - - util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE)); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - secret with no labels and data: from.properties.key = initial exists in namespace default
-	 *     - we assert that we can read it correctly first, by invoking localhost/key.
-	 *
-	 *     - then we change the secret by adding a label, this in turn does not
-	 *       change the result of localhost/key, because the data has not changed.
-	 *
-	 *     - then we change data inside the secret, and we must see the updated value.
-	 * 
- */ - @Test - void testSimple() { - manifests(Phase.CREATE); - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); - - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the secret - Assertions.assertEquals("initial", result); - - // then deploy a new version of left-configmap, but without changing its data, - // only add a label - V1Secret secret = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace(NAMESPACE) - .withName("event-reload").build()) - .withData(Map.of("application.properties", "from.properties.key=initial".getBytes())).build(); - - replaceSecret(secret, "event-reload"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "initial".equals(innerResult); - }); - - String logs = logs(); - Assertions.assertTrue(logs.contains("Secret event-reload was updated in namespace default")); - Assertions.assertTrue(logs.contains("data in secret has not changed, will not reload")); - - // change data - secret = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace(NAMESPACE) - .withName("event-reload").build()) - .withData(Map.of("application.properties", "from.properties.key=change-initial".getBytes())).build(); - - replaceSecret(secret, "event-reload"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "change-initial".equals(innerResult); - }); - - manifests(Phase.DELETE); - } - - private static void manifests(Phase phase) { - - try { - - V1Secret secret = (V1Secret) util.yaml("secret.yaml"); - V1Deployment deployment = (V1Deployment) util.yaml("deployment.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - - List envVars = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(List.of())); - - V1EnvVar configDisabledEnvVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED") - .value("FALSE"); - envVars.add(configDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, secret); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - - if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, null, secret); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - catch (Exception e) { - throw new RuntimeException(e); - } - - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - - private static void replaceSecret(V1Secret secret, String name) { - try { - api.replaceNamespacedSecret(name, NAMESPACE, secret, null, null, null, null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml deleted file mode 100644 index 7c74d61817..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-client-secrets-ingress-event-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-client-secrets-event-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml deleted file mode 100644 index 9e2848765f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml deleted file mode 100644 index e1fb85db7b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-client-secrets-event-reload - name: spring-cloud-kubernetes-client-secrets-event-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-client-secrets-event-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.java deleted file mode 100644 index a3fb642fe8..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.configuration.watcher; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -@RestController -public class ConfigWatcherTestApplication implements ApplicationListener { - - protected Log log = LogFactory.getLog(getClass()); - - private boolean value = false; - - public static void main(String[] args) { - SpringApplication.run(ConfigWatcherTestApplication.class, args); - } - - @GetMapping("/it") - public boolean index() { - log.info("Current value: " + value); - return value; - } - - @Override - public void onApplicationEvent(RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) { - log.info("Received remote refresh event"); - this.value = true; - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml deleted file mode 100644 index bc8184729f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml +++ /dev/null @@ -1,35 +0,0 @@ -spring: - application: - name: spring-cloud-kubernetes-configuration-watcher-it - cloud: - bus: - refresh: - enabled: false #disable this because we are going to provide our own refresh listener for testing purposes - enabled: false - autoconfigure: - exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration -management: - endpoint: - health: - probes: - enabled: true ---- -spring: - cloud: - bus: - enabled: true - stream: - default-binder: rabbit - config: - activate: - on-profile: bus-amqp ---- -spring: - cloud: - bus: - enabled: true - stream: - default-binder: kafka - config: - activate: - on-profile: bus-kafka diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshKafkaIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshKafkaIT.java deleted file mode 100644 index 532488bfc3..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshKafkaIT.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.configuration.watcher; - -import java.time.Duration; -import java.util.Objects; - -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author Kris Iyer - */ -class ActuatorRefreshKafkaIT { - - private static final String CONFIG_WATCHER_IT_IMAGE = "spring-cloud-kubernetes-configuration-watcher-it"; - - private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - - Commons.validateImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - - Commons.validateImage(CONFIG_WATCHER_IT_IMAGE, K3S); - Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_IT_IMAGE, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - Commons.cleanUp(CONFIG_WATCHER_IT_IMAGE, K3S); - Commons.systemPrune(); - } - - @BeforeEach - void setup() { - util.zookeeper(NAMESPACE, Phase.CREATE); - util.kafka(NAMESPACE, Phase.CREATE); - testApp(Phase.CREATE); - configWatcher(Phase.CREATE); - } - - @AfterEach - void after() { - util.zookeeper(NAMESPACE, Phase.DELETE); - util.kafka(NAMESPACE, Phase.DELETE); - testApp(Phase.DELETE); - configWatcher(Phase.DELETE); - } - - @Test - void testRefresh() { - // Create new configmap to trigger controller to signal app to refresh - V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName(CONFIG_WATCHER_IT_IMAGE) - .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "hello world") - .build(); - util.createAndWait(NAMESPACE, configMap, null); - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/it").build(); - - Boolean[] value = new Boolean[1]; - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(240)).until(() -> { - value[0] = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class).retryWhen(retrySpec()) - .block(); - return value[0]; - }); - - Assertions.assertTrue(value[0]); - - util.deleteAndWait(NAMESPACE, configMap, null); - } - - private void testApp(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("app/spring-cloud-kubernetes-configuration-watcher-it-bus-kafka-deployment.yaml"); - V1Service service = (V1Service) util.yaml("app/spring-cloud-kubernetes-configuration-watcher-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("app/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private void configWatcher(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml"); - V1Service service = (V1Service) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml"); - - V1ConfigMap configMap = (V1ConfigMap) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, null, true); - util.createAndWait(NAMESPACE, configMap, null); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, null); - util.deleteAndWait(NAMESPACE, configMap, null); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(240, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java deleted file mode 100644 index ab76fb3ee2..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.configuration.watcher; - -import java.time.Duration; -import java.util.Objects; - -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author Ryan Baxter - */ -class ActuatorRefreshRabbitMQIT { - - private static final String CONFIG_WATCHER_IT_IMAGE = "spring-cloud-kubernetes-configuration-watcher-it"; - - private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - - Commons.validateImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - - Commons.validateImage(CONFIG_WATCHER_IT_IMAGE, K3S); - Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_IT_IMAGE, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); - Commons.cleanUp(CONFIG_WATCHER_IT_IMAGE, K3S); - Commons.systemPrune(); - } - - @BeforeEach - void setup() { - util.rabbitMq(NAMESPACE, Phase.CREATE); - app(Phase.CREATE); - configWatcher(Phase.CREATE); - } - - @AfterEach - void afterEach() { - util.rabbitMq(NAMESPACE, Phase.DELETE); - app(Phase.DELETE); - configWatcher(Phase.DELETE); - } - - @Test - void testRefresh() { - // Create new configmap to trigger controller to signal app to refresh - V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName(CONFIG_WATCHER_IT_IMAGE) - .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "hello world") - .build(); - util.createAndWait(NAMESPACE, configMap, null); - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/it").build(); - - Boolean[] value = new Boolean[1]; - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(180)).until(() -> { - value[0] = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class).retryWhen(retrySpec()) - .block(); - return value[0]; - }); - - Assertions.assertTrue(value[0]); - util.deleteAndWait(NAMESPACE, configMap, null); - } - - private void app(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("app-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml"); - V1Service service = (V1Service) util.yaml("app/spring-cloud-kubernetes-configuration-watcher-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("app/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private void configWatcher(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-amqp-deployment.yaml"); - V1Service service = (V1Service) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml"); - V1ConfigMap configMap = (V1ConfigMap) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, null, true); - util.createAndWait(NAMESPACE, configMap, null); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, null); - util.deleteAndWait(NAMESPACE, configMap, null); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-amqp-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-amqp-deployment.yaml deleted file mode 100644 index c08365cfd7..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-amqp-deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-configuration-watcher-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configuration-watcher - template: - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-configuration-watcher - image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher - imagePullPolicy: IfNotPresent - env: - - name: SPRING_PROFILES_ACTIVE - value: bus-amqp - - name: SPRING_RABBITMQ_HOST - value: rabbitmq-service - readinessProbe: - httpGet: - port: 8888 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8888 - path: /actuator/health/liveness - ports: - - containerPort: 8888 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml deleted file mode 100644 index b345c9f8cb..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-configuration-watcher-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configuration-watcher - template: - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-configuration-watcher - image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher - imagePullPolicy: IfNotPresent - env: - - name: SPRING_PROFILES_ACTIVE - value: bus-kafka - - name: spring.kafka.bootstrap-servers - value: kafka:9092 - readinessProbe: - httpGet: - port: 8888 - path: /actuator/health/readiness - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - livenessProbe: - httpGet: - port: 8888 - path: /actuator/health/liveness - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - ports: - - containerPort: 8888 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml deleted file mode 100644 index 9869d82ddd..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-configuration-watcher-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configuration-watcher-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher-it - spec: - containers: - - name: spring-cloud-kubernetes-configuration-watcher-it - image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher-it - imagePullPolicy: IfNotPresent - env: - - name: SPRING_PROFILES_ACTIVE - value: bus-amqp - - name: SPRING_RABBITMQ_HOST - value: rabbitmq-service - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-bus-kafka-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-bus-kafka-deployment.yaml deleted file mode 100644 index 167aa15e24..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-bus-kafka-deployment.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-configuration-watcher-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-configuration-watcher-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher-it - spec: - containers: - - name: spring-cloud-kubernetes-configuration-watcher-it - image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher-it - imagePullPolicy: IfNotPresent - env: - - name: SPRING_PROFILES_ACTIVE - value: bus-kafka - - name: spring.kafka.bootstrap-servers - value: kafka:9092 - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml deleted file mode 100644 index 222b748ab9..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: it-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: /it - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-configuration-watcher-it - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-service.yaml deleted file mode 100644 index 7b8a1c2141..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/app/spring-cloud-kubernetes-configuration-watcher-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-configuration-watcher-it - name: spring-cloud-kubernetes-configuration-watcher-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-configuration-watcher-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/logback-test.xml deleted file mode 100644 index 9e2848765f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/pom.xml deleted file mode 100644 index ac436d8dbd..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-core-k8s-client-it - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/java/org/springframework/cloud/kubernetes/core/k8s/it/CoreK8SApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/java/org/springframework/cloud/kubernetes/core/k8s/it/CoreK8SApplicationIt.java deleted file mode 100644 index 510fae0a5b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/java/org/springframework/cloud/kubernetes/core/k8s/it/CoreK8SApplicationIt.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.core.k8s.it; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author Ryan Baxter - */ -@SpringBootApplication -public class CoreK8SApplicationIt { - - public static void main(String[] args) { - SpringApplication.run(CoreK8SApplicationIt.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/resources/application.yaml deleted file mode 100644 index cd4028226c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/main/resources/application.yaml +++ /dev/null @@ -1,8 +0,0 @@ -management: - endpoint: - health: - show-details: always - endpoints: - web: - exposure: - include: "*" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/java/org/springframework/cloud/kubernetes/core/k8s/it/ActuatorEndpointIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/java/org/springframework/cloud/kubernetes/core/k8s/it/ActuatorEndpointIT.java deleted file mode 100644 index 5db84fbdd4..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/java/org/springframework/cloud/kubernetes/core/k8s/it/ActuatorEndpointIT.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.core.k8s.it; - -import java.time.Duration; -import java.util.Map; -import java.util.Objects; - -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Ryan Baxter - */ -class ActuatorEndpointIT { - - private static final String K8S_CONFIG_CLIENT_IT_SERVICE_NAME = "spring-cloud-kubernetes-core-k8s-client-it"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - coreK8sClientIt(Phase.CREATE); - } - - @AfterAll - static void afterAll() { - coreK8sClientIt(Phase.DELETE); - Commons.systemPrune(); - } - - @Test - @SuppressWarnings("unchecked") - void testHealth() { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/actuator/health").build(); - - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map health = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Map components = (Map) health.get("components"); - assertThat(components.containsKey("kubernetes")).isTrue(); - Map kubernetes = (Map) components.get("kubernetes"); - assertThat(kubernetes.get("status")).isEqualTo("UP"); - Map details = (Map) kubernetes.get("details"); - assertThat(details.containsKey("hostIp")).isTrue(); - assertThat(details.containsKey("inside")).isTrue(); - assertThat(details.containsKey("labels")).isTrue(); - assertThat(details.containsKey("namespace")).isTrue(); - assertThat(details.containsKey("nodeName")).isTrue(); - assertThat(details.containsKey("podIp")).isTrue(); - assertThat(details.containsKey("podName")).isTrue(); - assertThat(details.containsKey("serviceAccount")).isTrue(); - - assertThat(components.containsKey("discoveryComposite")).isTrue(); - Map discoveryComposite = (Map) components.get("discoveryComposite"); - assertThat(discoveryComposite.get("status")).isEqualTo("UP"); - - } - - @Test - @SuppressWarnings("unchecked") - void testInfo() { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/actuator/info").build(); - - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map info = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Map kubernetes = (Map) info.get("kubernetes"); - assertThat(kubernetes.containsKey("hostIp")).isTrue(); - assertThat(kubernetes.containsKey("inside")).isTrue(); - assertThat(kubernetes.containsKey("namespace")).isTrue(); - assertThat(kubernetes.containsKey("nodeName")).isTrue(); - assertThat(kubernetes.containsKey("podIp")).isTrue(); - assertThat(kubernetes.containsKey("podName")).isTrue(); - assertThat(kubernetes.containsKey("serviceAccount")).isTrue(); - } - - private static void coreK8sClientIt(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("spring-cloud-kubernetes-core-k8s-client-it-deployment.yaml"); - V1Service service = (V1Service) util.yaml("spring-cloud-kubernetes-core-k8s-client-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("spring-cloud-kubernetes-core-k8s-client-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/logback-test.xml deleted file mode 100644 index 9e2848765f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-deployment.yaml deleted file mode 100644 index 102d98f1cb..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-core-k8s-client-it-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-core-k8s-client-it - template: - metadata: - labels: - app: spring-cloud-kubernetes-core-k8s-client-it - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-core-k8s-client-it - image: docker.io/springcloud/spring-cloud-kubernetes-core-k8s-client-it - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-service.yaml deleted file mode 100644 index c97d94c61c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-core-k8s-client-it - name: spring-cloud-kubernetes-core-k8s-client-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-core-k8s-client-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/pom.xml deleted file mode 100644 index 1b1b96ba42..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/pom.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - spring-cloud-kubernetes-integration-tests - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-discoveryclient-it - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.cloud - spring-cloud-starter-kubernetes-discoveryclient - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-webflux - test - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceIT.java deleted file mode 100644 index 8ff34d9fb1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceIT.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.discoveryclient.it; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api; -import io.kubernetes.client.openapi.models.V1ClusterRoleBinding; -import io.kubernetes.client.openapi.models.V1Container; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; -import io.kubernetes.client.openapi.models.V1EnvVarBuilder; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance; -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author mbialkowski1 - */ -class DiscoveryClientFilterNamespaceIT { - - private static final String DISCOVERY_SERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver"; - - private static final String SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME = "spring-cloud-kubernetes-discoveryclient-it"; - - private static final String NAMESPACE = "default"; - - private static final String NAMESPACE_LEFT = "left"; - - private static final String NAMESPACE_RIGHT = "right"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static RbacAuthorizationV1Api rbacApi; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - - Commons.validateImage(DISCOVERY_SERVER_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(DISCOVERY_SERVER_APP_NAME, K3S); - - Commons.validateImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - - util = new Util(K3S); - rbacApi = new RbacAuthorizationV1Api(); - util.createNamespace(NAMESPACE_LEFT); - util.createNamespace(NAMESPACE_RIGHT); - util.setUp(NAMESPACE); - - V1ClusterRoleBinding clusterRole = (V1ClusterRoleBinding) util - .yaml("namespace-filter/cluster-admin-serviceaccount-role.yaml"); - rbacApi.createClusterRoleBinding(clusterRole, null, null, null, null); - discoveryServer(Phase.CREATE); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(DISCOVERY_SERVER_APP_NAME, K3S); - Commons.cleanUp(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - discoveryServer(Phase.DELETE); - util.deleteNamespace(NAMESPACE_LEFT); - util.deleteNamespace(NAMESPACE_RIGHT); - Commons.systemPrune(); - } - - @AfterEach - void afterEach() { - util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.DELETE); - util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.DELETE); - discoveryIt(Phase.DELETE); - } - - @Test - void testDiscoveryClient() { - util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.CREATE); - util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.CREATE); - discoveryIt(Phase.CREATE); - - testLoadBalancer(); - testHealth(); - } - - private void testLoadBalancer() { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build(); - - String[] result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String[].class) - .retryWhen(retrySpec()).block(); - assertThat(result).containsAnyOf("service-wiremock"); - - // ServiceInstance - WebClient serviceInstanceClient = builder - .baseUrl("http://localhost:80/discoveryclient-it/service/service-wiremock").build(); - List serviceInstances = serviceInstanceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }).retryWhen(retrySpec()).block(); - - assertThat(serviceInstances).isNotNull(); - assertThat(serviceInstances.size()).isEqualTo(1); - assertThat(serviceInstances.get(0).getNamespace()).isEqualTo(NAMESPACE_LEFT); - - } - - @SuppressWarnings("unchecked") - void testHealth() { - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build(); - - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map health = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Map components = (Map) health.get("components"); - - Map discoveryComposite = (Map) components.get("discoveryComposite"); - assertThat(discoveryComposite.get("status")).isEqualTo("UP"); - } - - private void discoveryIt(Phase phase) { - - V1Deployment deployment = (V1Deployment) util - .yaml("client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml"); - V1Service service = (V1Service) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - // add namespaces filter property for left namespace - var env = new V1EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") - .withValue(NAMESPACE_LEFT).build(); - var container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); - container.setEnv(List.of(env)); - - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private static void discoveryServer(Phase phase) { - - V1Deployment deployment = (V1Deployment) util - .yaml("server/spring-cloud-kubernetes-discoveryserver-deployment.yaml"); - V1Service service = (V1Service) util.yaml("server/spring-cloud-kubernetes-discoveryserver-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("server/spring-cloud-kubernetes-discoveryserver-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - // add namespaces filter property for left namespace - // setup all-namespaces property - V1EnvVar env = new V1EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES") - .withValue("TRUE").build(); - V1Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); - container.setEnv(List.of(env)); - - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java deleted file mode 100644 index ac96344254..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.discoveryclient.it; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; -import java.util.Objects; - -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Ryan Baxter - */ -class DiscoveryClientIT { - - private static final String DISCOVERY_SERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver"; - - private static final String SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME = "spring-cloud-kubernetes-discoveryclient-it"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - - Commons.validateImage(DISCOVERY_SERVER_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(DISCOVERY_SERVER_APP_NAME, K3S); - - Commons.validateImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - discoveryServer(Phase.CREATE); - - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(DISCOVERY_SERVER_APP_NAME, K3S); - Commons.cleanUp(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); - - discoveryServer(Phase.DELETE); - discoveryIt(Phase.DELETE); - Commons.systemPrune(); - } - - @Test - void testDiscoveryClient() { - discoveryIt(Phase.CREATE); - testLoadBalancer(); - testHealth(); - } - - private void testLoadBalancer() { - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build(); - - String[] result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String[].class) - .retryWhen(retrySpec()).block(); - assertThat(Arrays.stream(result).anyMatch("spring-cloud-kubernetes-discoveryserver"::equalsIgnoreCase)) - .isTrue(); - - } - - @SuppressWarnings("unchecked") - void testHealth() { - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build(); - - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map health = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Map components = (Map) health.get("components"); - - Map discoveryComposite = (Map) components.get("discoveryComposite"); - assertThat(discoveryComposite.get("status")).isEqualTo("UP"); - } - - private static void discoveryIt(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml"); - V1Service service = (V1Service) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private static void discoveryServer(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("server/spring-cloud-kubernetes-discoveryserver-deployment.yaml"); - V1Service service = (V1Service) util.yaml("server/spring-cloud-kubernetes-discoveryserver-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("server/spring-cloud-kubernetes-discoveryserver-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml deleted file mode 100644 index dd39366a4c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: it-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: /discoveryclient-it - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-discoveryclient-it - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml deleted file mode 100644 index 32b4bd7e37..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-discoveryclient-it - name: spring-cloud-kubernetes-discoveryclient-it -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-discoveryclient-it - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/logback-test.xml deleted file mode 100644 index 9e2848765f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml index b9e7b293b0..cbbbb39769 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -42,64 +42,6 @@ true - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchIT.java index 8bd14f5410..92360824b9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchIT.java @@ -19,22 +19,17 @@ import java.io.InputStream; import java.time.Duration; import java.util.List; -import java.util.Objects; +import java.util.Set; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.utils.Serialization; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; @@ -43,46 +38,61 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForEndpointSlices; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForNamespaceFilterAndEndpointSlices; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForNamespaceFilterAndEndpoints; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.retrySpec; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; /** * @author wind57 */ class Fabric8CatalogWatchIT { - private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher"; - private static final String NAMESPACE = "default"; - private static final K3sContainer K3S = Commons.container(); + public static final String NAMESPACE_A = "namespacea"; + + public static final String NAMESPACE_B = "namespaceb"; + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher"; - private static KubernetesClient client; + private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + pomVersion(); + + private static final K3sContainer K3S = Commons.container(); private static Util util; @BeforeAll static void beforeAll() throws Exception { K3S.start(); + Commons.validateImage(IMAGE_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); + util = new Util(K3S); - client = util.client(); - Commons.validateImage(APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S); + util.createNamespace(NAMESPACE_A); + util.createNamespace(NAMESPACE_B); util.setUp(NAMESPACE); + util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); + util.busybox(NAMESPACE, Phase.CREATE); + + app(Phase.CREATE); } @AfterAll static void afterAll() { - Commons.systemPrune(); - } - @BeforeEach - void beforeEach() { - util.busybox(NAMESPACE, Phase.CREATE); + util.deleteNamespace(NAMESPACE_A); + util.deleteNamespace(NAMESPACE_B); + + app(Phase.DELETE); + Commons.systemPrune(); } /** @@ -95,18 +105,35 @@ void beforeEach() { */ @Test void testCatalogWatchWithEndpoints() throws Exception { - app(false, Phase.CREATE); - assertLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch"); + assertLogStatement(); test(); - app(false, Phase.DELETE); + + testCatalogWatchWithEndpointSlices(); + testCatalogWatchWithNamespaceFilterAndEndpoints(); + testCatalogWatchWithNamespaceFilterAndEndpointSlices(); } - @Test - void testCatalogWatchWithEndpointSlices() throws Exception { - app(true, Phase.CREATE); - assertLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch"); + void testCatalogWatchWithEndpointSlices() { + util.busybox(NAMESPACE, Phase.CREATE); + patchForEndpointSlices(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch", K3S, IMAGE_NAME); test(); - app(true, Phase.DELETE); + } + + void testCatalogWatchWithNamespaceFilterAndEndpoints() { + util.busybox(NAMESPACE_A, Phase.CREATE); + util.busybox(NAMESPACE_B, Phase.CREATE); + patchForNamespaceFilterAndEndpoints(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Fabric8CatalogWatchWithNamespacesDelegate.testCatalogWatchWithNamespaceFilterAndEndpoints(K3S, IMAGE_NAME, + util); + } + + void testCatalogWatchWithNamespaceFilterAndEndpointSlices() { + util.busybox(NAMESPACE_A, Phase.CREATE); + util.busybox(NAMESPACE_B, Phase.CREATE); + patchForNamespaceFilterAndEndpointSlices(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Fabric8CatalogWatchWithNamespacesDelegate.testCatalogWatchWithNamespaceFilterAndEndpointSlices(K3S, IMAGE_NAME, + util); } /** @@ -114,13 +141,13 @@ void testCatalogWatchWithEndpointSlices() throws Exception { * EndpointSlices. Here we make sure that in the test we actually use the correct * type. */ - private void assertLogStatement(String log) throws Exception { + private void assertLogStatement() throws Exception { String appPodName = K3S .execInContainer("kubectl", "get", "pods", "-l", "app=spring-cloud-kubernetes-fabric8-client-catalog-watcher", "-o=name", "--no-headers") .getStdout(); String allLogs = K3S.execInContainer("kubectl", "logs", appPodName.trim()).getStdout(); - Assertions.assertTrue(allLogs.contains(log)); + Assertions.assertTrue(allLogs.contains("stateGenerator is of type: Fabric8EndpointsCatalogWatch")); } /** @@ -143,6 +170,9 @@ private void test() { // watcher implementation // we will get the first busybox instances here. if (result != null) { + if (result.size() != 3) { + return false; + } holder[0] = result.get(0); holder[1] = result.get(1); return true; @@ -190,21 +220,18 @@ private void test() { return false; }); - Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME)); + Assertions.assertTrue(afterDelete[0].endpointName().contains(IMAGE_NAME)); Assertions.assertEquals("default", afterDelete[0].namespace()); } - private static void app(boolean useEndpointSlices, Phase phase) { + private static void app(Phase phase) { InputStream endpointsDeploymentStream = util.inputStream("app/watcher-endpoints-deployment.yaml"); - InputStream endpointSlicesDeploymentStream = util.inputStream("app/watcher-endpoint-slices-deployment.yaml"); InputStream serviceStream = util.inputStream("app/watcher-service.yaml"); InputStream ingressStream = util.inputStream("app/watcher-ingress.yaml"); - Deployment deployment = useEndpointSlices - ? Serialization.unmarshal(endpointSlicesDeploymentStream, Deployment.class) - : Serialization.unmarshal(endpointsDeploymentStream, Deployment.class); + Deployment deployment = Serialization.unmarshal(endpointsDeploymentStream, Deployment.class); Service service = Serialization.unmarshal(serviceStream, Service.class); Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); @@ -217,12 +244,4 @@ private static void app(boolean useEndpointSlices, Phase phase) { } - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchUtil.java new file mode 100644 index 0000000000..16b9f818e2 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchUtil.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.catalog.watch; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +final class Fabric8CatalogWatchUtil { + + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-fabric8-client-catalog-watcher"); + + private Fabric8CatalogWatchUtil() { + + } + + static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + static final String BODY_TWO = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES", + "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", + "value": "namespacea" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1", + "value": "default" + } + ] + }] + } + } + } + } + """; + + static final String BODY_THREE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES", + "value": "TRUE" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", + "value": "namespacea" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1", + "value": "default" + } + ] + }] + } + } + } + } + """; + + static void patchForEndpointSlices(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + + static void patchForNamespaceFilterAndEndpoints(Util util, String dockerImage, String deploymentName, + String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_TWO, POD_LABELS); + } + + static void patchForNamespaceFilterAndEndpointSlices(Util util, String dockerImage, String deploymentName, + String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_THREE, POD_LABELS); + } + + static WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + static RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesDelegate.java new file mode 100644 index 0000000000..ad1c7543ac --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesDelegate.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.catalog.watch; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchIT.NAMESPACE_A; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchIT.NAMESPACE_B; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.retrySpec; + +/** + * @author wind57 + */ +final class Fabric8CatalogWatchWithNamespacesDelegate { + + private Fabric8CatalogWatchWithNamespacesDelegate() { + + } + + private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher"; + + /** + *
+	 *     - we deploy one busybox service with 2 replica pods in namespace namespacea
+	 *     - we deploy one busybox service with 2 replica pods in namespace namespaceb
+	 *     - we enable the search to be made in namespacea and default ones
+	 *     - we receive an event from KubernetesCatalogWatcher, assert what is inside it
+	 *     - delete both busybox services in namespacea and namespaceb
+	 *     - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
+	 * 
+ */ + static void testCatalogWatchWithNamespaceFilterAndEndpoints(K3sContainer container, String imageName, Util util) { + Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch", container, imageName); + test(util); + } + + static void testCatalogWatchWithNamespaceFilterAndEndpointSlices(K3sContainer container, String imageName, + Util util) { + Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch", container, + imageName); + test(util); + } + + /** + * the test is the same for both endpoints and endpoint slices, the set-up for them is + * different. + */ + @SuppressWarnings("unchecked") + private static void test(Util util) { + + WebClient client = builder().baseUrl("http://localhost/result").build(); + EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2]; + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class); + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { + List result = (List) client.method(HttpMethod.GET) + .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) + .retryWhen(retrySpec()).block(); + + // we get 3 pods as input, but because they are sorted by name in the catalog + // watcher implementation + // we will get the first busybox instances here. + if (result != null) { + holder[0] = result.get(0); + holder[1] = result.get(1); + return true; + } + + return false; + }); + + EndpointNameAndNamespace resultOne = holder[0]; + EndpointNameAndNamespace resultTwo = holder[1]; + + Assertions.assertNotNull(resultOne); + Assertions.assertNotNull(resultTwo); + + Assertions.assertTrue(resultOne.endpointName().contains("busybox")); + Assertions.assertTrue(resultTwo.endpointName().contains("busybox")); + Assertions.assertEquals(NAMESPACE_A, resultOne.namespace()); + Assertions.assertEquals(NAMESPACE_A, resultTwo.namespace()); + + util.busybox(NAMESPACE_A, Phase.DELETE); + util.busybox(NAMESPACE_B, Phase.DELETE); + + // what we get after delete + EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1]; + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { + List result = (List) client.method(HttpMethod.GET) + .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) + .retryWhen(retrySpec()).block(); + + // we need to get the event from KubernetesCatalogWatch, but that happens + // on periodic bases. So in order to be sure we got the event we care about + // we wait until the result has a single entry, which means busybox was + // deleted + // + KubernetesCatalogWatch received the new update. + if (result != null && result.size() != 1) { + return false; + } + + // we will only receive one pod here, our own + if (result != null) { + afterDelete[0] = result.get(0); + return true; + } + + return false; + }); + + Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME)); + Assertions.assertEquals("default", afterDelete[0].namespace()); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesIT.java deleted file mode 100644 index bf998331fd..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Fabric8CatalogWatchWithNamespacesIT.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.catalog.watch; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class Fabric8CatalogWatchWithNamespacesIT { - - private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher"; - - private static final String NAMESPACE_A = "namespacea"; - - private static final String NAMESPACE_B = "namespaceb"; - - private static final String NAMESPACE_DEFAULT = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static KubernetesClient client; - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - util = new Util(K3S); - client = util.client(); - - Commons.validateImage(APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S); - - util.createNamespace(NAMESPACE_A); - util.createNamespace(NAMESPACE_B); - - util.setUpClusterWide(NAMESPACE_DEFAULT, Set.of(NAMESPACE_DEFAULT, NAMESPACE_A, NAMESPACE_B)); - } - - @BeforeEach - void beforeEach() { - util.busybox(NAMESPACE_A, Phase.CREATE); - util.busybox(NAMESPACE_B, Phase.CREATE); - } - - @AfterAll - static void afterAll() { - util.deleteNamespace(NAMESPACE_A); - util.deleteNamespace(NAMESPACE_B); - Commons.systemPrune(); - } - - /** - *
-	 *     - we deploy one busybox service with 2 replica pods in namespace namespacea
-	 *     - we deploy one busybox service with 2 replica pods in namespace namespaceb
-	 *     - we enable the search to be made in namespacea and default ones
-	 *     - we receive an event from KubernetesCatalogWatcher, assert what is inside it
-	 *     - delete both busybox services in namespacea and namespaceb
-	 *     - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
-	 * 
- */ - @Test - void testCatalogWatchWithEndpoints() throws Exception { - app(false, Phase.CREATE); - assertLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch"); - test(); - app(false, Phase.DELETE); - } - - @Test - void testCatalogWatchWithEndpointSlices() throws Exception { - app(true, Phase.CREATE); - assertLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch"); - test(); - app(true, Phase.DELETE); - } - - /** - * we log in debug mode the type of the StateGenerator we use, be that Endpoints or - * EndpointSlices. Here we make sure that in the test we actually use the correct - * type. - */ - private void assertLogStatement(String log) throws Exception { - String appPodName = K3S - .execInContainer("kubectl", "get", "pods", "-l", - "app=spring-cloud-kubernetes-fabric8-client-catalog-watcher", "-o=name", "--no-headers") - .getStdout(); - String allLogs = K3S.execInContainer("kubectl", "logs", appPodName.trim()).getStdout(); - Assertions.assertTrue(allLogs.contains(log)); - } - - /** - * the test is the same for both endpoints and endpoint slices, the set-up for them is - * different. - */ - @SuppressWarnings("unchecked") - private void test() { - - WebClient client = builder().baseUrl("http://localhost/result").build(); - EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2]; - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class); - - await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { - List result = (List) client.method(HttpMethod.GET) - .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) - .retryWhen(retrySpec()).block(); - - // we get 3 pods as input, but because they are sorted by name in the catalog - // watcher implementation - // we will get the first busybox instances here. - if (result != null) { - holder[0] = result.get(0); - holder[1] = result.get(1); - return true; - } - - return false; - }); - - EndpointNameAndNamespace resultOne = holder[0]; - EndpointNameAndNamespace resultTwo = holder[1]; - - Assertions.assertNotNull(resultOne); - Assertions.assertNotNull(resultTwo); - - Assertions.assertTrue(resultOne.endpointName().contains("busybox")); - Assertions.assertTrue(resultTwo.endpointName().contains("busybox")); - Assertions.assertEquals(NAMESPACE_A, resultOne.namespace()); - Assertions.assertEquals(NAMESPACE_A, resultTwo.namespace()); - - util.busybox(NAMESPACE_A, Phase.DELETE); - util.busybox(NAMESPACE_B, Phase.DELETE); - - // what we get after delete - EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1]; - - await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { - List result = (List) client.method(HttpMethod.GET) - .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) - .retryWhen(retrySpec()).block(); - - // we need to get the event from KubernetesCatalogWatch, but that happens - // on periodic bases. So in order to be sure we got the event we care about - // we wait until the result has a single entry, which means busybox was - // deleted - // + KubernetesCatalogWatch received the new update. - if (result != null && result.size() != 1) { - return false; - } - - // we will only receive one pod here, our own - if (result != null) { - afterDelete[0] = result.get(0); - return true; - } - - return false; - }); - - Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME)); - Assertions.assertEquals("default", afterDelete[0].namespace()); - - } - - private static void app(boolean useEndpointSlices, Phase phase) { - - InputStream endpointsDeploymentStream = util.inputStream("app/watcher-endpoints-deployment.yaml"); - InputStream endpointSlicesDeploymentStream = util.inputStream("app/watcher-endpoint-slices-deployment.yaml"); - InputStream serviceStream = util.inputStream("app/watcher-service.yaml"); - InputStream ingressStream = util.inputStream("app/watcher-ingress.yaml"); - - Deployment deployment = useEndpointSlices - ? Serialization.unmarshal(endpointSlicesDeploymentStream, Deployment.class) - : Serialization.unmarshal(endpointsDeploymentStream, Deployment.class); - - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar namespaceAEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") - .withValue(NAMESPACE_A).build(); - EnvVar namespaceDefaultEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1") - .withValue(NAMESPACE_DEFAULT).build(); - envVars.add(namespaceAEnvVar); - envVars.add(namespaceDefaultEnvVar); - - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(Fabric8CatalogWatchWithNamespacesIT.NAMESPACE_DEFAULT, null, deployment, service, - ingress, true); - } - else { - util.deleteAndWait(Fabric8CatalogWatchWithNamespacesIT.NAMESPACE_DEFAULT, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoint-slices-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoint-slices-deployment.yaml deleted file mode 100644 index 531a1447d8..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoint-slices-deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-catalog-watcher -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-catalog-watcher - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-catalog-watcher - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-catalog-watcher - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-catalog-watcher - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 - env: - - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY - value: DEBUG - - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES - value: true diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoints-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoints-deployment.yaml index d20024014d..fdce297c87 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoints-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-endpoints-deployment.yaml @@ -28,6 +28,6 @@ spec: - containerPort: 8080 env: - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY - value: DEBUG + value: "DEBUG" - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES - value: false + value: "FALSE" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/pom.xml deleted file mode 100644 index 68086dd4ae..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/pom.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - spring-cloud-kubernetes-integration-tests - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-configmap-event-reload - - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-starter-bootstrap - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/DataChangesInConfigMapReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/DataChangesInConfigMapReloadIT.java deleted file mode 100644 index 032e32d0b7..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/DataChangesInConfigMapReloadIT.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -class DataChangesInConfigMapReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-event-reload"; - - private static final String NAMESPACE = "default"; - - private static final String LEFT_NAMESPACE = "left"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static KubernetesClient client; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - - util.createNamespace(LEFT_NAMESPACE); - util.setUpClusterWide(NAMESPACE, Set.of(LEFT_NAMESPACE)); - } - - @AfterAll - static void afterAll() throws Exception { - util.deleteNamespace(LEFT_NAMESPACE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - configMap with no labels and data: left.value = left-initial exists in namespace left
-	 *     - we assert that we can read it correctly first, by invoking localhost/left
-	 *
-	 *     - then we change the configmap by adding a label, this in turn does not
-	 *       change the result of localhost/left, because the data has not changed.
-	 *
-	 *     - then we change data inside the config map, and we must see the updated value
-	 * 
- */ - @Test - void testSimple() { - manifests(Phase.CREATE); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); - - WebClient webClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the left-configmap - Assertions.assertEquals("left-initial", result); - - // then deploy a new version of left-configmap, but without changing its data, - // only add a label - ConfigMap configMap = new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder() - .withLabels(Map.of("new-label", "abc")).withNamespace("left").withName("left-configmap").build()) - .withData(Map.of("left.value", "left-initial")).build(); - - replaceConfigMap(configMap); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "left-initial".equals(innerResult); - }); - - String logs = logs(); - Assertions.assertTrue(logs.contains("ConfigMap left-configmap was updated in namespace left")); - Assertions.assertTrue(logs.contains("data in configmap has not changed, will not reload")); - - // change data - configMap = new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace("left") - .withName("left-configmap").build()) - .withData(Map.of("left.value", "left-after-change")).build(); - - replaceConfigMap(configMap); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "left-after-change".equals(innerResult); - }); - - manifests(Phase.DELETE); - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("deployment.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream configmapAsStream = util.inputStream("left-configmap.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar activeProfileProperty = new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE").withValue("one").build(); - envVars.add(activeProfileProperty); - - EnvVar secretsDisabledEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED") - .withValue("FALSE").build(); - - EnvVar debugLevel = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD").withName("DEBUG") - .build(); - envVars.add(debugLevel); - - envVars.add(secretsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - ConfigMap configMap = Serialization.unmarshal(configmapAsStream, ConfigMap.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(LEFT_NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(LEFT_NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - - private static void replaceConfigMap(ConfigMap configMap) { - client.configMaps().inNamespace(LEFT_NAMESPACE).resource(configMap).createOrReplace(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/ingress.yaml deleted file mode 100644 index 1069481ee9..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-ingress-event-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-configmap-event-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/service.yaml deleted file mode 100644 index bbaf7b1e4f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap-event-reload - name: spring-cloud-kubernetes-fabric8-client-configmap-event-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-configmap-event-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml deleted file mode 100644 index d0b514f1a7..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - - - - org.springframework.cloud - spring-cloud-starter - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java deleted file mode 100644 index 8594a99cc6..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -/** - * @author wind57 - */ - -@SpringBootApplication -@EnableConfigurationProperties(ConfigMapProperties.class) -public class ConfigMapApp { - - public static void main(String[] args) { - SpringApplication.run(ConfigMapApp.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java deleted file mode 100644 index 245ffaf4ff..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/BootstrapEnabledPollingReloadConfigMapMountIT.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -public class BootstrapEnabledPollingReloadConfigMapMountIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-polling-reload"; - - private static final String NAMESPACE = "default"; - - private static Util util; - - private static KubernetesClient client; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - we have bootstrap enabled, which means we will 'locate' property sources
-	 *       from config maps.
-	 *     - there are no explicit config maps to search for, but what we will also read,
-	 *     	 is 'spring.cloud.kubernetes.config.paths', which we have set to
-	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
-	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
-	 *
-	 *     - we then change the config map content, wait for k8s to pick it up and replace them
-	 *     - our polling will then detect that change, and trigger a reload.
-	 * 
- */ - @Test - void test() { - String logs = logs(); - // (1) - Assertions.assertTrue(logs.contains("paths property sources : [/tmp/application.properties]")); - // (2) - Assertions.assertTrue(logs.contains("will add file-based property source : /tmp/application.properties")); - // (3) - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); - - // replace data in configmap and wait for k8s to pick it up - // our polling will detect that and restart the app - InputStream configMapStream = util.inputStream("mount/configmap-mount.yaml"); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); - client.configMaps().inNamespace("default").resource(configMap).createOrReplace(); - - await().timeout(Duration.ofSeconds(360)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("as-mount-changed")); - - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("mount/deployment-mount.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream configMapStream = util.inputStream("mount/configmap-mount.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - - List existing = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - - // bootstrap is enabled, which means that in 'application-with-bootstrap.yaml', - // config-data support is disabled. - EnvVar withBootstrapActiveProfile = new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE") - .withValue("with-bootstrap").build(); - EnvVar enabledBootstrap = new EnvVarBuilder().withName("SPRING_CLOUD_BOOTSTRAP_ENABLED").withValue("TRUE") - .build(); - - EnvVar debugLevelReloadCommons = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD").withValue("DEBUG") - .build(); - EnvVar debugLevelConfig = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG").withValue("DEBUG") - .build(); - EnvVar debugLevelCommons = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS").withValue("DEBUG").build(); - - existing.add(withBootstrapActiveProfile); - existing.add(enabledBootstrap); - existing.add(debugLevelReloadCommons); - existing.add(debugLevelCommons); - existing.add(debugLevelConfig); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java deleted file mode 100644 index 699e205863..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class ConfigMapPollingReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-polling-reload"; - - private static final String NAMESPACE = "default"; - - private static Util util; - - private static KubernetesClient client; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - @Test - void test() { - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("initial", result); - - // then deploy a new version of configmap - // since we poll and have reload in place, the new property must be visible - ConfigMap map = new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("poll-reload").build()) - .withData(Map.of("application.properties", "from.properties.key=after-change")).build(); - - client.configMaps().inNamespace("default").resource(map).createOrReplace(); - - await().timeout(Duration.ofSeconds(60)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("after-change")); - - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("deployment.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream configMapStream = util.inputStream("configmap.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - - List existing = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - existing.add(new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE").withValue("no-mount").build()); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/PollingReloadConfigMapMountIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/PollingReloadConfigMapMountIT.java deleted file mode 100644 index 940bea1513..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/PollingReloadConfigMapMountIT.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class PollingReloadConfigMapMountIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-polling-reload"; - - private static final String NAMESPACE = "default"; - - private static Util util; - - private static KubernetesClient client; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - we have "spring.config.import: kubernetes", which means we will 'locate' property sources
-	 *       from config maps.
-	 *     - the property above means that at the moment we will be searching for config maps that only
-	 *       match the application name, in this specific test there is no such config map.
-	 *     - what we will also read, is 'spring.cloud.kubernetes.config.paths', which we have set to
-	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
-	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
-	 *
-	 *     - we then change the config map content, wait for k8s to pick it up and replace them
-	 *     - our polling will then detect that change, and trigger a reload.
-	 * 
- */ - @Test - void test() { - String logs = logs(); - // (1) - Assertions.assertTrue(logs.contains("paths property sources : [/tmp/application.properties]")); - // (2) - Assertions.assertTrue(logs.contains("will add file-based property source : /tmp/application.properties")); - // (3) - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); - - // replace data in configmap and wait for k8s to pick it up - // our polling will detect that and restart the app - InputStream configMapStream = util.inputStream("mount/configmap-mount.yaml"); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); - client.configMaps().inNamespace("default").resource(configMap).createOrReplace(); - - await().timeout(Duration.ofSeconds(360)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("as-mount-changed")); - - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("mount/deployment-mount.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream configMapStream = util.inputStream("mount/configmap-mount.yaml"); - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - - List existing = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - - // bootstrap is disabled, which means that in 'application-mount.yaml', - // config-data support is enabled. - EnvVar mountActiveProfile = new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE").withValue("mount").build(); - EnvVar disableBootstrap = new EnvVarBuilder().withName("SPRING_CLOUD_BOOTSTRAP_ENABLED").withValue("FALSE") - .build(); - - EnvVar debugLevelReloadCommons = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD").withValue("DEBUG") - .build(); - EnvVar debugLevelConfig = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG").withValue("DEBUG") - .build(); - EnvVar debugLevelCommons = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS").withValue("DEBUG").build(); - - existing.add(mountActiveProfile); - existing.add(disableBootstrap); - existing.add(debugLevelReloadCommons); - existing.add(debugLevelCommons); - existing.add(debugLevelConfig); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml deleted file mode 100644 index 39f4572c07..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-deployment-polling-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml deleted file mode 100644 index fd53e216c1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-ingress-polling-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/configmap-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/configmap-mount.yaml deleted file mode 100644 index f2ea29a5c2..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/configmap-mount.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: poll-reload-as-mount - namespace: default -data: - application.properties: | - from.properties.key=as-mount-initial diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/deployment-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/deployment-mount.yaml deleted file mode 100644 index 4678398618..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/mount/deployment-mount.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-deployment-polling-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 - - volumeMounts: - - name: config-map-volume - mountPath: /tmp - - volumes: - - name: config-map-volume - configMap: - name: poll-reload-as-mount diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml deleted file mode 100644 index 2995e2b6b0..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/pom.xml deleted file mode 100644 index c2454a4911..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-configmap - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8-all - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapController.java deleted file mode 100644 index 3f03d99ae5..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/ConfigMapController.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class ConfigMapController { - - private final ConfigMapProperties properties; - - public ConfigMapController(ConfigMapProperties properties) { - this.properties = properties; - } - - @GetMapping("/key1") - public String key1() { - return properties.getKey1(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/resources/application.yaml deleted file mode 100644 index ace4cb6b1f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/main/resources/application.yaml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - application: - name: my-configmap - config: - import: "kubernetes:" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8ConfigMapIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8ConfigMapIT.java deleted file mode 100644 index 701d3b262e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8ConfigMapIT.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.configmap; - -import java.io.InputStream; -import java.time.Duration; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -class Fabric8ConfigMapIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap"; - - private static final String NAMESPACE = "default"; - - private static KubernetesClient client; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - util = new Util(K3S); - client = util.client(); - - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void afterAll() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - @Test - void test() { - WebClient client = builder().baseUrl("http://localhost/key1").build(); - - String result = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals("value1", result); - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("fabric8-deployment.yaml"); - InputStream serviceStream = util.inputStream("fabric8-service.yaml"); - InputStream ingressStream = util.inputStream("fabric8-ingress.yaml"); - InputStream configMapStream = util.inputStream("fabric8-configmap.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, configMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, configMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-configmap.yaml deleted file mode 100644 index e0aea66b69..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-configmap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: my-configmap - namespace: default -data: - application.yaml: | - from: - yaml: - key1: value1 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-deployment.yaml deleted file mode 100644 index 8302565ac5..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-configmap - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-configmap - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-configmap - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-ingress.yaml deleted file mode 100644 index 51dc769bb2..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-configmap - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-service.yaml deleted file mode 100644 index 7b918dfc4e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/fabric8-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-configmap - name: spring-cloud-kubernetes-fabric8-client-configmap -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-configmap - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/pom.xml deleted file mode 100644 index 868c72d03c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/pom.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - spring-cloud-kubernetes-integration-tests - org.springframework.cloud - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-discovery - - - org.springframework.cloud - spring-cloud-starter-bootstrap - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapApp.java deleted file mode 100644 index 607af1beef..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapApp.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.discovery.bootstrap; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author wind57 - */ -@SpringBootApplication -public class Fabric8DiscoveryBootstrapApp { - - public static void main(String[] args) { - SpringApplication.run(Fabric8DiscoveryBootstrapApp.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapController.java deleted file mode 100644 index f9fac40b23..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapController.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.discovery.bootstrap; - -import java.util.List; - -import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class Fabric8DiscoveryBootstrapController { - - private final KubernetesDiscoveryClient discoveryClient; - - public Fabric8DiscoveryBootstrapController(KubernetesDiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; - } - - @GetMapping("/services") - public List allServices() { - return discoveryClient.getServices(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/resources/bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/resources/bootstrap.yaml deleted file mode 100644 index fec3cf7bcf..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/main/resources/bootstrap.yaml +++ /dev/null @@ -1,16 +0,0 @@ -spring: - application: - name: fabric8-discovery-with-bootstrap - cloud: - config: - discovery: - enabled: true - fail-fast: true - retry: - max-attempts: 60 - enabled: true - request-connect-timeout: 1000 - request-read-timeout: 1000 -logging: - level: - root: DEBUG diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapIT.java deleted file mode 100644 index 9b7ba3d1f9..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/bootstrap/Fabric8DiscoveryBootstrapIT.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.discovery.bootstrap; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -class Fabric8DiscoveryBootstrapIT { - - private static final String NAMESPACE = "default"; - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap"; - - private static KubernetesClient client; - - private static Util util; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - - util.setUp(NAMESPACE); - - manifests(Phase.CREATE); - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - * A simple test to discover services. - */ - @Test - void testSimple() { - WebClient client = builder().baseUrl("http://localhost/services").build(); - - List result = client.method(HttpMethod.GET).retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }).retryWhen(retrySpec()).block(); - - Assertions.assertEquals(result.size(), 3); - Assertions.assertTrue(result.contains("kubernetes")); - Assertions.assertTrue(result.contains("spring-cloud-kubernetes-fabric8-client-discovery")); - Assertions.assertTrue(result.contains("service-wiremock")); - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("fabric8-discovery-deployment.yaml"); - InputStream serviceStream = util.inputStream("fabric8-discovery-service.yaml"); - InputStream ingressStream = util.inputStream("fabric8-discovery-ingress.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - - List existing = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - existing.add(new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES") - .withValue("true").build()); - existing.add( - new EnvVarBuilder().withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY") - .withValue("DEBUG").build()); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); - - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-deployment.yaml deleted file mode 100644 index ccc4a43015..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-discovery-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-discovery - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-discovery - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-discovery - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-ingress.yaml deleted file mode 100644 index 334508bf16..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-fabric8-client-discovery-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-discovery - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-service.yaml deleted file mode 100644 index bea511d5eb..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/fabric8-discovery-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-discovery - name: spring-cloud-kubernetes-fabric8-client-discovery -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-discovery - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml index 51c1a12795..e5706fa8e8 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -30,6 +30,14 @@ spring-cloud-kubernetes-test-support + + + + + org.springframework.cloud + spring-cloud-starter + + @@ -42,64 +50,6 @@ true - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ApplicationDiscoveryListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ApplicationDiscoveryListener.java similarity index 95% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ApplicationDiscoveryListener.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ApplicationDiscoveryListener.java index 78534a9549..0ae7606ac2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ApplicationDiscoveryListener.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ApplicationDiscoveryListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import io.fabric8.kubernetes.api.model.Pod; import org.apache.commons.logging.LogFactory; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryApp.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryApp.java index 88675932a0..1ef7ea1a55 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryApp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryController.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryController.java index e56a86225c..898c0ebf73 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.List; @@ -22,6 +22,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ReactiveDiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ReactiveDiscoveryController.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ReactiveDiscoveryController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ReactiveDiscoveryController.java index d497f6f10e..d73cdfa601 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ReactiveDiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8ReactiveDiscoveryController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryBoostrapDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryBoostrapDelegate.java new file mode 100644 index 0000000000..c4584c3f0c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryBoostrapDelegate.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.discovery; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.retrySpec; + +/** + * @author wind57 + */ +final class Fabric8DiscoveryBoostrapDelegate { + + /** + * KubernetesDiscoveryClient::getServices call must include the external-name-service + * also. + */ + static void testAllServicesWithBootstrap() { + WebClient client = builder().baseUrl("http://localhost/services").build(); + + List result = client.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(result.size(), 5); + Assertions.assertTrue(result.contains("kubernetes")); + Assertions.assertTrue(result.contains("spring-cloud-kubernetes-fabric8-client-discovery")); + Assertions.assertTrue(result.contains("service-wiremock")); + Assertions.assertTrue(result.contains("busybox-service")); + Assertions.assertTrue(result.contains("external-name-service")); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientHealthDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientHealthDelegate.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientHealthDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientHealthDelegate.java index 4e2a932dce..5f7a34937c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientHealthDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientHealthDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.List; @@ -26,8 +26,8 @@ import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.builder; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.retrySpec; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.retrySpec; import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.waitForLogStatement; /** diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientUtil.java similarity index 82% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientUtil.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientUtil.java index 15949ff362..3fb07d1b47 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryClientUtil.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryClientUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.time.Duration; import java.util.Objects; @@ -51,6 +51,39 @@ private Fabric8DiscoveryClientUtil() { { "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES", "value": "TRUE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" + } + ] + }] + } + } + } + } + """; + + static final String BODY_ONE_WITH_BOOTSTRAP = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-discovery", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES", + "value": "TRUE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" } ] }] @@ -84,6 +117,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] @@ -125,6 +162,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] @@ -162,6 +203,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] @@ -195,6 +240,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] @@ -228,6 +277,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] @@ -249,6 +302,10 @@ private Fabric8DiscoveryClientUtil() { { "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", "value": "namespace-left" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" } ] }] diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryDelegate.java similarity index 90% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryDelegate.java index e997d422c2..f4debd425f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.List; import java.util.Map; @@ -26,8 +26,8 @@ import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.builder; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.retrySpec; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.retrySpec; /** * @author wind57 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryFilterDelegate.java similarity index 94% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryFilterDelegate.java index 51a40692cd..6e15af666e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryFilterDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.Comparator; import java.util.List; @@ -27,8 +27,8 @@ import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.builder; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.retrySpec; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.retrySpec; final class Fabric8DiscoveryFilterDelegate { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryNamespaceDelegate.java similarity index 86% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryNamespaceDelegate.java index 70e90c0cfe..59c7db5ab4 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryNamespaceDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.util.List; @@ -25,8 +25,8 @@ import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.builder; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.retrySpec; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.retrySpec; /** * @author mbialkowski1 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryPodMetadataIT.java similarity index 84% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryPodMetadataIT.java index 758cb2133b..242310ca01 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/Fabric8DiscoveryPodMetadataIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.discovery; +package org.springframework.cloud.kubernetes.fabric8.client.discovery; import java.io.InputStream; import java.time.Duration; @@ -49,13 +49,14 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_FIVE; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_FOUR; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_ONE; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_SEVEN; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_SIX; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_THREE; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8DiscoveryClientUtil.BODY_TWO; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_FIVE; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_FOUR; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_ONE; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_ONE_WITH_BOOTSTRAP; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_SEVEN; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_SIX; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_THREE; +import static org.springframework.cloud.kubernetes.fabric8.client.discovery.Fabric8DiscoveryClientUtil.BODY_TWO; import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; /** @@ -97,32 +98,32 @@ static void beforeAll() throws Exception { util.setUp(NAMESPACE); manifests(Phase.CREATE); - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE, false); util.busybox(NAMESPACE, Phase.CREATE); util.createNamespace(NAMESPACE_A_UAT); util.createNamespace(NAMESPACE_B_UAT); - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE, false); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE, false); util.createNamespace(NAMESPACE_LEFT); util.createNamespace(NAMESPACE_RIGHT); - util.wiremock(NAMESPACE_LEFT, "/wiremock", Phase.CREATE); - util.wiremock(NAMESPACE_RIGHT, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_LEFT, "/wiremock", Phase.CREATE, false); + util.wiremock(NAMESPACE_RIGHT, "/wiremock", Phase.CREATE, false); } @AfterAll static void after() throws Exception { - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE, false); util.busybox(NAMESPACE, Phase.DELETE); - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE, false); util.deleteNamespace(NAMESPACE_A_UAT); util.deleteNamespace(NAMESPACE_B_UAT); - util.wiremock(NAMESPACE_LEFT, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_RIGHT, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_LEFT, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_RIGHT, "/wiremock", Phase.DELETE, false); util.deleteNamespace(NAMESPACE_LEFT); util.deleteNamespace(NAMESPACE_RIGHT); @@ -175,6 +176,7 @@ void testPodMetadata() throws Exception { private void testAllOther() { testAllServices(); + testAllServicesWithBootstrap(); testExternalNameServiceInstance(); testBlockingConfiguration(); testDefaultConfiguration(); @@ -189,6 +191,12 @@ private void testAllServices() { Fabric8DiscoveryDelegate.testAllServices(); } + private void testAllServicesWithBootstrap() { + util.patchWithReplace(DOCKER_IMAGE, DEPLOYMENT_NAME, NAMESPACE, BODY_ONE_WITH_BOOTSTRAP, + Map.of("app", IMAGE_NAME)); + Fabric8DiscoveryBoostrapDelegate.testAllServicesWithBootstrap(); + } + private void testExternalNameServiceInstance() { Fabric8DiscoveryDelegate.testExternalNameServiceInstance(); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/fabric8-discovery-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/fabric8-discovery-deployment.yaml index f2cde39e41..47f0a16f38 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/fabric8-discovery-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/fabric8-discovery-deployment.yaml @@ -26,3 +26,6 @@ spec: path: /actuator/health/liveness ports: - containerPort: 8080 + env: + - name: SPRING_CLOUD_BOOTSTRAP_ENABLED + value: "FALSE" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml new file mode 100644 index 0000000000..d04c2e404d --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml @@ -0,0 +1,47 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.1.1-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-fabric8-client-istio + + + + org.springframework.cloud + spring-cloud-kubernetes-fabric8-istio + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioApp.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioApp.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioApp.java index 65b4f1066e..d5626db45d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioApp.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioApp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.istio; +package org.springframework.cloud.kubernetes.fabric8.client.istio; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioController.java similarity index 95% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioController.java index 3e759d0ea2..09e27f99fb 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/java/org/springframework/cloud/kubernetes/fabric8/istio/IstioController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/istio/IstioController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.istio; +package org.springframework.cloud.kubernetes.fabric8.client.istio; import java.util.Arrays; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/resources/bootstrap.yml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/resources/bootstrap.yml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/main/resources/bootstrap.yml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/main/resources/bootstrap.yml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/Fabric8IstioIT.java similarity index 95% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/Fabric8IstioIT.java index 455aa93e84..5411874e9f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/istio/Fabric8IstioIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.istio; +package org.springframework.cloud.kubernetes.fabric8.client.istio; import java.io.InputStream; import java.time.Duration; @@ -24,7 +24,6 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.utils.Serialization; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -51,14 +50,12 @@ class Fabric8IstioIT { private static final String NAMESPACE = "istio-test"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-istio-it"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-istio"; private static final String ISTIO_PROXY = "istio/proxyv2"; private static final String ISTIO_PILOT = "istio/pilot"; - private static KubernetesClient client; - private static Util util; private static K3sContainer K3S; @@ -68,7 +65,6 @@ static void beforeAll() throws Exception { K3S = Commons.container(); K3S.start(); util = new Util(K3S); - client = util.client(); Commons.validateImage(IMAGE_NAME, K3S); Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); @@ -91,6 +87,7 @@ static void beforeAll() throws Exception { @AfterAll static void afterAll() throws Exception { + util.deleteNamespace("istio-system"); Commons.cleanUp(IMAGE_NAME, K3S); Commons.systemPrune(); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-deployment.yaml similarity index 61% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-deployment.yaml index 4ec3911648..64d30edb43 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-fabric8-istio-it-deployment + name: spring-cloud-kubernetes-fabric8-client-istio spec: selector: matchLabels: - app: spring-cloud-kubernetes-fabric8-istio-it + app: spring-cloud-kubernetes-fabric8-client-istio template: metadata: labels: - app: spring-cloud-kubernetes-fabric8-istio-it + app: spring-cloud-kubernetes-fabric8-client-istio spec: serviceAccountName: spring-cloud-kubernetes-istio-serviceaccount containers: - - name: spring-cloud-kubernetes-fabric8-istio-it - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-istio-it + - name: spring-cloud-kubernetes-fabric8-client-istio + image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-istio imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -26,3 +26,6 @@ spec: path: /actuator/health/liveness ports: - containerPort: 8080 + env: + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_CLIENT_ISTIO + value: "DEBUG" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-ingress.yaml similarity index 69% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-ingress.yaml index f5e5d77642..3432a205dd 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: it-ingress + name: spring-cloud-kubernetes-fabric8-client-istio namespace: istio-test spec: rules: @@ -11,6 +11,6 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-fabric8-istio-it + name: spring-cloud-kubernetes-fabric8-client-istio port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-service.yaml new file mode 100644 index 0000000000..4ef87a439c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/istio-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-fabric8-client-istio + name: spring-cloud-kubernetes-fabric8-client-istio +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-fabric8-client-istio + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/pom.xml deleted file mode 100644 index 480ca36e96..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-loadbalancer - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-loadbalancer - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/cliend/loadbalancer/Fabric8ClientLoadbalancerApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/cliend/loadbalancer/Fabric8ClientLoadbalancerApp.java deleted file mode 100644 index 1ada611025..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/cliend/loadbalancer/Fabric8ClientLoadbalancerApp.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.cliend.loadbalancer; - -import java.net.URI; -import java.util.List; -import java.util.Map; - -import reactor.core.publisher.Mono; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpMethod; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.function.client.WebClient; - -@SpringBootApplication -@RestController -public class Fabric8ClientLoadbalancerApp { - - private final DiscoveryClient discoveryClient; - - public Fabric8ClientLoadbalancerApp(DiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; - } - - public static void main(String[] args) { - SpringApplication.run(Fabric8ClientLoadbalancerApp.class, args); - } - - @Bean - @LoadBalanced - WebClient.Builder builder() { - return WebClient.builder(); - } - - @GetMapping("/servicea") - public Mono greeting() { - return builder().build().method(HttpMethod.GET).uri(URI.create("http://service-wiremock/__admin/mappings")) - .retrieve().bodyToMono(Map.class); - } - - @GetMapping("/services") - public List services() { - return discoveryClient.getServices(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/resources/application.yaml deleted file mode 100644 index c83b17d1a4..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/main/resources/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -spring: - webflux: - base-path: /loadbalancer-it diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Fabric8ClientLoadbalancerIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Fabric8ClientLoadbalancerIT.java deleted file mode 100644 index b3fa1ec34b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Fabric8ClientLoadbalancerIT.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.loadbalancer.it; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -public class Fabric8ClientLoadbalancerIT { - - private static final String NAMESPACE = "default"; - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-loadbalancer"; - - private static KubernetesClient client; - - private static Util util; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - } - - @AfterAll - static void afterAll() throws Exception { - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - @BeforeEach - void beforeEach() { - util.wiremock(NAMESPACE, "/", Phase.CREATE); - } - - @AfterEach - void afterEach() { - util.wiremock(NAMESPACE, "/", Phase.DELETE); - } - - @Test - void testLoadBalancerServiceMode() { - - manifests("SERVICE", Phase.CREATE); - - WebClient client = builder().baseUrl("http://localhost/loadbalancer-it/servicea").build(); - - @SuppressWarnings("unchecked") - Map mapResult = (Map) client.method(HttpMethod.GET).retrieve() - .bodyToMono(Map.class).retryWhen(retrySpec()).block(); - - Assertions.assertTrue(mapResult.containsKey("mappings")); - Assertions.assertTrue(mapResult.containsKey("meta")); - - manifests("SERVICE", Phase.DELETE); - - } - - @Test - public void testLoadBalancerPodMode() { - - manifests("POD", Phase.CREATE); - - WebClient client = builder().baseUrl("http://localhost/loadbalancer-it/servicea").build(); - - @SuppressWarnings("unchecked") - Map mapResult = (Map) client.method(HttpMethod.GET).retrieve() - .bodyToMono(Map.class).retryWhen(retrySpec()).block(); - - Assertions.assertTrue(mapResult.containsKey("mappings")); - Assertions.assertTrue(mapResult.containsKey("meta")); - - manifests("SERVICE", Phase.DELETE); - - } - - private static void manifests(String type, Phase phase) { - - InputStream deploymentStream = util - .inputStream("spring-cloud-kubernetes-fabric8-client-loadbalancer-deployment.yaml"); - InputStream serviceStream = util - .inputStream("spring-cloud-kubernetes-fabric8-client-loadbalancer-service.yaml"); - InputStream ingressStream = util - .inputStream("spring-cloud-kubernetes-fabric8-client-loadbalancer-ingress.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar activeProfileProperty = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE") - .withValue(type).build(); - envVars.add(activeProfileProperty); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-deployment.yaml deleted file mode 100644 index ab0e659db1..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-loadbalancer-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-loadbalancer - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-loadbalancer - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-loadbalancer - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-loadbalancer - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /loadbalancer-it/actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /loadbalancer-it/actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-ingress.yaml deleted file mode 100644 index 17aafe66ed..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-ingress.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: it-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: /loadbalancer-it - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-loadbalancer - port: - number: 8080 - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-service.yaml deleted file mode 100644 index 1ae3abc6ee..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-fabric8-client-loadbalancer-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-loadbalancer - name: spring-cloud-kubernetes-fabric8-client-loadbalancer -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-loadbalancer - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml new file mode 100644 index 0000000000..1667c1b92f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml @@ -0,0 +1,57 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.1.1-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-fabric8-client-reload + + + + + org.springframework.cloud + spring-cloud-kubernetes-fabric8-config + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.springframework.cloud + spring-cloud-starter + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/App.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/App.java similarity index 86% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/App.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/App.java index de569386f3..83cd4057e7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/App.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/App.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -24,7 +24,8 @@ * @author wind57 */ @SpringBootApplication -@EnableConfigurationProperties({ LeftProperties.class, RightProperties.class, RightWithLabelsProperties.class }) +@EnableConfigurationProperties({ LeftProperties.class, RightProperties.class, RightWithLabelsProperties.class, + ConfigMapProperties.class, SecretProperties.class }) public class App { public static void main(String[] args) { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapProperties.java index 0b6f7e61d7..bb3fb709e1 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.polling.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/Controller.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Controller.java similarity index 71% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/Controller.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Controller.java index ae0b6cbd6d..12469a7fff 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/Controller.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Controller.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,11 +31,18 @@ public class Controller { private final RightWithLabelsProperties rightWithLabelsProperties; + private final ConfigMapProperties configMapProperties; + + private final SecretProperties secretProperties; + public Controller(LeftProperties leftProperties, RightProperties rightProperties, - RightWithLabelsProperties rightWithLabelsProperties) { + RightWithLabelsProperties rightWithLabelsProperties, ConfigMapProperties configMapProperties, + SecretProperties secretProperties) { this.leftProperties = leftProperties; this.rightProperties = rightProperties; this.rightWithLabelsProperties = rightWithLabelsProperties; + this.configMapProperties = configMapProperties; + this.secretProperties = secretProperties; } @GetMapping("/left") @@ -53,4 +60,14 @@ public String witLabel() { return rightWithLabelsProperties.getValue(); } + @GetMapping("/key") + public String key() { + return configMapProperties.getKey(); + } + + @GetMapping("/key-from-secret") + public String keyFromSecret() { + return secretProperties.getKey(); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/LeftProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/LeftProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/LeftProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/LeftProperties.java index 99a3571361..e844191a2c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/LeftProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/LeftProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightProperties.java index 6987674e2f..a245678fee 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightWithLabelsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightWithLabelsProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightWithLabelsProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightWithLabelsProperties.java index fc8e19dd56..a300b765d9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/RightWithLabelsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/RightWithLabelsProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretProperties.java similarity index 79% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretProperties.java index 99419fff5c..2a58879973 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.secrets.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author wind57 */ -@ConfigurationProperties("from.properties") -public class SecretsProperties { +@ConfigurationProperties("from.secret.properties") +public class SecretProperties { private String key; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/application-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-mount.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/application-mount.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-mount.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-no-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-no-mount.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-no-mount.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-no-mount.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-one.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-one.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-one.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-three.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-three.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-three.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-two.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-two.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-two.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-with-bootstrap.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/application-with-bootstrap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-with-bootstrap.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-with-secret.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/application-with-secret.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-one.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-one.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-one.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-three.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-three.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-three.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-two.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/bootstrap-two.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-two.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/bootstrap-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/resources/bootstrap-with-bootstrap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/BootstrapEnabledPollingReloadConfigMapMountDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/BootstrapEnabledPollingReloadConfigMapMountDelegate.java new file mode 100644 index 0000000000..44625ed594 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/BootstrapEnabledPollingReloadConfigMapMountDelegate.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.reload; + +import java.io.InputStream; +import java.time.Duration; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +final class BootstrapEnabledPollingReloadConfigMapMountDelegate { + + /** + *
+	 *     - we have bootstrap enabled, which means we will 'locate' property sources
+	 *       from config maps.
+	 *     - there are no explicit config maps to search for, but what we will also read,
+	 *     	 is 'spring.cloud.kubernetes.config.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
+	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
+	 *
+	 *     - we then change the config map content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + static void testPollingReloadConfigMapWithBootstrap(KubernetesClient client, Util util, K3sContainer container, + String appLabelValue) { + // (1) + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", container, appLabelValue); + // (2) + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", container, + appLabelValue); + // (3) + WebClient webClient = TestUtil.builder().baseUrl("http://localhost/key").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); + + // we first read the initial value from the configmap + Assertions.assertEquals("as-mount-initial", result); + + // replace data in configmap and wait for k8s to pick it up + // our polling will detect that and restart the app + InputStream configMapStream = util.inputStream("configmap.yaml"); + ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); + configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); + client.configMaps().inNamespace("default").resource(configMap).createOrReplace(); + + await().timeout(Duration.ofSeconds(360)).until(() -> webClient.method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).retryWhen(TestUtil.retrySpec()).block().equals("as-mount-changed")); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapMountPollingReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapMountPollingReloadDelegate.java new file mode 100644 index 0000000000..67555c43a5 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/ConfigMapMountPollingReloadDelegate.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.reload; + +import java.io.InputStream; +import java.time.Duration; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +final class ConfigMapMountPollingReloadDelegate { + + /** + *
+	 *     - we have "spring.config.import: kubernetes", which means we will 'locate' property sources
+	 *       from config maps.
+	 *     - the property above means that at the moment we will be searching for config maps that only
+	 *       match the application name, in this specific test there is no such config map.
+	 *     - what we will also read, is 'spring.cloud.kubernetes.config.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
+	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
+	 *
+	 *     - we then change the config map content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + static void testConfigMapMountPollingReload(KubernetesClient client, Util util, K3sContainer container, + String appLabelValue) { + // (1) + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", container, appLabelValue); + // (2) + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", container, + appLabelValue); + // (3) + WebClient webClient = TestUtil.builder().baseUrl("http://localhost/key").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); + + // we first read the initial value from the configmap + Assertions.assertEquals("as-mount-initial", result); + + // replace data in configmap and wait for k8s to pick it up + // our polling will detect that and restart the app + InputStream configMapStream = util.inputStream("configmap.yaml"); + ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); + configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); + client.configMaps().inNamespace("default").resource(configMap).createOrReplace(); + + await().timeout(Duration.ofSeconds(360)).until(() -> webClient.method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).retryWhen(TestUtil.retrySpec()).block().equals("as-mount-changed")); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/DataChangesInConfigMapReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/DataChangesInConfigMapReloadDelegate.java new file mode 100644 index 0000000000..f3d20d1672 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/DataChangesInConfigMapReloadDelegate.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.reload; + +import java.time.Duration; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.logs; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.replaceConfigMap; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.retrySpec; + +final class DataChangesInConfigMapReloadDelegate { + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-reload"; + + private static final String LEFT_NAMESPACE = "left"; + + /** + *
+	 *     - configMap with no labels and data: left.value = left-initial exists in namespace left
+	 *     - we assert that we can read it correctly first, by invoking localhost/left
+	 *
+	 *     - then we change the configmap by adding a label, this in turn does not
+	 *       change the result of localhost/left, because the data has not changed.
+	 *
+	 *     - then we change data inside the config map, and we must see the updated value
+	 * 
+ */ + static void testDataChangesInConfigMap(KubernetesClient client, K3sContainer container, String appLabelValue) { + Commons.assertReloadLogStatements("added configmap informer for namespace", + "added secret informer for namespace", IMAGE_NAME); + + WebClient webClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + // we first read the initial value from the left-configmap + Assertions.assertEquals("left-initial", result); + + // then deploy a new version of left-configmap, but without changing its data, + // only add a label + ConfigMap configMap = new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder() + .withLabels(Map.of("new-label", "abc")).withNamespace("left").withName("left-configmap").build()) + .withData(Map.of("left.value", "left-initial")).build(); + + replaceConfigMap(client, configMap, "left"); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "left-initial".equals(innerResult); + }); + + String logs = logs(container, appLabelValue); + Assertions.assertTrue(logs.contains("ConfigMap left-configmap was updated in namespace left")); + Assertions.assertTrue(logs.contains("data in configmap has not changed, will not reload")); + + // change data + configMap = new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace("left") + .withName("left-configmap").build()) + .withData(Map.of("left.value", "left-after-change")).build(); + + replaceConfigMap(client, configMap, "left"); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "left-after-change".equals(innerResult); + }); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/ConfigMapEventReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8EventReloadIT.java similarity index 61% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/ConfigMapEventReloadIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8EventReloadIT.java index 5029dbf8b6..6309e280dc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/ConfigMapEventReloadIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8EventReloadIT.java @@ -14,23 +14,17 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.fabric8.client.reload; import java.io.InputStream; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; @@ -41,25 +35,24 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion; /** * @author wind57 */ -class ConfigMapEventReloadIT { +class Fabric8EventReloadIT { - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-event-reload"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-reload"; + + private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + pomVersion(); private static final String NAMESPACE = "default"; @@ -81,6 +74,9 @@ static void beforeAll() throws Exception { util.createNamespace("left"); util.createNamespace("right"); util.setUpClusterWide(NAMESPACE, Set.of("left", "right")); + util.setUp(NAMESPACE); + + manifests(Phase.CREATE); } @AfterAll @@ -89,6 +85,8 @@ static void afterAll() throws Exception { util.deleteNamespace("right"); Commons.cleanUp(IMAGE_NAME, K3S); Commons.systemPrune(); + + manifests(Phase.DELETE); } /** @@ -101,20 +99,20 @@ static void afterAll() throws Exception { */ @Test void testInformFromOneNamespaceEventNotTriggered() { - manifests("one", Phase.CREATE, false); Commons.assertReloadLogStatements("added configmap informer for namespace", "added secret informer for namespace", IMAGE_NAME); - WebClient webClient = builder().baseUrl("http://localhost/left").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); + WebClient webClient = TestUtil.builder().baseUrl("http://localhost/left").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); // we first read the initial value from the left-configmap Assertions.assertEquals("left-initial", result); // then read the value from the right-configmap - webClient = builder().baseUrl("http://localhost/right").build(); - result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()).block(); + webClient = TestUtil.builder().baseUrl("http://localhost/right").build(); + result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(TestUtil.retrySpec()) + .block(); Assertions.assertEquals("right-initial", result); // then deploy a new version of right-configmap @@ -122,37 +120,48 @@ void testInformFromOneNamespaceEventNotTriggered() { .withMetadata(new ObjectMetaBuilder().withNamespace("right").withName("right-configmap").build()) .withData(Map.of("right.value", "right-after-change")).build(); - replaceConfigMap(rightConfigMapAfterChange); + TestUtil.replaceConfigMap(client, rightConfigMapAfterChange, "right"); - // wait dummy for 5 seconds - LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5)); + webClient = TestUtil.builder().baseUrl("http://localhost/left").build(); - webClient = builder().baseUrl("http://localhost/left").build(); - result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()).block(); - // left configmap has not changed, no restart of app has happened - Assertions.assertEquals("left-initial", result); + WebClient finalWebClient = webClient; + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(30)).until(() -> { + String innerResult = finalWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); + // left configmap has not changed, no restart of app has happened + return "left-initial".equals(innerResult); + }); - manifests("one", Phase.DELETE, false); + testInformFromOneNamespaceEventTriggered(); + testInform(); + testInformFromOneNamespaceEventTriggeredSecretsDisabled(); + testDataChangesInConfigMap(); + testConfigMapMountPollingReload(); + testPollingReloadConfigMapWithBootstrap(); + testSecretReload(); } /** *
-	 *     - there are two namespaces : left and right
-	 *     - each of the namespaces has one configmap
-	 *     - we watch the "right" namespace and make a change in the configmap in the same namespace
-	 *     - as such, event is triggered and we see the updated value
+	 * - there are two namespaces : left and right
+	 * - each of the namespaces has one configmap
+	 * - we watch the "right" namespace and make a change in the configmap in the same
+	 * namespace
+	 * - as such, event is triggered and we see the updated value
 	 * 
*/ - @Test void testInformFromOneNamespaceEventTriggered() { - manifests("two", Phase.CREATE, false); + + TestUtil.reCreateSources(util, client); + TestUtil.patchOne(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Commons.assertReloadLogStatements("added configmap informer for namespace", "added secret informer for namespace", IMAGE_NAME); // read the value from the right-configmap - WebClient webClient = builder().baseUrl("http://localhost/right").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); + WebClient webClient = TestUtil.builder().baseUrl("http://localhost/right").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); Assertions.assertEquals("right-initial", result); // then deploy a new version of right-configmap @@ -160,46 +169,47 @@ void testInformFromOneNamespaceEventTriggered() { .withMetadata(new ObjectMetaBuilder().withNamespace("right").withName("right-configmap").build()) .withData(Map.of("right.value", "right-after-change")).build(); - replaceConfigMap(rightConfigMapAfterChange); + TestUtil.replaceConfigMap(client, rightConfigMapAfterChange, "right"); String[] resultAfterChange = new String[1]; await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/right").build(); + WebClient innerWebClient = TestUtil.builder().baseUrl("http://localhost/right").build(); String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); + .retryWhen(TestUtil.retrySpec()).block(); resultAfterChange[0] = innerResult; return innerResult != null; }); Assertions.assertEquals("right-after-change", resultAfterChange[0]); - - manifests("two", Phase.DELETE, false); } /** *
-	 *     - there are two namespaces : left and right (though we do not care about the left one)
-	 *     - left has one configmap : left-configmap
-	 *     - right has two configmaps: right-configmap, right-configmap-with-label
-	 *     - we watch the "right" namespace, but enable tagging; which means that only
-	 *       right-configmap-with-label triggers changes.
+	 * - there are two namespaces : left and right (though we do not care about the left
+	 * one)
+	 * - left has one configmap : left-configmap
+	 * - right has two configmaps: right-configmap, right-configmap-with-label
+	 * - we watch the "right" namespace, but enable tagging; which means that only
+	 * right-configmap-with-label triggers changes.
 	 * 
*/ - @Test void testInform() { - manifests("three", Phase.CREATE, false); + + TestUtil.reCreateSources(util, client); + TestUtil.patchTwo(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Commons.assertReloadLogStatements("added configmap informer for namespace", "added secret informer for namespace", IMAGE_NAME); // read the initial value from the right-configmap - WebClient rightWebClient = builder().baseUrl("http://localhost/right").build(); + WebClient rightWebClient = TestUtil.builder().baseUrl("http://localhost/right").build(); String rightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); + .retryWhen(TestUtil.retrySpec()).block(); Assertions.assertEquals("right-initial", rightResult); // then read the initial value from the right-with-label-configmap - WebClient rightWithLabelWebClient = builder().baseUrl("http://localhost/with-label").build(); + WebClient rightWithLabelWebClient = TestUtil.builder().baseUrl("http://localhost/with-label").build(); String rightWithLabelResult = rightWithLabelWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); + .retryWhen(TestUtil.retrySpec()).block(); Assertions.assertEquals("right-with-label-initial", rightWithLabelResult); // then deploy a new version of right-configmap @@ -207,15 +217,14 @@ void testInform() { .withMetadata(new ObjectMetaBuilder().withNamespace("right").withName("right-configmap").build()) .withData(Map.of("right.value", "right-after-change")).build(); - replaceConfigMap(rightConfigMapAfterChange); - - // sleep for 5 seconds - LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5)); + TestUtil.replaceConfigMap(client, rightConfigMapAfterChange, "right"); // nothing changes in our app, because we are watching only labeled configmaps - rightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-initial", rightResult); + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(30)).until(() -> { + String innerRightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); + return "right-initial".equals(innerRightResult); + }); // then deploy a new version of right-with-label-configmap ConfigMap rightWithLabelConfigMapAfterChange = new ConfigMapBuilder() @@ -223,15 +232,15 @@ void testInform() { new ObjectMetaBuilder().withNamespace("right").withName("right-configmap-with-label").build()) .withData(Map.of("right.with.label.value", "right-with-label-after-change")).build(); - replaceConfigMap(rightWithLabelConfigMapAfterChange); + TestUtil.replaceConfigMap(client, rightWithLabelConfigMapAfterChange, "right"); // since we have changed a labeled configmap, app will restart and pick up the new // value String[] resultAfterChange = new String[1]; await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/with-label").build(); + WebClient innerWebClient = TestUtil.builder().baseUrl("http://localhost/with-label").build(); String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); + .retryWhen(TestUtil.retrySpec()).block(); resultAfterChange[0] = innerResult; return innerResult != null; }); @@ -239,32 +248,33 @@ void testInform() { // right-configmap now will see the new value also, but only because the other // configmap has triggered the restart - rightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); + rightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); Assertions.assertEquals("right-after-change", rightResult); - - manifests("three", Phase.DELETE, false); } /** *
-	 *     - there are two namespaces : left and right
-	 *     - each of the namespaces has one configmap
-	 *     - secrets are disabled
-	 *     - we watch the "right" namespace and make a change in the configmap in the same namespace
-	 *     - as such, event is triggered and we see the updated value
+	 * - there are two namespaces : left and right
+	 * - each of the namespaces has one configmap
+	 * - secrets are disabled
+	 * - we watch the "right" namespace and make a change in the configmap in the same
+	 * namespace
+	 * - as such, event is triggered and we see the updated value
 	 * 
*/ - @Test void testInformFromOneNamespaceEventTriggeredSecretsDisabled() { - manifests("two", Phase.CREATE, true); + + TestUtil.reCreateSources(util, client); + TestUtil.patchThree(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + Commons.assertReloadLogStatements("added configmap informer for namespace", "added secret informer for namespace", IMAGE_NAME); // read the value from the right-configmap - WebClient webClient = builder().baseUrl("http://localhost/right").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); + WebClient webClient = TestUtil.builder().baseUrl("http://localhost/right").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(TestUtil.retrySpec()).block(); Assertions.assertEquals("right-initial", result); // then deploy a new version of right-configmap @@ -272,22 +282,45 @@ void testInformFromOneNamespaceEventTriggeredSecretsDisabled() { .withMetadata(new ObjectMetaBuilder().withNamespace("right").withName("right-configmap").build()) .withData(Map.of("right.value", "right-after-change")).build(); - replaceConfigMap(rightConfigMapAfterChange); + TestUtil.replaceConfigMap(client, rightConfigMapAfterChange, "right"); String[] resultAfterChange = new String[1]; await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/right").build(); + WebClient innerWebClient = TestUtil.builder().baseUrl("http://localhost/right").build(); String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); + .retryWhen(TestUtil.retrySpec()).block(); resultAfterChange[0] = innerResult; return innerResult != null; }); Assertions.assertEquals("right-after-change", resultAfterChange[0]); - manifests("two", Phase.DELETE, true); } - private static void manifests(String activeProfile, Phase phase, boolean secretsDisabled) { + void testDataChangesInConfigMap() { + TestUtil.reCreateSources(util, client); + TestUtil.patchFour(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + DataChangesInConfigMapReloadDelegate.testDataChangesInConfigMap(client, K3S, IMAGE_NAME); + } + + void testConfigMapMountPollingReload() { + TestUtil.reCreateSources(util, client); + TestUtil.patchFive(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + ConfigMapMountPollingReloadDelegate.testConfigMapMountPollingReload(client, util, K3S, IMAGE_NAME); + } + + void testPollingReloadConfigMapWithBootstrap() { + TestUtil.reCreateSources(util, client); + TestUtil.patchSix(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + BootstrapEnabledPollingReloadConfigMapMountDelegate.testPollingReloadConfigMapWithBootstrap(client, util, K3S, + IMAGE_NAME); + } + + void testSecretReload() { + TestUtil.patchSeven(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE); + SecretsEventsReloadDelegate.testSecretReload(client, K3S, IMAGE_NAME); + } + + private static void manifests(Phase phase) { InputStream deploymentStream = util.inputStream("deployment.yaml"); InputStream serviceStream = util.inputStream("service.yaml"); @@ -295,59 +328,34 @@ private static void manifests(String activeProfile, Phase phase, boolean secrets InputStream leftConfigMapStream = util.inputStream("left-configmap.yaml"); InputStream rightConfigMapStream = util.inputStream("right-configmap.yaml"); InputStream rightWithLabelConfigMapStream = util.inputStream("right-configmap-with-label.yaml"); + InputStream configMapAsStream = util.inputStream("configmap.yaml"); + InputStream secretAsStream = util.inputStream("secret.yaml"); Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar activeProfileProperty = new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE").withValue(activeProfile) - .build(); - envVars.add(activeProfileProperty); - - if (secretsDisabled) { - EnvVar secretsDisabledEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED") - .withValue("FALSE").build(); - envVars.add(secretsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - } - - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - Service service = Serialization.unmarshal(serviceStream, Service.class); Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); ConfigMap leftConfigMap = Serialization.unmarshal(leftConfigMapStream, ConfigMap.class); ConfigMap rightConfigMap = Serialization.unmarshal(rightConfigMapStream, ConfigMap.class); ConfigMap rightWithLabelConfigMap = Serialization.unmarshal(rightWithLabelConfigMapStream, ConfigMap.class); + ConfigMap configMap = Serialization.unmarshal(configMapAsStream, ConfigMap.class); + Secret secret = Serialization.unmarshal(secretAsStream, Secret.class); if (phase.equals(Phase.CREATE)) { util.createAndWait("left", leftConfigMap, null); util.createAndWait("right", rightConfigMap, null); - if ("three".equals(activeProfile)) { - util.createAndWait("right", rightWithLabelConfigMap, null); - } + util.createAndWait("right", rightWithLabelConfigMap, null); + util.createAndWait(NAMESPACE, configMap, secret); util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); } else { util.deleteAndWait("left", leftConfigMap, null); util.deleteAndWait("right", rightConfigMap, null); - if ("three".equals(activeProfile)) { - util.deleteAndWait("right", rightWithLabelConfigMap, null); - } + util.deleteAndWait("right", rightWithLabelConfigMap, null); + util.deleteAndWait(NAMESPACE, configMap, secret); util.deleteAndWait(NAMESPACE, deployment, service, ingress); } } - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - - private static void replaceConfigMap(ConfigMap configMap) { - client.configMaps().inNamespace("right").resource(configMap).createOrReplace(); - } - } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretsEventsReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretsEventsReloadDelegate.java new file mode 100644 index 0000000000..2e89605930 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/SecretsEventsReloadDelegate.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.reload; + +import java.time.Duration; +import java.util.Base64; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.builder; +import static org.springframework.cloud.kubernetes.fabric8.client.reload.TestUtil.retrySpec; + +/** + * @author wind57 + */ +final class SecretsEventsReloadDelegate { + + /** + *
+	 *     - secret with no labels and data: from.secret.properties.key = secret-initial exists in namespace default
+	 *     - we assert that we can read it correctly first, by invoking localhost/key.
+	 *
+	 *     - then we change the secret by adding a label, this in turn does not
+	 *       change the result of localhost/key, because the data has not changed.
+	 *
+	 *     - then we change data inside the secret, and we must see the updated value.
+	 * 
+ */ + static void testSecretReload(KubernetesClient client, K3sContainer container, String appLabelValue) { + Commons.assertReloadLogStatements("added secret informer for namespace", + "added configmap informer for namespace", appLabelValue); + + WebClient webClient = builder().baseUrl("http://localhost/key-from-secret").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + Assertions.assertEquals("secret-initial", result); + + Secret secret = new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("letter", "a")).withNamespace("default") + .withName("event-reload").build()) + .withData(Map.of("application.properties", + Base64.getEncoder().encodeToString("from.secret.properties.key=secret-initial".getBytes()))) + .build(); + client.secrets().inNamespace("default").resource(secret).createOrReplace(); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/key-from-secret").build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "secret-initial".equals(innerResult); + }); + + Commons.waitForLogStatement("Secret event-reload was updated in namespace default", container, appLabelValue); + Commons.waitForLogStatement("data in secret has not changed, will not reload", container, appLabelValue); + + // change data + secret = new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("event-reload").build()) + .withData(Map.of("application.properties", + Base64.getEncoder() + .encodeToString("from.secret.properties.key=secret-initial-changed".getBytes()))) + .build(); + + client.secrets().inNamespace("default").resource(secret).createOrReplace(); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/key-from-secret").build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "secret-initial-changed".equals(innerResult); + }); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestUtil.java new file mode 100644 index 0000000000..ac83723776 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/TestUtil.java @@ -0,0 +1,372 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.client.reload; + +import java.io.InputStream; +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import org.testcontainers.containers.Container; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +final class TestUtil { + + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-fabric8-client-reload"); + + private static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "two" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_TWO = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "three" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_THREE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "two" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", + "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_FOUR = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "one" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", + "value": "FALSE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_FIVE = """ + { + "spec": { + "template": { + "spec": { + "volumes": [ + { + "configMap": { + "defaultMode": 420, + "name": "poll-reload" + }, + "name": "config-map-volume" + } + ], + "containers": [{ + "volumeMounts": [ + { + "mountPath": "/tmp", + "name": "config-map-volume" + } + ], + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "mount" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", + "value": "DEBUG" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_SIX = """ + { + "spec": { + "template": { + "spec": { + "volumes": [ + { + "configMap": { + "defaultMode": 420, + "name": "poll-reload" + }, + "name": "config-map-volume" + } + ], + "containers": [{ + "volumeMounts": [ + { + "mountPath": "/tmp", + "name": "config-map-volume" + } + ], + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "with-bootstrap" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", + "value": "DEBUG" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_SEVEN = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-fabric8-client-configmap-event-reload", + "image": "image_name_here", + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "with-secret" + } + ] + }] + } + } + } + } + """; + + private TestUtil() { + + } + + static void reCreateSources(Util util, KubernetesClient client) { + InputStream leftConfigMapStream = util.inputStream("left-configmap.yaml"); + InputStream rightConfigMapStream = util.inputStream("right-configmap.yaml"); + InputStream configMapStream = util.inputStream("configmap.yaml"); + + ConfigMap leftConfigMap = Serialization.unmarshal(leftConfigMapStream, ConfigMap.class); + ConfigMap rightConfigMap = Serialization.unmarshal(rightConfigMapStream, ConfigMap.class); + ConfigMap configMap = Serialization.unmarshal(configMapStream, ConfigMap.class); + + replaceConfigMap(client, leftConfigMap, "left"); + replaceConfigMap(client, rightConfigMap, "right"); + replaceConfigMap(client, configMap, "default"); + } + + static void replaceConfigMap(KubernetesClient client, ConfigMap configMap, String namespace) { + client.configMaps().inNamespace(namespace).resource(configMap).createOrReplace(); + } + + static void patchOne(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + + static void patchTwo(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_TWO, POD_LABELS); + } + + static void patchThree(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_THREE, POD_LABELS); + } + + static void patchFour(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_FOUR, POD_LABELS); + } + + static void patchFive(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_FIVE, POD_LABELS); + } + + static void patchSix(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_SIX, POD_LABELS); + } + + static void patchSeven(Util util, String dockerImage, String deploymentName, String namespace) { + util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_SEVEN, POD_LABELS); + } + + static WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + static RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); + } + + static String logs(K3sContainer container, String appLabelValue) { + try { + String appPodName = container + .execInContainer("sh", "-c", + "kubectl get pods -l app=" + appLabelValue + " -o=name --no-headers | tr -d '\n'") + .getStdout(); + + Container.ExecResult execResult = container.execInContainer("sh", "-c", + "kubectl logs " + appPodName.trim()); + return execResult.getStdout(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/configmap.yaml similarity index 72% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/configmap.yaml index 194603083c..2c6c6d0839 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/configmap.yaml @@ -5,4 +5,4 @@ metadata: namespace: default data: application.properties: | - from.properties.key=initial + from.properties.key=as-mount-initial diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/deployment.yaml similarity index 62% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/deployment.yaml index a7fe7eaca3..aba13d5127 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-fabric8-client-configmap-deployment-event-reload + name: spring-cloud-kubernetes-fabric8-client-reload spec: selector: matchLabels: - app: spring-cloud-kubernetes-fabric8-client-configmap-event-reload + app: spring-cloud-kubernetes-fabric8-client-reload template: metadata: labels: - app: spring-cloud-kubernetes-fabric8-client-configmap-event-reload + app: spring-cloud-kubernetes-fabric8-client-reload spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-fabric8-client-configmap-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-configmap-event-reload + - name: spring-cloud-kubernetes-fabric8-client-reload + image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-reload imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -28,4 +28,8 @@ spec: - containerPort: 8080 env: - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_CONFIG_RELOAD - value: DEBUG + value: "DEBUG" + - name: SPRING_PROFILES_ACTIVE + value: "one" + - name: SPRING_CLOUD_BOOTSTRAP_ENABLED + value: "TRUE" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/ingress.yaml similarity index 67% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/ingress.yaml index 083811f404..d6e9e49a9e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/spring-cloud-kubernetes-client-config-it-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: it-ingress + name: spring-cloud-kubernetes-fabric8-client-reload-ingress namespace: default spec: rules: @@ -11,6 +11,6 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-client-config-it + name: spring-cloud-kubernetes-fabric8-client-reload port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/left-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/left-configmap.yaml similarity index 75% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/left-configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/left-configmap.yaml index 560954befa..a72d8af5dc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/left-configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/left-configmap.yaml @@ -4,4 +4,4 @@ metadata: name: left-configmap namespace: left data: - left.value: left-initial + left.value: "left-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap-with-label.yaml similarity index 76% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap-with-label.yaml index 56531a6352..658ebca4c2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap-with-label.yaml @@ -6,4 +6,4 @@ metadata: labels: spring.cloud.kubernetes.config.informer.enabled: true data: - right.with.label.value: right-with-label-initial + right.with.label.value: "right-with-label-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap.yaml similarity index 74% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap.yaml index a14273279b..0ede471da5 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/right-configmap.yaml @@ -4,4 +4,4 @@ metadata: name: right-configmap namespace: right data: - right.value: right-initial + right.value: "right-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/secret.yaml similarity index 51% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/secret.yaml index 9ad19aadfc..260f79102c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/secret.yaml @@ -4,6 +4,6 @@ metadata: name: event-reload namespace: default data: - # from.properties.key=initial + # from.secret.properties.key=secret-initial application.properties: | - ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFs + ZnJvbS5zZWNyZXQucHJvcGVydGllcy5rZXk9c2VjcmV0LWluaXRpYWw= diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/service.yaml new file mode 100644 index 0000000000..3f73ad0763 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/resources/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-fabric8-client-reload + name: spring-cloud-kubernetes-fabric8-client-reload +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-fabric8-client-reload + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml deleted file mode 100644 index 3fad77ce5d..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-secrets-event-reload - - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java deleted file mode 100644 index cc49b3e4bc..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.secrets.event.reload; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -/** - * @author wind57 - */ - -@SpringBootApplication -@EnableConfigurationProperties(SecretsProperties.class) -public class SecretsApp { - - public static void main(String[] args) { - SpringApplication.run(SecretsApp.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java deleted file mode 100644 index 5c3d3ddac7..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.secrets.event.reload; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class SecretsController { - - private final SecretsProperties properties; - - public SecretsController(SecretsProperties properties) { - this.properties = properties; - } - - @GetMapping("/key") - public String key() { - return properties.getKey(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/DataChangesInSecretsReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/DataChangesInSecretsReloadIT.java deleted file mode 100644 index 25a681729a..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/DataChangesInSecretsReloadIT.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.secrets.event.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class DataChangesInSecretsReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-secrets-event-reload"; - - private static final String NAMESPACE = "default"; - - private static KubernetesClient client; - - private static Util util; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - } - - @AfterAll - static void after() throws Exception { - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - /** - *
-	 *     - secret with no labels and data: from.properties.key = initial exists in namespace default
-	 *     - we assert that we can read it correctly first, by invoking localhost/key.
-	 *
-	 *     - then we change the secret by adding a label, this in turn does not
-	 *       change the result of localhost/key, because the data has not changed.
-	 *
-	 *     - then we change data inside the secret, and we must see the updated value.
-	 * 
- */ - @Test - void testSimple() { - manifests(Phase.CREATE); - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); - - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("initial", result); - - Secret secret = new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("letter", "a")).withNamespace(NAMESPACE) - .withName("event-reload").build()) - .withData(Map.of("application.properties", - Base64.getEncoder().encodeToString("from.properties.key=initial".getBytes()))) - .build(); - client.secrets().inNamespace(NAMESPACE).resource(secret).createOrReplace(); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "initial".equals(innerResult); - }); - - String logs = logs(); - Assertions.assertTrue(logs.contains("Secret event-reload was updated in namespace default")); - Assertions.assertTrue(logs.contains("data in secret has not changed, will not reload")); - - // change data - secret = new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace(NAMESPACE).withName("event-reload").build()) - .withData(Map.of("application.properties", - Base64.getEncoder().encodeToString("from.properties.key=initial-changed".getBytes()))) - .build(); - - client.secrets().inNamespace(NAMESPACE).resource(secret).createOrReplace(); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .retryWhen(retrySpec()).block(); - return "initial-changed".equals(innerResult); - }); - - manifests(Phase.DELETE); - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("deployment.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream secretAsStream = util.inputStream("secret.yaml"); - - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar activeProfileProperty = new EnvVarBuilder().withName("SPRING_PROFILES_ACTIVE").withValue("one").build(); - envVars.add(activeProfileProperty); - - EnvVar configMapsDisabledEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED") - .withValue("FALSE").build(); - - EnvVar debugLevel = new EnvVarBuilder() - .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD").withName("DEBUG") - .build(); - envVars.add(debugLevel); - - envVars.add(configMapsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - Secret secret = Serialization.unmarshal(secretAsStream, Secret.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, secret); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, null, secret); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private String logs() { - try { - String appPodName = K3S.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); - - Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java deleted file mode 100644 index d7c80dfd54..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2013-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.secrets.event.reload; - -import java.io.InputStream; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.EnvVar; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class SecretsEventsReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-secrets-event-reload"; - - private static final String NAMESPACE = "default"; - - private static KubernetesClient client; - - private static Util util; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - } - - @AfterAll - static void after() throws Exception { - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - @Test - void testSimple() { - manifests(Phase.CREATE, false); - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the secret - Assertions.assertEquals("initial", result); - - // then deploy a new version of the secret - // since we poll and have reload in place, the new property must be visible - Secret secret = new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("event-reload").build()) - .withData(Map.of("application.properties", - Base64.getEncoder().encodeToString("from.properties.key=after-change".getBytes()))) - .build(); - - client.secrets().inNamespace("default").resource(secret).createOrReplace(); - - await().timeout(Duration.ofSeconds(120)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("after-change")); - - manifests(Phase.DELETE, false); - } - - @Test - void testSimpleConfigMapsDisabled() { - manifests(Phase.CREATE, true); - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); - WebClient webClient = builder().baseUrl("http://localhost/key").build(); - String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the secret - Assertions.assertEquals("initial", result); - - // then deploy a new version of the secret - // since we poll and have reload in place, the new property must be visible - Secret secret = new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("event-reload").build()) - .withData(Map.of("application.properties", - Base64.getEncoder().encodeToString("from.properties.key=after-change".getBytes()))) - .build(); - - client.secrets().inNamespace("default").resource(secret).createOrReplace(); - - await().timeout(Duration.ofSeconds(120)).until(() -> webClient.method(HttpMethod.GET).retrieve() - .bodyToMono(String.class).retryWhen(retrySpec()).block().equals("after-change")); - - manifests(Phase.DELETE, true); - - } - - private static void manifests(Phase phase, boolean configMapsDisabled) { - - InputStream deploymentStream = util.inputStream("deployment.yaml"); - InputStream serviceStream = util.inputStream("service.yaml"); - InputStream ingressStream = util.inputStream("ingress.yaml"); - InputStream secretStream = util.inputStream("secret.yaml"); - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - Secret secret = Serialization.unmarshal(secretStream, Secret.class); - - if (configMapsDisabled) { - List envVars = new ArrayList<>( - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); - EnvVar configMapsDisabledEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED") - .withValue("FALSE").build(); - envVars.add(configMapsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - } - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, secret); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, null, secret); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml deleted file mode 100644 index ce3261817b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-secrets-deployment-event-reload -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-secrets-event-reload - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 - env: - - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_CONFIG_RELOAD - value: DEBUG diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml deleted file mode 100644 index 19e5d0df5c..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-cloud-kubernetes-fabric8-client-secrets-ingress-event-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml deleted file mode 100644 index 239bc00bbb..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/pom.xml deleted file mode 100644 index 5ee2a45a27..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-client-simple-core - - - - org.springframework.cloud - spring-cloud-starter-kubernetes-fabric8-all - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreApp.java deleted file mode 100644 index facc56c287..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreApp.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.simple.core; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -/** - * @author wind57 - */ -@EnableConfigurationProperties(SimpleCoreProperties.class) -@SpringBootApplication -public class SimpleCoreApp { - - public static void main(String[] args) { - SpringApplication.run(SimpleCoreApp.class, args); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreController.java deleted file mode 100644 index 7f4c7b537e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreController.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.simple.core; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class SimpleCoreController { - - private final SimpleCoreProperties properties; - - public SimpleCoreController(SimpleCoreProperties properties) { - this.properties = properties; - } - - @GetMapping("/message") - public String message() { - return properties.getMessage(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreProperties.java deleted file mode 100644 index 480ed09ef0..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/java/org/springframework/cloud/kubernetes/fabric8/simple/core/SimpleCoreProperties.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.simple.core; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * @author wind57 - */ -@ConfigurationProperties("greeting") -public class SimpleCoreProperties { - - private String message; - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application-kubernetes.yml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application-kubernetes.yml deleted file mode 100644 index 392692c9e3..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application-kubernetes.yml +++ /dev/null @@ -1,2 +0,0 @@ -greeting: - message: Hello from k8s profile diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application.yml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application.yml deleted file mode 100644 index f2c433821f..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/main/resources/application.yml +++ /dev/null @@ -1,12 +0,0 @@ -server: - port: 8080 - -greeting: - message: Hello sb - -# we enable some of the management endpoints to make it possible to restart the application -management: - endpoint: - health: - enabled: true - show-details: always diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/java/org/springframework/cloud/kubernetes/fabric8/core/SimpleCoreIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/java/org/springframework/cloud/kubernetes/fabric8/core/SimpleCoreIT.java deleted file mode 100644 index 7a810b3b2a..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/java/org/springframework/cloud/kubernetes/fabric8/core/SimpleCoreIT.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.core; - -import java.io.InputStream; -import java.time.Duration; -import java.util.Objects; - -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -class SimpleCoreIT { - - private static final String NAMESPACE = "default"; - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-simple-core"; - - private static KubernetesClient client; - - private static Util util; - - private static final K3sContainer K3S = Commons.container(); - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - - util = new Util(K3S); - client = util.client(); - util.setUp(NAMESPACE); - - manifests(Phase.CREATE); - } - - @AfterAll - static void after() throws Exception { - manifests(Phase.DELETE); - Commons.cleanUp(IMAGE_NAME, K3S); - Commons.systemPrune(); - } - - @Test - void test() { - WebClient client = builder().baseUrl("http://localhost/message").build(); - - String result = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) - .block(); - - // value must come from application-kubernetes.yml - Assertions.assertEquals("Hello from k8s profile", result); - } - - private static void manifests(Phase phase) { - - InputStream deploymentStream = util.inputStream("simple-core-deployment.yaml"); - InputStream serviceStream = util.inputStream("simple-core-service.yaml"); - InputStream ingressStream = util.inputStream("simple-core-ingress.yaml"); - Deployment deployment = Serialization.unmarshal(deploymentStream, Deployment.class); - Service service = Serialization.unmarshal(serviceStream, Service.class); - Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-deployment.yaml deleted file mode 100644 index 9f53a3807b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-fabric8-client-simple-core-deployment -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-fabric8-client-simple-core - template: - metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-simple-core - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-fabric8-client-simple-core - image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-simple-core - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-service.yaml deleted file mode 100644 index 34ae76bce3..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-fabric8-client-simple-core - name: spring-cloud-kubernetes-fabric8-client-simple-core -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-fabric8-client-simple-core - type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/pom.xml deleted file mode 100644 index 80a88a9750..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/pom.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - org.springframework.cloud - spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT - - 4.0.0 - - spring-cloud-kubernetes-fabric8-istio-it - - - - org.springframework.cloud - spring-cloud-kubernetes-fabric8-istio - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.cloud - spring-cloud-kubernetes-test-support - - - - - - - ../src/main/resources - true - - - src/main/resources - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/logback-test.xml deleted file mode 100644 index ee24334373..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml new file mode 100644 index 0000000000..f363aef3e6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml @@ -0,0 +1,48 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 3.1.1-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-k8s-client-catalog-watcher + + + + org.springframework.cloud + spring-cloud-kubernetes-client-discovery + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/Application.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/Application.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/Application.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/Application.java index d896ecb20a..bd8fdf5f1e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/Application.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/Application.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/EndpointNameAndNamespaceService.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/EndpointNameAndNamespaceService.java similarity index 94% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/EndpointNameAndNamespaceService.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/EndpointNameAndNamespaceService.java index 7f0c9cf750..7ee940e908 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/EndpointNameAndNamespaceService.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/EndpointNameAndNamespaceService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartBeatListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartBeatListener.java similarity index 95% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartBeatListener.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartBeatListener.java index 88ef4e734d..ad1910846a 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartBeatListener.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartBeatListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartbeatController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartbeatController.java similarity index 94% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartbeatController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartbeatController.java index 6b6a1a0178..7a738a2eb2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/client/catalog/HeartbeatController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/HeartbeatController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchIT.java similarity index 91% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchIT.java index 4918f807a0..2dd5769748 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import java.time.Duration; import java.util.List; @@ -48,9 +48,6 @@ import org.springframework.web.reactive.function.client.WebClient; import static org.awaitility.Awaitility.await; -import static org.springframework.cloud.kubernetes.client.catalog.KubernetesClientCatalogWatchUtils.patchForEndpointSlices; -import static org.springframework.cloud.kubernetes.client.catalog.KubernetesClientCatalogWatchUtils.patchForEndpointSlicesNamespaces; -import static org.springframework.cloud.kubernetes.client.catalog.KubernetesClientCatalogWatchUtils.patchForEndpointsNamespaces; import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.waitForLogStatement; /** @@ -59,7 +56,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class KubernetesClientCatalogWatchIT { - private static final String APP_NAME = "spring-cloud-kubernetes-client-catalog-watcher"; + private static final String APP_NAME = "spring-cloud-kubernetes-k8s-client-catalog-watcher"; private static final String NAMESPACE = "default"; @@ -115,7 +112,7 @@ void testCatalogWatchWithEndpoints() { @Test @Order(2) void testCatalogWatchWithEndpointSlices() { - patchForEndpointSlices(APP_NAME, NAMESPACE, DOCKER_IMAGE); + KubernetesClientCatalogWatchUtils.patchForEndpointSlices(APP_NAME, NAMESPACE, DOCKER_IMAGE); waitForLogStatement("stateGenerator is of type: KubernetesEndpointSlicesCatalogWatch", K3S, APP_NAME); test(); @@ -128,13 +125,14 @@ void testCatalogWatchWithEndpointsNamespaces() { util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE_A, NAMESPACE_B)); util.busybox(NAMESPACE_A, Phase.CREATE); util.busybox(NAMESPACE_B, Phase.CREATE); - patchForEndpointsNamespaces(APP_NAME, NAMESPACE, DOCKER_IMAGE); - KubernetesClientCatalogWatchNamespacesDelegate.testCatalogWatchWithEndpointsNamespaces(); + + KubernetesClientCatalogWatchUtils.patchForEndpointsNamespaces(APP_NAME, NAMESPACE, DOCKER_IMAGE); + KubernetesClientCatalogWatchNamespacesDelegate.testCatalogWatchWithEndpointsNamespaces(APP_NAME); util.busybox(NAMESPACE_A, Phase.CREATE); util.busybox(NAMESPACE_B, Phase.CREATE); - patchForEndpointSlicesNamespaces(APP_NAME, NAMESPACE, DOCKER_IMAGE); - KubernetesClientCatalogWatchNamespacesDelegate.testCatalogWatchWithEndpointSlicesNamespaces(); + KubernetesClientCatalogWatchUtils.patchForEndpointSlicesNamespaces(APP_NAME, NAMESPACE, DOCKER_IMAGE); + KubernetesClientCatalogWatchNamespacesDelegate.testCatalogWatchWithEndpointSlicesNamespaces(APP_NAME); } /** diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchNamespacesDelegate.java similarity index 94% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchNamespacesDelegate.java index 30ba34b249..66283f7199 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchNamespacesDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; import java.time.Duration; import java.util.Arrays; @@ -47,8 +47,6 @@ private KubernetesClientCatalogWatchNamespacesDelegate() { } - private static final String APP_NAME = "spring-cloud-kubernetes-client-catalog-watcher"; - private static final String NAMESPACE_A = "namespacea"; private static final String NAMESPACE_B = "namespaceb"; @@ -67,13 +65,13 @@ private KubernetesClientCatalogWatchNamespacesDelegate() { * - assert that we receive only spring-cloud-kubernetes-client-catalog-watcher pod *
*/ - static void testCatalogWatchWithEndpointsNamespaces() { - waitForLogStatement("stateGenerator is of type: KubernetesEndpointsCatalogWatch", K3S, APP_NAME); + static void testCatalogWatchWithEndpointsNamespaces(String deploymentName) { + waitForLogStatement("stateGenerator is of type: KubernetesEndpointsCatalogWatch", K3S, deploymentName); testForNamespacesFilter(); } - static void testCatalogWatchWithEndpointSlicesNamespaces() { - waitForLogStatement("stateGenerator is of type: KubernetesEndpointSlicesCatalogWatch", K3S, APP_NAME); + static void testCatalogWatchWithEndpointSlicesNamespaces(String deploymentName) { + waitForLogStatement("stateGenerator is of type: KubernetesEndpointSlicesCatalogWatch", K3S, deploymentName); testForNamespacesFilter(); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchUtils.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchUtils.java similarity index 87% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchUtils.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchUtils.java index d5a2637496..083b302495 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchUtils.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchUtils.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.catalog; +package org.springframework.cloud.kubernetes.k8s.client.catalog.watcher; + +import java.util.Map; import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; @@ -23,6 +25,9 @@ */ final class KubernetesClientCatalogWatchUtils { + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-k8s-client-catalog-watcher"); + private KubernetesClientCatalogWatchUtils() { } @@ -33,7 +38,7 @@ private KubernetesClientCatalogWatchUtils() { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-catalog-watcher", + "name": "spring-cloud-kubernetes-k8s-client-catalog-watcher", "image": "image_name_here", "env": [ { @@ -58,7 +63,7 @@ private KubernetesClientCatalogWatchUtils() { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-catalog-watcher", + "name": "spring-cloud-kubernetes-k8s-client-catalog-watcher", "image": "image_name_here", "env": [ { @@ -91,7 +96,7 @@ private KubernetesClientCatalogWatchUtils() { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-catalog-watcher", + "name": "spring-cloud-kubernetes-k8s-client-catalog-watcher", "image": "image_name_here", "env": [ { @@ -119,15 +124,15 @@ private KubernetesClientCatalogWatchUtils() { """; static void patchForEndpointSlices(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_ONE); + patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); } static void patchForEndpointsNamespaces(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_TWO); + patchWithReplace(imageName, deploymentName, namespace, BODY_TWO, POD_LABELS); } static void patchForEndpointSlicesNamespaces(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_THREE); + patchWithReplace(imageName, deploymentName, namespace, BODY_THREE, POD_LABELS); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml similarity index 69% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml index e0b9b534ae..04bed12ef2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-client-catalog-watcher + name: spring-cloud-kubernetes-k8s-client-catalog-watcher spec: selector: matchLabels: - app: spring-cloud-kubernetes-client-catalog-watcher + app: spring-cloud-kubernetes-k8s-client-catalog-watcher template: metadata: labels: - app: spring-cloud-kubernetes-client-catalog-watcher + app: spring-cloud-kubernetes-k8s-client-catalog-watcher spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-client-catalog-watcher - image: docker.io/springcloud/spring-cloud-kubernetes-client-catalog-watcher + - name: spring-cloud-kubernetes-k8s-client-catalog-watcher + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-catalog-watcher imagePullPolicy: IfNotPresent readinessProbe: httpGet: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml similarity index 66% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml index 5bb49e9734..1c07b37eda 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: spring-cloud-kubernetes-client-discovery-ingress-it + name: spring-cloud-kubernetes-k8s-client-catalog-watcher namespace: default spec: rules: @@ -11,6 +11,6 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-client-discovery-it + name: spring-cloud-kubernetes-k8s-client-catalog-watcher port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-service.yaml new file mode 100644 index 0000000000..a3e99b6637 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/app/watcher-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-k8s-client-catalog-watcher + name: spring-cloud-kubernetes-k8s-client-catalog-watcher +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-k8s-client-catalog-watcher + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml similarity index 64% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml index da8fed84e1..451cdc46b9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml @@ -5,11 +5,11 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 - spring-cloud-kubernetes-configuration-watcher-it + spring-cloud-kubernetes-k8s-client-configuration-watcher jar @@ -67,65 +67,29 @@ true - - - org.springframework.boot spring-boot-maven-plugin - docker.io/springcloud/${project.artifactId}:${project.version} + true build-image - ${skip.build.image} + true - package - - build-image - repackage - package - - repackage - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - + + true + - - - ${testsToRun} - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java similarity index 78% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java index 7f1af69e45..5eea8aa0a7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java @@ -16,16 +16,13 @@ package org.springframework.cloud.kubernetes.configuration.watcher; +import java.net.SocketException; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; import com.github.tomakehurst.wiremock.client.WireMock; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; import io.kubernetes.client.openapi.models.V1Service; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -57,6 +54,9 @@ class ActuatorRefreshIT { private static final String NAMESPACE = "default"; + private static final String DOCKER_IMAGE = "docker.io/springcloud/" + SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME + ":" + + Commons.pomVersion(); + private static final K3sContainer K3S = Commons.container(); private static Util util; @@ -68,22 +68,25 @@ static void beforeAll() throws Exception { Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); util = new Util(K3S); util.setUp(NAMESPACE); + + configWatcher(Phase.CREATE); } @AfterAll static void afterAll() throws Exception { + configWatcher(Phase.DELETE); Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); Commons.systemPrune(); } @BeforeEach void setup() { - util.wiremock(NAMESPACE, "/", Phase.CREATE); + util.wiremock(NAMESPACE, WIREMOCK_PATH, Phase.CREATE); } @AfterEach void after() { - util.wiremock(NAMESPACE, "/", Phase.DELETE); + util.wiremock(NAMESPACE, WIREMOCK_PATH, Phase.DELETE); } /* @@ -96,36 +99,34 @@ void after() { // curl :8080/__admin/mappings @Test void testActuatorRefresh() { - configWatcher(Phase.CREATE, false); WireMock.configureFor(WIREMOCK_HOST, WIREMOCK_PORT, WIREMOCK_PATH); - await().timeout(Duration.ofSeconds(60)) + await().timeout(Duration.ofSeconds(60)).ignoreException(SocketException.class) .until(() -> WireMock .stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh")) .willReturn(WireMock.aResponse().withBody("{}").withStatus(200))) .getResponse().wasConfigured()); - // Create new configmap to trigger controller to signal app to refresh - V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName("service-wiremock") - .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "bar").build(); - util.createAndWait(NAMESPACE, configMap, null); + createConfigMap(); // Wait a bit before we verify await().atMost(Duration.ofSeconds(30)).until( () -> !WireMock.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))).isEmpty()); - WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))); - util.deleteAndWait(NAMESPACE, configMap, null); - configWatcher(Phase.DELETE, false); + deleteConfigMap(); + + // the other test + testActuatorRefreshReloadDisabled(); + } /* * same test as above, but reload is disabled. */ - @Test void testActuatorRefreshReloadDisabled() { - configWatcher(Phase.CREATE, true); + + TestUtil.patchForDisabledReload(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, DOCKER_IMAGE); WireMock.configureFor(WIREMOCK_HOST, WIREMOCK_PORT, WIREMOCK_PATH); await().timeout(Duration.ofSeconds(60)) @@ -134,50 +135,29 @@ void testActuatorRefreshReloadDisabled() { .willReturn(WireMock.aResponse().withBody("{}").withStatus(200))) .getResponse().wasConfigured()); - // Create new configmap to trigger controller to signal app to refresh - V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName("service-wiremock") - .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "bar").build(); - util.createAndWait(NAMESPACE, configMap, null); + createConfigMap(); // Wait a bit before we verify await().atMost(Duration.ofSeconds(30)).until( () -> !WireMock.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))).isEmpty()); - Assertions.assertTrue(logs().contains("creating NOOP strategy because reload is disabled")); + Commons.waitForLogStatement("creating NOOP strategy because reload is disabled", K3S, + SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME); + // nothing related to 'ConfigReloadUtil' is present in logs // this proves that once we disable reload everything still works Assertions.assertFalse(logs().contains("ConfigReloadUtil")); - WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))); - util.deleteAndWait(NAMESPACE, configMap, null); - configWatcher(Phase.DELETE, true); + deleteConfigMap(); + } - private void configWatcher(Phase phase, boolean disableReload) { + private static void configWatcher(Phase phase) { V1ConfigMap configMap = (V1ConfigMap) util .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); V1Deployment deployment = (V1Deployment) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml"); - - List envVars = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(new ArrayList<>())); - - V1EnvVar commonsDebug = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD").value("DEBUG"); - V1EnvVar watcherDebug = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER").value("DEBUG"); - - envVars.add(commonsDebug); - envVars.add(watcherDebug); - - if (disableReload) { - V1EnvVar disableReloadEnvVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_RELOAD_ENABLED").value("FALSE"); - envVars.add(disableReloadEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - } - + .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-deployment.yaml"); V1Service service = (V1Service) util .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml"); @@ -192,6 +172,19 @@ private void configWatcher(Phase phase, boolean disableReload) { } + // Create new configmap to trigger controller to signal app to refresh + private void createConfigMap() { + V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName("service-wiremock") + .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "bar").build(); + util.createAndWait(NAMESPACE, configMap, null); + } + + private void deleteConfigMap() { + V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName("service-wiremock") + .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata().addToData("foo", "bar").build(); + util.deleteAndWait(NAMESPACE, configMap, null); + } + private String logs() { try { String appPodName = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java index 108c35562b..c8ae889a26 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.configuration.watcher; +import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; @@ -32,9 +33,7 @@ import io.kubernetes.client.openapi.models.V1SecretBuilder; import io.kubernetes.client.openapi.models.V1Service; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; @@ -72,11 +71,15 @@ static void beforeAll() throws Exception { util = new Util(K3S); util.createNamespace(LEFT_NAMESPACE); util.createNamespace(RIGHT_NAMESPACE); + util.wiremock(DEFAULT_NAMESPACE, "/", Phase.CREATE); util.setUpClusterWide(DEFAULT_NAMESPACE, Set.of(DEFAULT_NAMESPACE, LEFT_NAMESPACE, RIGHT_NAMESPACE)); + configWatcher(Phase.CREATE); } @AfterAll static void afterAll() throws Exception { + configWatcher(Phase.DELETE); + util.wiremock(DEFAULT_NAMESPACE, "/", Phase.DELETE); util.deleteClusterWide(DEFAULT_NAMESPACE, Set.of(DEFAULT_NAMESPACE, LEFT_NAMESPACE, RIGHT_NAMESPACE)); util.deleteNamespace(LEFT_NAMESPACE); util.deleteNamespace(RIGHT_NAMESPACE); @@ -84,18 +87,6 @@ static void afterAll() throws Exception { Commons.systemPrune(); } - @BeforeEach - void setup() { - configWatcher(Phase.CREATE); - util.wiremock(DEFAULT_NAMESPACE, "/", Phase.CREATE); - } - - @AfterEach - void after() { - configWatcher(Phase.DELETE); - util.wiremock(DEFAULT_NAMESPACE, "/", Phase.DELETE); - } - /** *
 	 *     - deploy config-watcher in default namespace
@@ -139,8 +130,8 @@ void testConfigMapActuatorRefreshMultipleNamespaces() {
 				() -> !WireMock.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))).isEmpty());
 		WireMock.verify(WireMock.exactly(2), WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh")));
 
-		util.deleteAndWait(LEFT_NAMESPACE, leftConfigMap, null);
-		util.deleteAndWait(RIGHT_NAMESPACE, rightConfigMap, null);
+		testSecretActuatorRefreshMultipleNamespaces();
+
 	}
 
 	/**
@@ -153,10 +144,8 @@ void testConfigMapActuatorRefreshMultipleNamespaces() {
 	 *     - same as above for the secret-right.
 	 * 
*/ - @Test void testSecretActuatorRefreshMultipleNamespaces() { - WireMock.configureFor(WIREMOCK_HOST, WIREMOCK_PORT, WIREMOCK_PATH); - await().timeout(Duration.ofSeconds(60)) + await().timeout(Duration.ofSeconds(60)).ignoreException(SocketException.class) .until(() -> WireMock .stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh")) .willReturn(WireMock.aResponse().withBody("{}").withStatus(200))) @@ -186,18 +175,15 @@ void testSecretActuatorRefreshMultipleNamespaces() { await().atMost(Duration.ofSeconds(30)).until( () -> !WireMock.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))).isEmpty()); - WireMock.verify(WireMock.exactly(2), WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))); - - util.deleteAndWait(LEFT_NAMESPACE, null, leftSecret); - util.deleteAndWait(RIGHT_NAMESPACE, null, rightSecret); + WireMock.verify(WireMock.exactly(4), WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh"))); } - private void configWatcher(Phase phase) { + private static void configWatcher(Phase phase) { V1ConfigMap configMap = (V1ConfigMap) util .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); V1Deployment deployment = (V1Deployment) util - .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml"); + .yaml("config-watcher/spring-cloud-kubernetes-configuration-watcher-deployment.yaml"); List envVars = List.of( new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_RELOAD_NAMESPACES_0").value(LEFT_NAMESPACE), diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/TestUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/TestUtil.java new file mode 100644 index 0000000000..b9d77740a8 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/TestUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.configuration.watcher; + +import java.util.Map; + +import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; + +/** + * @author wind57 + */ +final class TestUtil { + + private TestUtil() { + + } + + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-configuration-watcher"); + + private static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-configuration-watcher", + "image": "image_name_here", + "env": [ + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_RELOAD_ENABLED", + "value": "FALSE" + } + ] + }] + } + } + } + } + """; + + static void patchForDisabledReload(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-configmap.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-deployment.yaml similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-deployment.yaml index 403db22b05..4d42d92b8d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-deployment.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-configuration-watcher-deployment + name: spring-cloud-kubernetes-configuration-watcher spec: selector: matchLabels: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-config-it/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml new file mode 100644 index 0000000000..012c6eea32 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml @@ -0,0 +1,53 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 3.1.1-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-k8s-client-discovery-server + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-webflux + test + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/HeartbeatListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/HeartbeatListener.java new file mode 100644 index 0000000000..099bd69bd9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/HeartbeatListener.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryclient.it; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.context.ApplicationListener; +import org.springframework.core.log.LogAccessor; +import org.springframework.stereotype.Component; + +/** + * @author wind57 + */ +@Component +class HeartbeatListener implements ApplicationListener { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(HeartbeatListener.class)); + + AtomicReference> state = new AtomicReference<>(List.of()); + + @Override + @SuppressWarnings("unchecked") + public void onApplicationEvent(HeartbeatEvent event) { + LOG.info("received heartbeat event"); + List state = (List) event.getValue(); + this.state.set(state); + LOG.info("state received : " + state); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java similarity index 71% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java index 6138c11e94..6f90bae995 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java @@ -22,6 +22,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -30,13 +32,17 @@ * @author Ryan Baxter */ @SpringBootApplication +@EnableScheduling @RestController -public class KubernetesDiscoveryClientApplicationIt { +class KubernetesDiscoveryClientApplicationIt { private final DiscoveryClient discoveryClient; - public KubernetesDiscoveryClientApplicationIt(DiscoveryClient discoveryClient) { + private final HeartbeatListener heartbeatListener; + + KubernetesDiscoveryClientApplicationIt(DiscoveryClient discoveryClient, HeartbeatListener heartbeatListener) { this.discoveryClient = discoveryClient; + this.heartbeatListener = heartbeatListener; } public static void main(String[] args) { @@ -44,13 +50,18 @@ public static void main(String[] args) { } @GetMapping("/services") - public List services() { + List services() { return discoveryClient.getServices(); } @GetMapping("/service/{serviceId}") - public List service(@PathVariable String serviceId) { + List service(@PathVariable String serviceId) { return discoveryClient.getInstances(serviceId); } + @GetMapping("/state") + List state() { + return heartbeatListener.state.get(); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceDelegate.java new file mode 100644 index 0000000000..b4b265eee6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientFilterNamespaceDelegate.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryclient.it; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Objects; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.boot.test.json.BasicJsonTester; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author mbialkowski1 + */ +final class DiscoveryClientFilterNamespaceDelegate { + + private DiscoveryClientFilterNamespaceDelegate() { + + } + + private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester( + DiscoveryClientFilterNamespaceDelegate.class); + + static void testNamespaceDiscoveryClient(K3sContainer container) { + testLoadBalancer(); + testHealth(); + testForHeartbeat(container); + } + + private static void testLoadBalancer() { + + WebClient.Builder builder = builder(); + WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build(); + String result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathArrayValue("$") + .contains("service-wiremock"); + + // ServiceInstance + WebClient serviceInstanceClient = builder + .baseUrl("http://localhost:80/discoveryclient-it/service/service-wiremock").build(); + String serviceInstances = serviceInstanceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(serviceInstances)).extractingJsonPathStringValue("$.[0].serviceId") + .isEqualTo("service-wiremock"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(serviceInstances)).extractingJsonPathStringValue("$.[0].namespace") + .isEqualTo("left"); + } + + private static void testHealth() { + WebClient.Builder builder = builder(); + WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build(); + + String health = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(health)) + .extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP"); + } + + private static void testForHeartbeat(K3sContainer container) { + + // 1. logs from discovery server + Commons.waitForLogStatement("using delay : 3000", container, "spring-cloud-kubernetes-discoveryserver"); + Commons.waitForLogStatement("received heartbeat event", container, "spring-cloud-kubernetes-discoveryserver"); + Commons.waitForLogStatement("state received :", container, "spring-cloud-kubernetes-discoveryserver"); + + // 2. logs from discovery client + Commons.waitForLogStatement("using delay : 3000", container, + "spring-cloud-kubernetes-k8s-client-discovery-server"); + Commons.waitForLogStatement("state received : ", container, + "spring-cloud-kubernetes-k8s-client-discovery-server"); + + // 3. heartbeat listener message + WebClient.Builder builder = builder(); + WebClient client = builder.baseUrl("http://localhost:80/discoveryclient-it/state").build(); + String result = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + Condition> wireMockService = new Condition<>( + map -> map.entrySet().stream().anyMatch(en -> en.getValue().contains("service-wiremock-deployment")), + ""); + + Condition> discoveryServerService = new Condition<>( + map -> map.entrySet().stream() + .anyMatch(en -> en.getValue().contains("spring-cloud-kubernetes-k8s-client-discovery-server")), + ""); + + Assertions.assertThat(BASIC_JSON_TESTER.from(result)) + .>extractingJsonPathArrayValue("$.[*]").areAtLeastOne(wireMockService); + + Assertions.assertThat(BASIC_JSON_TESTER.from(result)) + .>extractingJsonPathArrayValue("$.[*]") + .areAtLeastOne(discoveryServerService); + } + + private static WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private static RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java new file mode 100644 index 0000000000..be2e90a71d --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java @@ -0,0 +1,304 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discoveryclient.it; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api; +import io.kubernetes.client.openapi.models.V1ClusterRoleBinding; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1Ingress; +import io.kubernetes.client.openapi.models.V1Service; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.boot.test.json.BasicJsonTester; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.cloud.kubernetes.discoveryclient.it.DiscoveryClientFilterNamespaceDelegate.testNamespaceDiscoveryClient; +import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; + +/** + * @author Ryan Baxter + */ +class DiscoveryClientIT { + + private static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-k8s-client-discovery-server", + "image": "image_name_here", + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", + "value": "left" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY", + "value": "3000" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERY", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_TWO = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-k8s-client-discovery-server", + "image": "image_name_here", + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", + "value": "TRUE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY", + "value": "3000" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-k8s-client-discovery-server"); + + private static final Map POD_LABELS_DISCOVERY = Map.of("app", + "spring-cloud-kubernetes-discoveryserver"); + + private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(DiscoveryClientIT.class); + + private static final String DISCOVERY_SERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver"; + + private static final String SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME = "spring-cloud-kubernetes-k8s-client-discovery-server"; + + private static final String NAMESPACE = "default"; + + private static final String NAMESPACE_LEFT = "left"; + + private static final String NAMESPACE_RIGHT = "right"; + + private static final K3sContainer K3S = Commons.container(); + + private static Util util; + + private static RbacAuthorizationV1Api rbacApi; + + private static V1ClusterRoleBinding clusterRoleBinding; + + @BeforeAll + static void beforeAll() throws Exception { + K3S.start(); + + Commons.validateImage(DISCOVERY_SERVER_APP_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(DISCOVERY_SERVER_APP_NAME, K3S); + + Commons.validateImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); + + util = new Util(K3S); + rbacApi = new RbacAuthorizationV1Api(); + util.setUp(NAMESPACE); + + util.createNamespace(NAMESPACE_LEFT); + util.createNamespace(NAMESPACE_RIGHT); + + clusterRoleBinding = (V1ClusterRoleBinding) util + .yaml("namespace-filter/cluster-admin-serviceaccount-role.yaml"); + rbacApi.createClusterRoleBinding(clusterRoleBinding, null, null, null, null); + + util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.CREATE, false); + util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.CREATE, false); + + discoveryServer(Phase.CREATE); + + } + + @AfterAll + static void afterAll() throws Exception { + rbacApi.deleteClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null, null, null, null, null, + null); + Commons.cleanUp(DISCOVERY_SERVER_APP_NAME, K3S); + Commons.cleanUp(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S); + + util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.DELETE, false); + util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.DELETE, false); + + util.deleteNamespace(NAMESPACE_LEFT); + util.deleteNamespace(NAMESPACE_RIGHT); + + discoveryServer(Phase.DELETE); + discoveryClient(Phase.DELETE); + Commons.systemPrune(); + } + + @Test + void testDiscoveryClient() { + discoveryClient(Phase.CREATE); + testLoadBalancer(); + testHealth(); + + patchForAllNamespaces("docker.io/springcloud/spring-cloud-kubernetes-discoveryserver:" + Commons.pomVersion(), + "spring-cloud-kubernetes-discoveryserver-deployment", NAMESPACE); + patchForNamespaceFilter( + "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery-server:" + Commons.pomVersion(), + "spring-cloud-kubernetes-k8s-client-discovery-server-deployment", NAMESPACE); + testNamespaceDiscoveryClient(K3S); + } + + private void testLoadBalancer() { + WebClient.Builder builder = builder(); + WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build(); + + String result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathArrayValue("$") + .contains("spring-cloud-kubernetes-discoveryserver"); + + // since 'spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled' + // is false by default, we will not receive any heartbeat events, + // simply because there are no beans registered to provide that to us. + // We assert this by doing a call to our internal /state + // endpoint, waiting 10 seconds and doing it again. Since the watch delay is set + // to 3 seconds, if there would be proper events, + // we would get a result that is different from '[]'. + + WebClient.Builder stateBuilder = builder(); + WebClient client = stateBuilder.baseUrl("http://localhost:80/discoveryclient-it/state").build(); + String stateResult = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + Assertions.assertThat(BASIC_JSON_TESTER.from(stateResult)).isEqualTo("[]"); + + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(10)); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + + String stateResultAfter10Seconds = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + Assertions.assertThat(BASIC_JSON_TESTER.from(stateResultAfter10Seconds)).isEqualTo("[]"); + } + + void testHealth() { + WebClient.Builder clientBuilder = builder(); + WebClient.Builder serverBuilder = builder(); + + WebClient client = clientBuilder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build(); + WebClient server = serverBuilder.baseUrl("http://localhost:80/actuator/health").build(); + + String clientHealth = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + String serverHealth = server.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(clientHealth)) + .extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(serverHealth)) + .extractingJsonPathStringValue("$.components.kubernetes.status").isEqualTo("UP"); + } + + private static void discoveryClient(Phase phase) { + V1Deployment deployment = (V1Deployment) util + .yaml("client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml"); + V1Service service = (V1Service) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-service.yaml"); + V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + } + else { + util.deleteAndWait(NAMESPACE, deployment, service, ingress); + } + } + + private static void discoveryServer(Phase phase) { + V1Deployment deployment = (V1Deployment) util + .yaml("server/spring-cloud-kubernetes-discoveryserver-deployment.yaml"); + V1Service service = (V1Service) util.yaml("server/spring-cloud-kubernetes-discoveryserver-service.yaml"); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(NAMESPACE, null, deployment, service, null, true); + } + else { + util.deleteAndWait(NAMESPACE, deployment, service, null); + } + } + + private WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + + static void patchForNamespaceFilter(String image, String deploymentName, String namespace) { + patchWithReplace(image, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + + static void patchForAllNamespaces(String image, String deploymentName, String namespace) { + patchWithReplace(image, deploymentName, namespace, BODY_TWO, POD_LABELS_DISCOVERY); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml similarity index 61% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml index 3ae56bfbce..21ce9fd42b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-discoveryclient-it-deployment + name: spring-cloud-kubernetes-k8s-client-discovery-server-deployment spec: selector: matchLabels: - app: spring-cloud-kubernetes-discoveryclient-it + app: spring-cloud-kubernetes-k8s-client-discovery-server template: metadata: labels: - app: spring-cloud-kubernetes-discoveryclient-it + app: spring-cloud-kubernetes-k8s-client-discovery-server spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-discoveryclient-it - image: docker.io/springcloud/spring-cloud-kubernetes-discoveryclient-it + - name: spring-cloud-kubernetes-k8s-client-discovery-server + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery-server imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -30,5 +30,8 @@ spec: initialDelaySeconds: 10 periodSeconds: 2 failureThreshold: 3 + env: + - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY + value: "3000" ports: - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml new file mode 100644 index 0000000000..306edf2ba0 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/client/spring-cloud-kubernetes-discoveryclient-it-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-k8s-client-discovery-server + name: spring-cloud-kubernetes-k8s-client-discovery-server +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-k8s-client-discovery-server + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/ingress.yaml similarity index 55% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/ingress.yaml index d820d54e94..9635d979b7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/ingress.yaml @@ -1,12 +1,20 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: discoveryserver-ingress + name: it-ingress namespace: default spec: rules: - http: paths: + - path: /discoveryclient-it + pathType: Prefix + backend: + service: + name: spring-cloud-kubernetes-k8s-client-discovery-server + port: + number: 8080 + - path: / pathType: Prefix backend: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/namespace-filter/cluster-admin-serviceaccount-role.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/namespace-filter/cluster-admin-serviceaccount-role.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/namespace-filter/cluster-admin-serviceaccount-role.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/namespace-filter/cluster-admin-serviceaccount-role.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml similarity index 66% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml index 3672520dbd..522f92bae6 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-deployment.yaml @@ -24,5 +24,14 @@ spec: httpGet: port: 8761 path: /actuator/health/liveness + env: + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER + value: "DEBUG" + - name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY + value: "3000" + - name: MANAGEMENT_ENDPOINT_HEALTH_SHOWCOMPONENTS + value: "ALWAYS" + - name: MANAGEMENT_ENDPOINT_HEALTH_SHOWDETAILS + value: "ALWAYS" ports: - containerPort: 8761 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoveryclient-it/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/src/test/resources/server/spring-cloud-kubernetes-discoveryserver-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml similarity index 50% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml index 6728433e66..a0b1e00040 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml @@ -3,16 +3,19 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps + spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 - jar - spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app + spring-cloud-kubernetes-k8s-client-discovery + + org.springframework.cloud + spring-cloud-kubernetes-client-discovery + org.springframework.boot spring-boot-starter-test @@ -21,18 +24,27 @@ org.springframework.boot spring-boot-starter-webflux + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.cloud spring-cloud-kubernetes-test-support - + + org.springframework.boot + spring-boot-starter-test + test + + - ../../src/main/resources + ../src/main/resources true @@ -40,40 +52,6 @@ true - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - - - - - - ${testsToRun} - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java index 3a430e5862..af9e0fdba2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java index 01903f24d3..37af2c7082 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import io.kubernetes.client.openapi.models.V1Pod; import org.apache.commons.logging.LogFactory; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java index d0ca41c436..189b24bdd0 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java index 59cd031b2c..bd79be6566 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.util.List; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java similarity index 73% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java index cfadb181fa..a844f47506 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.ArrayList; @@ -62,9 +62,9 @@ class KubernetesClientDiscoveryClientIT { private static final String NAMESPACE_B = "b"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-discovery-deployment-it"; + private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; private static final String NAMESPACE_A_UAT = "a-uat"; @@ -98,10 +98,20 @@ static void afterAll() throws Exception { */ @Test @Order(1) - void testSimple() { + void testSimple() throws Exception { util.busybox(NAMESPACE, Phase.CREATE); + // find both pods + String[] both = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=busybox -o=name --no-headers") + .getStdout().split("\n"); + // add a label to first pod + K3S.execInContainer("sh", "-c", + "kubectl label pods " + both[0].split("/")[1] + " custom-label=custom-label-value"); + // add annotation to the second pod + K3S.execInContainer("sh", "-c", + "kubectl annotate pods " + both[1].split("/")[1] + " custom-annotation=custom-annotation-value"); + Commons.waitForLogStatement("serviceSharedInformer will use namespace : default", K3S, IMAGE_NAME); WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); @@ -111,13 +121,14 @@ void testSimple() { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 3); + Assertions.assertEquals(servicesResult.size(), 4); Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); Assertions.assertTrue(servicesResult.contains("busybox-service")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); WebClient ourServiceClient = builder() - .baseUrl("http://localhost/service-instances/spring-cloud-kubernetes-client-discovery-it").build(); + .baseUrl("http://localhost/service-instances/spring-cloud-kubernetes-k8s-client-discovery").build(); List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() .bodyToMono(new ParameterizedTypeReference>() { @@ -128,11 +139,11 @@ void testSimple() { DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0); Assertions.assertNotNull(serviceInstance.getInstanceId()); - Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-client-discovery-it"); + Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-k8s-client-discovery"); Assertions.assertNotNull(serviceInstance.getHost()); Assertions.assertEquals(serviceInstance.getMetadata(), - Map.of("app", "spring-cloud-kubernetes-client-discovery-it", "custom-spring-k8s", "spring-k8s", "http", - "8080", "k8s_namespace", "default", "type", "ClusterIP")); + Map.of("app", "spring-cloud-kubernetes-k8s-client-discovery", "custom-spring-k8s", "spring-k8s", + "port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP")); Assertions.assertEquals(serviceInstance.getPort(), 8080); Assertions.assertEquals(serviceInstance.getNamespace(), "default"); @@ -145,6 +156,26 @@ void testSimple() { Assertions.assertEquals(busyBoxServiceInstances.size(), 2); + DefaultKubernetesServiceInstance withCustomLabel = busyBoxServiceInstances.stream() + .filter(x -> x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0); + Assertions.assertEquals(withCustomLabel.getServiceId(), "busybox-service"); + Assertions.assertNotNull(withCustomLabel.getInstanceId()); + Assertions.assertNotNull(withCustomLabel.getHost()); + Assertions.assertEquals(withCustomLabel.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); + Assertions.assertTrue(withCustomLabel.podMetadata().get("labels").entrySet().stream() + .anyMatch(x -> x.getKey().equals("custom-label") && x.getValue().equals("custom-label-value"))); + + DefaultKubernetesServiceInstance withCustomAnnotation = busyBoxServiceInstances.stream() + .filter(x -> !x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0); + Assertions.assertEquals(withCustomAnnotation.getServiceId(), "busybox-service"); + Assertions.assertNotNull(withCustomAnnotation.getInstanceId()); + Assertions.assertNotNull(withCustomAnnotation.getHost()); + Assertions.assertEquals(withCustomAnnotation.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); + Assertions.assertTrue(withCustomAnnotation.podMetadata().get("annotations").entrySet().stream().anyMatch( + x -> x.getKey().equals("custom-annotation") && x.getValue().equals("custom-annotation-value"))); + // enforces this : // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 WebClient clientForNonExistentService = builder().baseUrl("http://localhost/service-instances/non-existent") @@ -165,6 +196,8 @@ void testSimple() { * - config server is enabled for all namespaces * - wiremock service is deployed in namespace-a * - busybox service is deployed in namespace-b + * - external-name-service is deployed in namespace "default" and such a service type is requested, + * thus found also. * * Our discovery searches in all namespaces, thus finds them both. * @@ -175,7 +208,7 @@ void testAllNamespaces() { util.createNamespace(NAMESPACE_A); util.createNamespace(NAMESPACE_B); util.setUpClusterWideClusterRoleBinding(NAMESPACE); - util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE, false); util.busybox(NAMESPACE_B, Phase.CREATE); KubernetesClientDiscoveryClientUtils.patchForAllNamespaces(DEPLOYMENT_NAME, NAMESPACE); @@ -187,11 +220,12 @@ void testAllNamespaces() { .bodyToMono(new ParameterizedTypeReference>() { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 7); + Assertions.assertEquals(servicesResult.size(), 8); Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); Assertions.assertTrue(servicesResult.contains("busybox-service")); Assertions.assertTrue(servicesResult.contains("service-wiremock")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); // enforces this : // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 @@ -204,6 +238,23 @@ void testAllNamespaces() { Assertions.assertEquals(resultForNonExistentService.size(), 0); + // test ExternalName fields + WebClient externalNameClient = builder().baseUrl("http://localhost/service-instances/external-name-service") + .build(); + List externalNameServices = externalNameClient.method(HttpMethod.GET) + .retrieve().bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + DefaultKubernetesServiceInstance externalNameService = externalNameServices.get(0); + Assertions.assertNotNull(externalNameService.getInstanceId()); + Assertions.assertEquals(externalNameService.getHost(), "spring.io"); + Assertions.assertEquals(externalNameService.getPort(), -1); + Assertions.assertEquals(externalNameService.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ExternalName")); + Assertions.assertFalse(externalNameService.isSecure()); + Assertions.assertEquals(externalNameService.getUri().toASCIIString(), "spring.io"); + Assertions.assertEquals(externalNameService.getScheme(), "http"); + // do not remove wiremock in namespace a, it is required in the next test util.busybox(NAMESPACE_B, Phase.DELETE); util.deleteClusterWideClusterRoleBinding(NAMESPACE); @@ -222,7 +273,7 @@ void testAllNamespaces() { @Order(3) void testSpecificNamespace() { util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE, false); KubernetesClientDiscoveryClientUtils.patchForSingleNamespace(DEPLOYMENT_NAME, NAMESPACE); @@ -266,8 +317,8 @@ void testSpecificNamespace() { Assertions.assertEquals(resultForNonExistentService.size(), 0); - util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE, false); util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); util.deleteNamespace(NAMESPACE_A); util.deleteNamespace(NAMESPACE_B); @@ -277,7 +328,7 @@ void testSpecificNamespace() { @Order(4) void testSimplePodMetadata() { util.setUp(NAMESPACE); - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it:" + Commons.pomVersion(); + String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); KubernetesClientDiscoveryClientUtils.patchForPodMetadata(imageName, DEPLOYMENT_NAME, NAMESPACE); new KubernetesClientDiscoveryPodMetadataITDelegate().testSimple(); } @@ -285,7 +336,7 @@ void testSimplePodMetadata() { @Test @Order(5) void filterMatchesOneNamespaceViaThePredicate() { - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it:" + Commons.pomVersion(); + String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); KubernetesClientDiscoveryClientUtils.patchForUATNamespacesTests(imageName, DEPLOYMENT_NAME, NAMESPACE); new KubernetesClientDiscoveryFilterITDelegate().filterMatchesOneNamespaceViaThePredicate(util); @@ -308,7 +359,7 @@ void filterMatchesBothNamespacesViaThePredicate() { // patch the deployment to change what namespaces are take into account KubernetesClientDiscoveryClientUtils.patchForTwoNamespacesMatchViaThePredicate(DEPLOYMENT_NAME, NAMESPACE); - new KubernetesClientDiscoveryFilterITDelegate().filterMatchesBothNamespacesViaThePredicate(util); + new KubernetesClientDiscoveryFilterITDelegate().filterMatchesBothNamespacesViaThePredicate(); } @Test @@ -318,7 +369,7 @@ void testBlockingConfiguration() { // filter tests are done, clean-up a bit to prepare everything for health tests deleteNamespacesAndWiremock(); - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it:" + Commons.pomVersion(); + String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); KubernetesClientDiscoveryClientUtils.patchForBlockingHealth(imageName, DEPLOYMENT_NAME, NAMESPACE); new KubernetesClientDiscoveryHealthITDelegate().testBlockingConfiguration(K3S); @@ -330,7 +381,7 @@ void testReactiveConfiguration() { KubernetesClientDiscoveryClientUtils.patchForReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - new KubernetesClientDiscoveryHealthITDelegate().testReactiveConfiguration(util, K3S); + new KubernetesClientDiscoveryHealthITDelegate().testReactiveConfiguration(K3S); } @Test @@ -339,12 +390,12 @@ void testDefaultConfiguration() { KubernetesClientDiscoveryClientUtils.patchForBlockingAndReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - new KubernetesClientDiscoveryHealthITDelegate().testDefaultConfiguration(util, K3S); + new KubernetesClientDiscoveryHealthITDelegate().testDefaultConfiguration(K3S); } private void deleteNamespacesAndWiremock() { - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE, false); util.deleteNamespace(NAMESPACE_A_UAT); util.deleteNamespace(NAMESPACE_B_UAT); } @@ -352,10 +403,12 @@ private void deleteNamespacesAndWiremock() { private static void manifests(Phase phase) { V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml"); V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml"); + V1Service externalNameService = (V1Service) util.yaml("external-name-service.yaml"); V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml"); if (phase.equals(Phase.DELETE)) { util.deleteAndWait(NAMESPACE, deployment, service, ingress); + util.deleteAndWait(NAMESPACE, null, externalNameService, null); return; } @@ -366,15 +419,27 @@ private static void manifests(Phase phase) { .orElse(List.of())); V1EnvVar debugLevel = new V1EnvVar() .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY").value("DEBUG"); + V1EnvVar commonsLevel = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY").value("DEBUG"); V1EnvVar debugLevelForClient = new V1EnvVar() .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT").value("DEBUG"); + V1EnvVar addLabels = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODLABELS") + .value("TRUE"); + + V1EnvVar addAnnotations = new V1EnvVar() + .name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODANNOTATIONS").value("TRUE"); + envVars.add(debugLevel); envVars.add(debugLevelForClient); + envVars.add(addLabels); + envVars.add(addAnnotations); + envVars.add(commonsLevel); deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + util.createAndWait(NAMESPACE, null, null, externalNameService, null, true); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientUtils.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java similarity index 80% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientUtils.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java index 0c92e83941..7e18bbe72e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java @@ -14,11 +14,9 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.log.LogAccessor; +import java.util.Map; import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithMerge; import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; @@ -28,8 +26,7 @@ */ final class KubernetesClientDiscoveryClientUtils { - private static final LogAccessor LOG = new LogAccessor( - LogFactory.getLog(KubernetesClientDiscoveryClientUtils.class)); + private static final Map POD_LABELS = Map.of("app", "spring-cloud-kubernetes-k8s-client-discovery"); // patch the filter so that it matches both namespaces private static final String BODY_ONE = """ @@ -38,7 +35,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [{ "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_FILTER", "value": "#root.metadata.namespace matches '^.*uat$'" @@ -56,7 +53,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY", @@ -99,7 +96,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [{ "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", "value": "TRUE" @@ -120,7 +117,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "image": "image_name_here", "env": [ { @@ -147,18 +144,24 @@ final class KubernetesClientDiscoveryClientUtils { } """; - // patch to include all namespaces + // patch to include all namespaces + external name services private static final String BODY_FIVE = """ { "spec": { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", - "env": [{ - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", - "value": "TRUE" - }] + "name": "spring-cloud-kubernetes-k8s-client-discovery", + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", + "value": "TRUE" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES", + "value": "TRUE" + } + ] }] } } @@ -173,7 +176,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", @@ -197,7 +200,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "image": "image_name_here", "env": [ { @@ -234,7 +237,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", @@ -258,7 +261,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", @@ -282,7 +285,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1", @@ -306,7 +309,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "env": [ { "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", @@ -326,7 +329,7 @@ final class KubernetesClientDiscoveryClientUtils { "template": { "spec": { "containers": [{ - "name": "spring-cloud-kubernetes-client-discovery", + "name": "spring-cloud-kubernetes-k8s-client-discovery", "image": "image_name_here", "env": [ { @@ -358,54 +361,55 @@ private KubernetesClientDiscoveryClientUtils() { } static void patchForTwoNamespacesMatchViaThePredicate(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_ONE); + patchWithMerge(deploymentName, namespace, BODY_ONE, POD_LABELS); } static void patchForReactiveHealth(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_TWO); + patchWithMerge(deploymentName, namespace, BODY_TWO, POD_LABELS); } static void patchForBlockingAndReactiveHealth(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_THREE); + patchWithMerge(deploymentName, namespace, BODY_THREE, POD_LABELS); } // notice the usage of 'PATCH_FORMAT_JSON_MERGE_PATCH' here, it will not merge // env variables static void patchForBlockingHealth(String image, String deploymentName, String namespace) { - patchWithReplace(image, deploymentName, namespace, BODY_FOUR); + patchWithReplace(image, deploymentName, namespace, BODY_FOUR, POD_LABELS); } // add SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES=TRUE + // and SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES=TRUE static void patchForAllNamespaces(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_FIVE); + patchWithMerge(deploymentName, namespace, BODY_FIVE, POD_LABELS); } static void patchForSingleNamespace(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_SIX); + patchWithMerge(deploymentName, namespace, BODY_SIX, POD_LABELS); } static void patchForPodMetadata(String imageName, String deploymentName, String namespace) { - patchWithReplace(imageName, deploymentName, namespace, BODY_SEVEN); + patchWithReplace(imageName, deploymentName, namespace, BODY_SEVEN, POD_LABELS); } static void patchForReactiveOnly(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_EIGHT); + patchWithMerge(deploymentName, namespace, BODY_EIGHT, POD_LABELS); } static void patchForBlockingAndReactive(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_NINE); + patchWithMerge(deploymentName, namespace, BODY_NINE, POD_LABELS); } static void patchForTwoNamespacesBlockingOnly(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_TEN); + patchWithMerge(deploymentName, namespace, BODY_TEN, POD_LABELS); } static void patchToAddBlockingSupport(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_ELEVEN); + patchWithMerge(deploymentName, namespace, BODY_ELEVEN, POD_LABELS); } static void patchForUATNamespacesTests(String image, String deploymentName, String namespace) { - patchWithReplace(image, deploymentName, namespace, BODY_TWELVE); + patchWithReplace(image, deploymentName, namespace, BODY_TWELVE, POD_LABELS); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryFilterITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java similarity index 90% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryFilterITDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java index 21e25911ea..5e6c6f4bc9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryFilterITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.Comparator; @@ -47,7 +47,7 @@ class KubernetesClientDiscoveryFilterITDelegate { private static final String NAMESPACE = "default"; - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-discovery-deployment-it"; + private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; void filterMatchesOneNamespaceViaThePredicate(Util util) { @@ -55,8 +55,8 @@ void filterMatchesOneNamespaceViaThePredicate(Util util) { util.createNamespace(NAMESPACE_A_UAT); util.createNamespace(NAMESPACE_B_UAT); util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A_UAT, NAMESPACE_B_UAT)); - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE, false); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE, false); WebClient clientServices = builder().baseUrl("http://localhost/services").build(); @@ -81,7 +81,7 @@ void filterMatchesOneNamespaceViaThePredicate(Util util) { Assertions.assertEquals(first.getPort(), 8080); Assertions.assertEquals(first.getNamespace(), "a-uat"); Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); } @@ -95,7 +95,7 @@ void filterMatchesOneNamespaceViaThePredicate(Util util) { * As such, both services are found via 'getInstances' call. * */ - void filterMatchesBothNamespacesViaThePredicate(Util util) { + void filterMatchesBothNamespacesViaThePredicate() { // patch the deployment to change what namespaces are take into account KubernetesClientDiscoveryClientUtils.patchForTwoNamespacesMatchViaThePredicate(DEPLOYMENT_NAME, NAMESPACE); @@ -125,7 +125,7 @@ void filterMatchesBothNamespacesViaThePredicate(Util util) { Assertions.assertEquals(first.getPort(), 8080); Assertions.assertEquals(first.getNamespace(), "a-uat"); Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); DefaultKubernetesServiceInstance second = sorted.get(1); Assertions.assertEquals(second.getServiceId(), "service-wiremock"); @@ -133,7 +133,7 @@ void filterMatchesBothNamespacesViaThePredicate(Util util) { Assertions.assertEquals(second.getPort(), 8080); Assertions.assertEquals(second.getNamespace(), "b-uat"); Assertions.assertEquals(second.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java similarity index 67% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthITDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java index e06b5df7d2..f38f2f2080 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.List; @@ -29,7 +29,6 @@ import reactor.util.retry.RetryBackoffSpec; import org.springframework.boot.test.json.BasicJsonTester; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -42,15 +41,17 @@ */ class KubernetesClientDiscoveryHealthITDelegate { + KubernetesClientDiscoveryHealthITDelegate() { + + } + private static final String REACTIVE_STATUS = "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status"; private static final String BLOCKING_STATUS = "$.components.discoveryComposite.components.discoveryClient.status"; private static final String NAMESPACE = "default"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; - - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-discovery-deployment-it"; + private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester( KubernetesClientDiscoveryHealthITDelegate.class); @@ -69,7 +70,7 @@ void testBlockingConfiguration(K3sContainer container) { assertLogStatement(container, "publishing InstanceRegisteredEvent"); assertLogStatement(container, "Discovery Client has been initialized"); assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); @@ -85,7 +86,8 @@ void testBlockingConfiguration(K3sContainer container) { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathArrayValue( "$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(REACTIVE_STATUS); @@ -99,7 +101,7 @@ void testBlockingConfiguration(K3sContainer container) { * We assert for logs and call '/health' endpoint to see that blocking discovery * client was initialized. */ - void testReactiveConfiguration(Util util, K3sContainer container) { + void testReactiveConfiguration(K3sContainer container) { KubernetesClientDiscoveryClientUtils.patchForReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); @@ -107,7 +109,7 @@ void testReactiveConfiguration(Util util, K3sContainer container) { assertLogStatement(container, "publishing InstanceRegisteredEvent"); assertLogStatement(container, "Discovery Client has been initialized"); assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); @@ -122,7 +124,8 @@ void testReactiveConfiguration(Util util, K3sContainer container) { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(BLOCKING_STATUS); @@ -134,7 +137,7 @@ void testReactiveConfiguration(Util util, K3sContainer container) { .bodyToMono(new ParameterizedTypeReference>() { }).retryWhen(retrySpec()).block(); - Assertions.assertThat(servicesResult).contains("spring-cloud-kubernetes-client-discovery-it"); + Assertions.assertThat(servicesResult).contains("spring-cloud-kubernetes-k8s-client-discovery"); Assertions.assertThat(servicesResult).contains("kubernetes"); } @@ -142,7 +145,7 @@ void testReactiveConfiguration(Util util, K3sContainer container) { /** * Both blocking and reactive are enabled. */ - void testDefaultConfiguration(Util util, K3sContainer container) { + void testDefaultConfiguration(K3sContainer container) { KubernetesClientDiscoveryClientUtils.patchForBlockingAndReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); @@ -150,12 +153,15 @@ void testDefaultConfiguration(Util util, K3sContainer container) { assertLogStatement(container, "publishing InstanceRegisteredEvent"); assertLogStatement(container, "Discovery Client has been initialized"); assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); + WebClient infoClient = builder().baseUrl("http://localhost/actuator/info").build(); String healthResult = healthClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) .retryWhen(retrySpec()).block(); + String infoResult = infoClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP"); @@ -167,7 +173,8 @@ void testDefaultConfiguration(Util util, K3sContainer container) { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathArrayValue( "$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status").isEqualTo("UP"); @@ -178,8 +185,65 @@ void testDefaultConfiguration(Util util, K3sContainer container) { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); + + // assert health/info also + assertHealth(healthResult); + assertInfo(infoResult); + } + + private void assertHealth(String healthResult) { + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.hostIp").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathBooleanValue("$.components.kubernetes.details.inside").isEqualTo(true); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.labels.app") + .isEqualTo("spring-cloud-kubernetes-k8s-client-discovery"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.namespace").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.nodeName").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.podIp").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.podName").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.kubernetes.details.serviceAccount").isNotEmpty(); + } + + private void assertInfo(String infoResult) { + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)).extractingJsonPathStringValue("$.kubernetes.hostIp") + .isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)).extractingJsonPathBooleanValue("$.kubernetes.inside") + .isEqualTo(true); + + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) + .extractingJsonPathStringValue("$.kubernetes.namespace").isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)).extractingJsonPathStringValue("$.kubernetes.nodeName") + .isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)).extractingJsonPathStringValue("$.kubernetes.podIp") + .isNotEmpty(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)).extractingJsonPathStringValue("$.kubernetes.podName") + .isNotEmpty(); + Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) + .extractingJsonPathStringValue("$.kubernetes.serviceAccount").isNotEmpty(); } private WebClient.Builder builder() { @@ -192,8 +256,10 @@ private RetryBackoffSpec retrySpec() { private void assertLogStatement(K3sContainer container, String message) { try { - String appPodName = container.execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); + String appPodName = container + .execInContainer("sh", "-c", + "kubectl get pods -l app=" + DEPLOYMENT_NAME + " -o=name --no-headers | tr -d '\n'") + .getStdout(); await().pollDelay(Duration.ofSeconds(4)).pollInterval(Duration.ofSeconds(1)).atMost(20, TimeUnit.SECONDS) .until(() -> { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java index 6d36632b5a..a996309828 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.Comparator; @@ -44,7 +44,7 @@ class KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate { private static final String REACTIVE_PUBLISH = "Will publish InstanceRegisteredEvent from reactive implementation"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; /** * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java similarity index 85% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataITDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java index 5bfebc4739..a67dc95c2a 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.List; @@ -50,12 +50,13 @@ void testSimple() { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 2); + Assertions.assertEquals(servicesResult.size(), 3); Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); WebClient ourServiceClient = builder() - .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-client-discovery-it").build(); + .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-k8s-client-discovery").build(); List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() .bodyToMono(new ParameterizedTypeReference>() { @@ -66,11 +67,11 @@ void testSimple() { DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0); Assertions.assertNotNull(serviceInstance.getInstanceId()); - Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-client-discovery-it"); + Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-k8s-client-discovery"); Assertions.assertNotNull(serviceInstance.getHost()); Assertions.assertEquals(serviceInstance.getMetadata(), - Map.of("http", "8080", "k8s_namespace", "default", "type", "ClusterIP", "label-app", - "spring-cloud-kubernetes-client-discovery-it", "annotation-custom-spring-k8s", "spring-k8s")); + Map.of("port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP", "label-app", + "spring-cloud-kubernetes-k8s-client-discovery", "annotation-custom-spring-k8s", "spring-k8s")); Assertions.assertEquals(serviceInstance.getPort(), 8080); Assertions.assertEquals(serviceInstance.getNamespace(), "default"); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySelectiveNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySelectiveNamespacesIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java index 406b273b5d..a8e51d5c1c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySelectiveNamespacesIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.discovery; import java.time.Duration; import java.util.ArrayList; @@ -65,9 +65,9 @@ class KubernetesClientDiscoverySelectiveNamespacesIT { private static final String NAMESPACE_B = "b"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-discovery-deployment-it"; + private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; private static Util util; @@ -84,9 +84,9 @@ static void beforeAll() throws Exception { util.createNamespace(NAMESPACE_A); util.createNamespace(NAMESPACE_B); util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); - util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE, false); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE, false); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE, false); manifests(Phase.CREATE); } @@ -94,9 +94,9 @@ static void beforeAll() throws Exception { static void afterAll() throws Exception { Commons.cleanUp(IMAGE_NAME, K3S); - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE, false); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE, false); util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); util.deleteNamespace(NAMESPACE_A); util.deleteNamespace(NAMESPACE_B); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/external-name-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/external-name-service.yaml new file mode 100644 index 0000000000..649d5da75c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/external-name-service.yaml @@ -0,0 +1,7 @@ +kind: Service +apiVersion: v1 +metadata: + name: external-name-service +spec: + type: ExternalName + externalName: spring.io diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml similarity index 70% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml index bb8eaa28e3..27045945fc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-client-discovery-deployment-it + name: spring-cloud-kubernetes-k8s-client-discovery spec: selector: matchLabels: - app: spring-cloud-kubernetes-client-discovery-it + app: spring-cloud-kubernetes-k8s-client-discovery template: metadata: labels: - app: spring-cloud-kubernetes-client-discovery-it + app: spring-cloud-kubernetes-k8s-client-discovery spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-client-discovery - image: docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it + - name: spring-cloud-kubernetes-k8s-client-discovery + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery imagePullPolicy: IfNotPresent readinessProbe: httpGet: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml similarity index 68% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml index d0b52aa4ce..27e0242960 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-simple-core/src/test/resources/simple-core-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: it-ingress + name: spring-cloud-kubernetes-k8s-client-discovery namespace: default spec: rules: @@ -11,6 +11,6 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-fabric8-client-simple-core + name: spring-cloud-kubernetes-k8s-client-discovery port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml similarity index 55% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml index 7b9592cf05..4f19d879c0 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml @@ -2,15 +2,15 @@ apiVersion: v1 kind: Service metadata: labels: - app: spring-cloud-kubernetes-client-discovery-it + app: spring-cloud-kubernetes-k8s-client-discovery annotations: custom-spring-k8s: spring-k8s - name: spring-cloud-kubernetes-client-discovery-it + name: spring-cloud-kubernetes-k8s-client-discovery spec: ports: - name: http port: 8080 targetPort: 8080 selector: - app: spring-cloud-kubernetes-client-discovery-it + app: spring-cloud-kubernetes-k8s-client-discovery type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml similarity index 54% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml index 695b8e3dc1..b9353b3788 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml @@ -3,14 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 jar - spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a + kafka-configmap-app-a @@ -39,38 +39,6 @@ true - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml similarity index 51% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml index 9645e7ccaa..bdbfc5d1a9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml @@ -3,15 +3,15 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.0-SNAPSHOT - ../../spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps + 3.1.1-SNAPSHOT + ../../spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps 4.0.0 jar - spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b + kafka-configmap-app-b @@ -40,38 +40,6 @@ true - - - - - org.springframework.boot - spring-boot-maven-plugin - - docker.io/springcloud/${project.artifactId}:${project.version} - - - - build-image - - ${skip.build.image} - - package - - build-image - - - - repackage - package - - repackage - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml similarity index 63% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml index bbc4fe6fbf..97f8d429c2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml @@ -3,27 +3,28 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 jar - spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b + kafka-configmap-test-app org.springframework.boot - spring-boot-starter-web + spring-boot-starter-test - org.springframework.cloud - spring-cloud-starter-bus-amqp + org.springframework.boot + spring-boot-starter-webflux + - org.springframework.boot - spring-boot-starter-actuator + org.springframework.cloud + spring-cloud-kubernetes-test-support @@ -31,7 +32,7 @@ - ../src/main/resources + ../../src/main/resources true @@ -39,39 +40,30 @@ true - + - org.springframework.boot spring-boot-maven-plugin - docker.io/springcloud/${project.artifactId}:${project.version} + true build-image - ${skip.build.image} + true - package - - build-image - repackage - package - - repackage - + + true + - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java similarity index 95% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java index 83f78479dc..57c94a5b2b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppsIT.java @@ -49,9 +49,9 @@ */ class ConfigurationWatcherMultipleAppsIT { - private static final String CONFIG_WATCHER_APP_A_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a"; + private static final String CONFIG_WATCHER_APP_A_IMAGE = "kafka-configmap-app-a"; - private static final String CONFIG_WATCHER_APP_B_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b"; + private static final String CONFIG_WATCHER_APP_B_IMAGE = "kafka-configmap-app-b"; private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; @@ -91,7 +91,6 @@ static void afterAll() throws Exception { @BeforeEach void setup() { - util.zookeeper(NAMESPACE, Phase.CREATE); util.kafka(NAMESPACE, Phase.CREATE); appA(Phase.CREATE); appB(Phase.CREATE); @@ -100,7 +99,6 @@ void setup() { @AfterEach void afterEach() { - util.zookeeper(NAMESPACE, Phase.DELETE); util.kafka(NAMESPACE, Phase.DELETE); appA(Phase.DELETE); appB(Phase.DELETE); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml similarity index 90% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml index 3211344e44..282c03a4f6 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-a/app-a-deployment.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - name: app-a - image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a + image: docker.io/springcloud/kafka-configmap-app-a imagePullPolicy: IfNotPresent env: - name: SPRING_PROFILES_ACTIVE diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-a/app-a-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-a/app-a-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-a/app-a-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-a/app-a-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml similarity index 90% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml index 0e0bb0e4ba..b7f1e35818 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-b/app-b-deployment.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - name: app-b - image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b + image: docker.io/springcloud/kafka-configmap-app-b imagePullPolicy: IfNotPresent env: - name: SPRING_PROFILES_ACTIVE diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-b/app-b-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-b/app-b-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/app-b/app-b-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/app-b/app-b-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml similarity index 55% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml index f35d82fca2..9fa753de36 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml @@ -5,16 +5,16 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 - spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps pom - spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a - spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b - spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app + kafka-configmap-app-a + kafka-configmap-app-b + kafka-configmap-test-app diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/pom.xml new file mode 100644 index 0000000000..62639c0277 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/pom.xml @@ -0,0 +1,52 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.1.1-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-k8s-client-loadbalancer + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-loadbalancer + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/KubernetesClientReactiveDiscoveryClientApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java similarity index 55% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/KubernetesClientReactiveDiscoveryClientApplicationIt.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java index ff3fc05602..e581d1f5dd 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/client/reactive/discovery/it/KubernetesClientReactiveDiscoveryClientApplicationIt.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java @@ -14,39 +14,42 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.reactive.discovery.it; +package org.springframework.cloud.kubernetes.k8s.client.loadbalancer; -import java.util.stream.Collectors; - -import reactor.core.publisher.Mono; +import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.http.HttpMethod; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; /** * @author Ryan Baxter */ + @SpringBootApplication @RestController -public class KubernetesClientReactiveDiscoveryClientApplicationIt { +class KubernetesClientLoadBalancerApplication { + + private static final String URL = "http://service-wiremock/__admin/mappings"; - private final KubernetesInformerReactiveDiscoveryClient discoveryClient; + private final WebClient.Builder client; - public KubernetesClientReactiveDiscoveryClientApplicationIt( - KubernetesInformerReactiveDiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; + KubernetesClientLoadBalancerApplication(WebClient.Builder client) { + this.client = client; } public static void main(String[] args) { - SpringApplication.run(KubernetesClientReactiveDiscoveryClientApplicationIt.class, args); + SpringApplication.run(KubernetesClientLoadBalancerApplication.class, args); } - @GetMapping("/services") - public Mono services() { - return discoveryClient.getServices().collect(Collectors.joining(",")); + @GetMapping("/loadbalancer-it/service") + @SuppressWarnings("unchecked") + Map greeting() { + return (Map) client.baseUrl(URL).build().method(HttpMethod.GET).retrieve().bodyToMono(Map.class) + .block(); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java new file mode 100644 index 0000000000..2c029aa38e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.loadbalancer; + +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +@Configuration +class KubernetesClientLoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java similarity index 59% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java index 3faf4d06c8..85374aaaa1 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java @@ -14,43 +14,69 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.loadbalancer.it; +package org.springframework.cloud.kubernetes.k8s.client.loadbalancer; -import java.time.Duration; import java.util.Map; -import java.util.Objects; import io.kubernetes.client.openapi.models.V1Deployment; import io.kubernetes.client.openapi.models.V1Ingress; import io.kubernetes.client.openapi.models.V1Service; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.testcontainers.k3s.K3sContainer; import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; +import org.springframework.boot.test.json.BasicJsonTester; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; +import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithMerge; + /** * @author Ryan Baxter */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class LoadBalancerIT { + private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(LoadBalancerIT.class); + + private static final String BODY_FOR_MERGE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-cloud-kubernetes-k8s-client-loadbalancer", + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE", + "value": "SERVICE" + } + ] + }] + } + } + } + } + """; + + private static final Map POD_LABELS = Map.of("app", + "spring-cloud-kubernetes-k8s-client-loadbalancer"); + private static final String SERVICE_URL = "http://localhost:80/loadbalancer-it/service"; - private static final String SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME = "spring-cloud-kubernetes-client-loadbalancer-it"; + private static final String SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME = "spring-cloud-kubernetes-k8s-client-loadbalancer"; private static final String NAMESPACE = "default"; @@ -65,36 +91,37 @@ static void beforeAll() throws Exception { Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, K3S); util = new Util(K3S); util.setUp(NAMESPACE); + loadbalancerIt(Phase.CREATE); } @AfterAll static void afterAll() throws Exception { + loadbalancerIt(Phase.DELETE); Commons.cleanUp(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, K3S); Commons.systemPrune(); } @BeforeEach void setup() { - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE, false); } @AfterEach void afterEach() { - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE, false); } @Test - void testLoadBalancerServiceMode() { - loadbalancerIt(false, Phase.CREATE); + @Order(1) + void testLoadBalancerPodMode() { testLoadBalancer(); - loadbalancerIt(false, Phase.DELETE); } @Test - void testLoadBalancerPodMode() { - loadbalancerIt(true, Phase.CREATE); + @Order(2) + void testLoadBalancerServiceMode() { + patchForServiceMode(); testLoadBalancer(); - loadbalancerIt(true, Phase.DELETE); } private void testLoadBalancer() { @@ -102,23 +129,17 @@ private void testLoadBalancer() { WebClient.Builder builder = builder(); WebClient serviceClient = builder.baseUrl(SERVICE_URL).build(); - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class); - @SuppressWarnings("unchecked") - Map result = (Map) serviceClient.method(HttpMethod.GET).retrieve() - .bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())).retryWhen(retrySpec()) - .block(); - - Assertions.assertTrue(result.containsKey("mappings")); - Assertions.assertTrue(result.containsKey("meta")); - + String result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).block(); + Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathArrayValue("$.mappings").isEmpty(); + Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathNumberValue("$.meta.total") + .isEqualTo(0); } - private void loadbalancerIt(boolean podBased, Phase phase) { - V1Deployment deployment = podBased - ? (V1Deployment) util.yaml("spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml") - : (V1Deployment) util.yaml("spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml"); - V1Service service = (V1Service) util.yaml("spring-cloud-kubernetes-client-loadbalancer-it-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml"); + private static void loadbalancerIt(Phase phase) { + V1Deployment deployment = (V1Deployment) util + .yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml"); + V1Service service = (V1Service) util.yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml"); + V1Ingress ingress = (V1Ingress) util.yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml"); if (phase.equals(Phase.CREATE)) { util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); @@ -132,8 +153,9 @@ private WebClient.Builder builder() { return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); } - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + private static void patchForServiceMode() { + patchWithMerge("spring-cloud-kubernetes-k8s-client-loadbalancer", LoadBalancerIT.NAMESPACE, BODY_FOR_MERGE, + POD_LABELS); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml similarity index 65% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml index 8b1401e6b5..a0aab26b34 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml @@ -1,23 +1,23 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-client-loadbalancer-it-deployment + name: spring-cloud-kubernetes-k8s-client-loadbalancer spec: selector: matchLabels: - app: spring-cloud-kubernetes-client-loadbalancer-it + app: spring-cloud-kubernetes-k8s-client-loadbalancer template: metadata: labels: - app: spring-cloud-kubernetes-client-loadbalancer-it + app: spring-cloud-kubernetes-k8s-client-loadbalancer spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-client-loadbalancer-it + - name: spring-cloud-kubernetes-k8s-client-loadbalancer env: - name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE value: POD - image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-loadbalancer imagePullPolicy: IfNotPresent readinessProbe: httpGet: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml similarity index 81% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml index 8e44f4e61e..35c5d8daa2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml @@ -11,7 +11,7 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-client-loadbalancer-it + name: spring-cloud-kubernetes-k8s-client-loadbalancer port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml new file mode 100644 index 0000000000..6fe86a0aae --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-k8s-client-loadbalancer + name: spring-cloud-kubernetes-k8s-client-loadbalancer +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-k8s-client-loadbalancer + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml similarity index 55% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml index cae614303f..305784ff5a 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml @@ -5,17 +5,17 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 - spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps pom - spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a - spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b - spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app + rabbitmq-secret-app-a + rabbitmq-secret-app-b + rabbitmq-secret-test-app diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml new file mode 100644 index 0000000000..87e4c32ee6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml @@ -0,0 +1,45 @@ + + + + spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps + org.springframework.cloud + 3.1.1-SNAPSHOT + + 4.0.0 + jar + + rabbitmq-secret-app-a + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appA/AppATestApplication.java diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml new file mode 100644 index 0000000000..bcdf8a62c4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml @@ -0,0 +1,45 @@ + + + + spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps + org.springframework.cloud + 3.1.1-SNAPSHOT + + 4.0.0 + jar + + rabbitmq-secret-app-b + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/appB/AppBTestApplication.java diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/src/main/resources/application.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/src/main/resources/application.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml similarity index 63% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/pom.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml index 4fc7972d38..cb971c7573 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml @@ -3,27 +3,28 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps + spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 jar - spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a + rabbitmq-secret-test-app org.springframework.boot - spring-boot-starter-web + spring-boot-starter-test - org.springframework.cloud - spring-cloud-starter-bus-amqp + org.springframework.boot + spring-boot-starter-webflux + - org.springframework.boot - spring-boot-starter-actuator + org.springframework.cloud + spring-cloud-kubernetes-test-support @@ -31,7 +32,7 @@ - ../src/main/resources + ../../src/main/resources true @@ -39,39 +40,33 @@ true - + - org.springframework.boot spring-boot-maven-plugin - docker.io/springcloud/${project.artifactId}:${project.version} + true build-image - ${skip.build.image} + true - package - - build-image - repackage - package - - repackage - + + true + - - + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java index 879816041d..6ebc617304 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/multiple/apps/ConfigurationWatcherMultipleAppIT.java @@ -49,9 +49,9 @@ */ class ConfigurationWatcherMultipleAppIT { - private static final String CONFIG_WATCHER_APP_A_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a"; + private static final String CONFIG_WATCHER_APP_A_IMAGE = "rabbitmq-secret-app-a"; - private static final String CONFIG_WATCHER_APP_B_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b"; + private static final String CONFIG_WATCHER_APP_B_IMAGE = "rabbitmq-secret-app-b"; private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-a/app-a-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-a/app-a-deployment.yaml similarity index 87% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-a/app-a-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-a/app-a-deployment.yaml index 6f76090f0f..54bfba1abb 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-a/app-a-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-a/app-a-deployment.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - name: app-a - image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a + image: docker.io/springcloud/rabbitmq-secret-app-a imagePullPolicy: IfNotPresent env: - name: SPRING_PROFILES_ACTIVE diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-a/app-a-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-a/app-a-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-a/app-a-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-a/app-a-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-b/app-b-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-b/app-b-deployment.yaml similarity index 87% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-b/app-b-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-b/app-b-deployment.yaml index 83c87a1995..3769c5b499 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-b/app-b-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-b/app-b-deployment.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - name: app-b - image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b + image: docker.io/springcloud/rabbitmq-secret-app-b imagePullPolicy: IfNotPresent env: - name: SPRING_PROFILES_ACTIVE diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-b/app-b-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-b/app-b-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/app-b/app-b-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/app-b/app-b-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml similarity index 97% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml index e4f95bc867..44e9af80b7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml @@ -24,7 +24,7 @@ spec: - name: SPRING_CLOUD_BUS_DESTINATION value: multiple-apps - name: SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_REFRESHDELAY - value: 1 + value: "1" readinessProbe: httpGet: port: 8888 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-reactive-discoveryclient-it/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml new file mode 100644 index 0000000000..79d6d31db9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.1.1-SNAPSHOT + + + spring-cloud-kubernetes-k8s-client-reload + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-config + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.springframework.cloud + spring-cloud-starter + + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/App.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java similarity index 86% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/App.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java index 8cb37a95e5..23d76328cf 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/App.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -24,7 +24,8 @@ * @author wind57 */ @SpringBootApplication -@EnableConfigurationProperties({ LeftProperties.class, RightProperties.class, RightWithLabelsProperties.class }) +@EnableConfigurationProperties({ LeftProperties.class, RightProperties.class, RightWithLabelsProperties.class, + ConfigMapProperties.class, SecretsProperties.class }) public class App { public static void main(String[] args) { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java index c4c11b438b..f6e65df380 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/client/configmap/polling/reload/ConfigMapProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.polling.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/Controller.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java similarity index 80% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/Controller.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java index ff3a8c851c..e2eb643534 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/Controller.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,11 +31,14 @@ public class Controller { private final RightWithLabelsProperties rightWithLabelsProperties; + private final ConfigMapProperties configMapProperties; + public Controller(LeftProperties leftProperties, RightProperties rightProperties, - RightWithLabelsProperties rightWithLabelsProperties) { + RightWithLabelsProperties rightWithLabelsProperties, ConfigMapProperties configMapProperties) { this.leftProperties = leftProperties; this.rightProperties = rightProperties; this.rightWithLabelsProperties = rightWithLabelsProperties; + this.configMapProperties = configMapProperties; } @GetMapping("/left") @@ -53,4 +56,9 @@ public String witLabel() { return rightWithLabelsProperties.getValue(); } + @GetMapping("/mount") + public String key() { + return configMapProperties.getKey(); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/LeftProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/LeftProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/LeftProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/LeftProperties.java index 4be53d9239..a7dcc3a4c3 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/LeftProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/LeftProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightProperties.java index f4e28d2923..5424a17aaf 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightWithLabelsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightWithLabelsProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightWithLabelsProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightWithLabelsProperties.java index e4a9c3409e..40d030bf2b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/event/reload/RightWithLabelsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/RightWithLabelsProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java index 7d40247bb6..b0e0597a80 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.secrets.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java index e48c7a83fb..43615d873f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.secrets.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-mount.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-one.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/main/resources/application-three.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/application-two.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application-with-bootstrap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml index ed832dbece..fb84489801 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml @@ -18,4 +18,3 @@ spring: secrets: enabled: true enable-api: true - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-one.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-three.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/main/resources/bootstrap-two.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/bootstrap-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/bootstrap-with-bootstrap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java new file mode 100644 index 0000000000..ae7d6dea7a --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; + +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +final class BootstrapEnabledPollingReloadConfigMapMountDelegate { + + private static final String NAMESPACE = "default"; + + /** + *
+	 *     - we have bootstrap enabled, which means we will 'locate' property sources
+	 *       from config maps.
+	 *     - there are no explicit config maps to search for, but what we will also read,
+	 *     	 is 'spring.cloud.kubernetes.config.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
+	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
+	 *
+	 *     - we then change the config map content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + static void testBootstrapEnabledPollingReloadConfigMapMount(String deploymentName, K3sContainer k3sContainer, + Util util, String imageName) throws Exception { + + recreateMountConfigMap(util); + K8sClientConfigMapReloadITUtil.patchSix(deploymentName, "default", imageName); + + // (1) + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", k3sContainer, + deploymentName); + + // (2) + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", k3sContainer, + deploymentName); + + // (3) + WebClient webClient = K8sClientConfigMapReloadITUtil.builder().baseUrl("http://localhost/mount").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block(); + + // we first read the initial value from the configmap + Assertions.assertEquals("as-mount-initial", result); + + // replace data in configmap and wait for k8s to pick it up + // our polling will detect that and restart the app + V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); + configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); + new CoreV1Api().replaceNamespacedConfigMap("poll-reload-as-mount", NAMESPACE, configMap, null, null, null, + null); + + await().timeout(Duration.ofSeconds(180)) + .until(() -> webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block().equals("as-mount-changed")); + + } + + private static void recreateMountConfigMap(Util util) { + V1ConfigMap mountConfigMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); + + util.deleteAndWait("default", mountConfigMap, null); + util.createAndWait("default", mountConfigMap, null); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java new file mode 100644 index 0000000000..7f7dddc37f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; + +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +final class DataChangesInConfigMapReloadDelegate { + + private static final String NAMESPACE = "default"; + + private static final String LEFT_NAMESPACE = "left"; + + /** + *
+	 *     - configMap with no labels and data: left.value = left-initial exists in namespace left
+	 *     - we assert that we can read it correctly first, by invoking localhost/left
+	 *
+	 *     - then we change the configmap by adding a label, this in turn does not
+	 *       change the result of localhost/left, because the data has not changed.
+	 *
+	 *     - then we change data inside the config map, and we must see the updated value
+	 * 
+ */ + static void testSimple(String dockerImage, String deploymentName, K3sContainer k3sContainer) { + + K8sClientConfigMapReloadITUtil.patchFour(deploymentName, NAMESPACE, dockerImage); + Commons.assertReloadLogStatements("added configmap informer for namespace", + "added secret informer for namespace", deploymentName); + + WebClient webClient = K8sClientConfigMapReloadITUtil.builder().baseUrl("http://localhost/" + LEFT_NAMESPACE) + .build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block(); + + // we first read the initial value from the left-configmap + Assertions.assertEquals("left-initial", result); + + // then deploy a new version of left-configmap, but without changing its data, + // only add a label + V1ConfigMap configMap = new V1ConfigMapBuilder().withMetadata(new V1ObjectMetaBuilder() + .withLabels(Map.of("new-label", "abc")).withNamespace("left").withName("left-configmap").build()) + .withData(Map.of("left.value", "left-initial")).build(); + + replaceConfigMap(configMap); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = K8sClientConfigMapReloadITUtil.builder() + .baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block(); + return "left-initial".equals(innerResult); + }); + + String logs = K8sClientConfigMapReloadITUtil.logs(deploymentName, k3sContainer); + Assertions.assertTrue(logs.contains("ConfigMap left-configmap was updated in namespace left")); + Assertions.assertTrue(logs.contains("data in configmap has not changed, will not reload")); + + // change data + configMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace("left") + .withName("left-configmap").build()) + .withData(Map.of("left.value", "left-after-change")).build(); + + replaceConfigMap(configMap); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = K8sClientConfigMapReloadITUtil.builder() + .baseUrl("http://localhost/" + LEFT_NAMESPACE).build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block(); + return "left-after-change".equals(innerResult); + }); + + } + + private static void replaceConfigMap(V1ConfigMap configMap) { + try { + new CoreV1Api().replaceNamespacedConfigMap("left-configmap", LEFT_NAMESPACE, configMap, null, null, null, + null); + } + catch (ApiException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java similarity index 77% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java index ffa58a0d78..b55ad11bd6 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java @@ -14,14 +14,10 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.configmap.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; @@ -31,7 +27,6 @@ import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; import io.kubernetes.client.openapi.models.V1Ingress; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; @@ -40,25 +35,33 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.BootstrapEnabledPollingReloadConfigMapMountDelegate.testBootstrapEnabledPollingReloadConfigMapMount; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.DataChangesInConfigMapReloadDelegate.testSimple; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.builder; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchOne; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchThree; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchTwo; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.retrySpec; +import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.PollingReloadConfigMapMountDelegate.testPollingReloadConfigMapMount; /** * @author wind57 */ -class ConfigMapEventReloadIT { +class K8sClientConfigMapReloadIT { - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-configmap-event-reload"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; + + private static final String DEPLOYMENT_NAME = "spring-k8s-client-reload"; + + private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + Commons.pomVersion(); private static final String NAMESPACE = "default"; @@ -77,12 +80,14 @@ static void beforeAll() throws Exception { util.createNamespace("left"); util.createNamespace("right"); util.setUpClusterWide(NAMESPACE, Set.of("left", "right")); + util.setUp(NAMESPACE); api = new CoreV1Api(); } @AfterAll static void afterAll() throws Exception { util.deleteClusterWide(NAMESPACE, Set.of("left", "right")); + manifests(Phase.DELETE); util.deleteNamespace("left"); util.deleteNamespace("right"); Commons.cleanUp(IMAGE_NAME, K3S); @@ -99,9 +104,9 @@ static void afterAll() throws Exception { */ @Test void testInformFromOneNamespaceEventNotTriggered() throws Exception { - manifests("one", Phase.CREATE, false); + manifests(Phase.CREATE); Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); + "added secret informer for namespace", "spring-k8s-client-reload"); WebClient webClient = builder().baseUrl("http://localhost/left").build(); String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) @@ -130,7 +135,20 @@ void testInformFromOneNamespaceEventNotTriggered() throws Exception { // left configmap has not changed, no restart of app has happened Assertions.assertEquals("left-initial", result); - manifests("one", Phase.DELETE, false); + testAllOther(); + + } + + // since we patch each deployment with "replace" strategy, any of the above can be + // commented out and debugged individually. + private void testAllOther() throws Exception { + testInformFromOneNamespaceEventTriggered(); + testInform(); + testInformFromOneNamespaceEventTriggeredSecretsDisabled(); + testSimple(DOCKER_IMAGE, DEPLOYMENT_NAME, K3S); + testPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); + testBootstrapEnabledPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); + } /** @@ -141,11 +159,11 @@ void testInformFromOneNamespaceEventNotTriggered() throws Exception { * - as such, event is triggered and we see the updated value * */ - @Test void testInformFromOneNamespaceEventTriggered() throws Exception { - manifests("two", Phase.CREATE, false); + recreateConfigMaps(); + patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); + "added secret informer for namespace", DEPLOYMENT_NAME); // read the value from the right-configmap WebClient webClient = builder().baseUrl("http://localhost/right").build(); @@ -170,8 +188,6 @@ void testInformFromOneNamespaceEventTriggered() throws Exception { return innerResult != null; }); Assertions.assertEquals("right-after-change", resultAfterChange[0]); - - manifests("two", Phase.DELETE, false); } /** @@ -183,11 +199,14 @@ void testInformFromOneNamespaceEventTriggered() throws Exception { * right-configmap-with-label triggers changes. * */ - @Test void testInform() throws Exception { - manifests("three", Phase.CREATE, false); + recreateConfigMaps(); + V1ConfigMap rightWithLabelConfigMap = (V1ConfigMap) util.yaml("right-configmap-with-label.yaml"); + util.createAndWait("right", rightWithLabelConfigMap, null); + patchTwo(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); + Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); + "added secret informer for namespace", DEPLOYMENT_NAME); // read the initial value from the right-configmap WebClient rightWebClient = builder().baseUrl("http://localhost/right").build(); @@ -240,8 +259,7 @@ void testInform() throws Exception { rightResult = rightWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) .block(); Assertions.assertEquals("right-after-change", rightResult); - - manifests("three", Phase.DELETE, false); + util.deleteAndWait("right", rightWithLabelConfigMap, null); } /** @@ -252,11 +270,11 @@ void testInform() throws Exception { * - as such, event is triggered and we see the updated value * */ - @Test void testInformFromOneNamespaceEventTriggeredSecretsDisabled() throws Exception { - manifests("two", Phase.CREATE, true); + recreateConfigMaps(); + patchThree(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", IMAGE_NAME); + "added secret informer for namespace", DEPLOYMENT_NAME); // read the value from the right-configmap WebClient webClient = builder().baseUrl("http://localhost/right").build(); @@ -281,49 +299,42 @@ void testInformFromOneNamespaceEventTriggeredSecretsDisabled() throws Exception return innerResult != null; }); Assertions.assertEquals("right-after-change", resultAfterChange[0]); + } + + private void recreateConfigMaps() { + V1ConfigMap leftConfigMap = (V1ConfigMap) util.yaml("left-configmap.yaml"); + V1ConfigMap rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); + + util.deleteAndWait("left", leftConfigMap, null); + util.deleteAndWait("right", rightConfigMap, null); - manifests("two", Phase.DELETE, true); + util.createAndWait("left", leftConfigMap, null); + util.createAndWait("right", rightConfigMap, null); } - private static void manifests(String deploymentRoot, Phase phase, boolean secretsDisabled) { + private static void manifests(Phase phase) { try { V1ConfigMap leftConfigMap = (V1ConfigMap) util.yaml("left-configmap.yaml"); V1ConfigMap rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); - V1ConfigMap rightWithLabelConfigMap = (V1ConfigMap) util.yaml("right-configmap-with-label.yaml"); + V1ConfigMap mountConfigMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - V1Deployment deployment = (V1Deployment) util.yaml(deploymentRoot + "/deployment.yaml"); + V1Deployment deployment = (V1Deployment) util.yaml("deployment.yaml"); V1Service service = (V1Service) util.yaml("service.yaml"); V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - List envVars = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(List.of())); - - if (secretsDisabled) { - V1EnvVar secretsDisabledEnvVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED") - .value("FALSE"); - envVars.add(secretsDisabledEnvVar); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - } - if (phase.equals(Phase.CREATE)) { + util.createAndWait(NAMESPACE, mountConfigMap, null); util.createAndWait("left", leftConfigMap, null); util.createAndWait("right", rightConfigMap, null); - - if ("three".equals(deploymentRoot)) { - util.createAndWait("right", rightWithLabelConfigMap, null); - } util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); } if (phase.equals(Phase.DELETE)) { + util.deleteAndWait(NAMESPACE, mountConfigMap, null); util.deleteAndWait("left", leftConfigMap, null); util.deleteAndWait("right", rightConfigMap, null); - if ("three".equals(deploymentRoot)) { - util.deleteAndWait("right", rightWithLabelConfigMap, null); - } util.deleteAndWait(NAMESPACE, deployment, service, ingress); } @@ -334,14 +345,6 @@ private static void manifests(String deploymentRoot, Phase phase, boolean secret } - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - private static void replaceConfigMap(V1ConfigMap configMap, String name) throws ApiException { api.replaceNamespacedConfigMap(name, "right", configMap, null, null, null, null); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java new file mode 100644 index 0000000000..9d50fd639c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java @@ -0,0 +1,453 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +import org.testcontainers.containers.Container; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; + +/** + * @author wind57 + */ +final class K8sClientConfigMapReloadITUtil { + + private static final Map POD_LABELS = Map.of("app", "spring-k8s-client-reload"); + + private K8sClientConfigMapReloadITUtil() { + } + + private static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "two" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_TWO = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "three" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_THREE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "two" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", + "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_FOUR = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "one" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", + "value": "FALSE" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_FIVE = """ + { + "spec": { + "template": { + "spec": { + "volumes": [ + { + "configMap": { + "defaultMode": 420, + "name": "poll-reload-as-mount" + }, + "name": "config-map-volume" + } + ], + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "volumeMounts": [ + { + "mountPath": "/tmp", + "name": "config-map-volume" + } + ], + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "mount" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "FALSE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", + "value": "DEBUG" + } + ] + }] + } + } + } + } + """; + + private static final String BODY_SIX = """ + { + "spec": { + "template": { + "spec": { + "volumes": [ + { + "configMap": { + "defaultMode": 420, + "name": "poll-reload-as-mount" + }, + "name": "config-map-volume" + } + ], + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "volumeMounts": [ + { + "mountPath": "/tmp", + "name": "config-map-volume" + } + ], + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "with-bootstrap" + }, + { + "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", + "value": "TRUE" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", + "value": "DEBUG" + }, + { + "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", + "value": "DEBUG" + } + ] + }] + } + } + } + } + """; + + static void patchOne(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + + static void patchTwo(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_TWO, POD_LABELS); + } + + static void patchThree(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_THREE, POD_LABELS); + } + + static void patchFour(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_FOUR, POD_LABELS); + } + + static void patchFive(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_FIVE, POD_LABELS); + } + + static void patchSix(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_SIX, POD_LABELS); + } + + static WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + static RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(120, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + + static String logs(String appLabelValue, K3sContainer k3sContainer) { + try { + String appPodName = k3sContainer + .execInContainer("sh", "-c", + "kubectl get pods -l app=" + appLabelValue + " -o=name --no-headers | tr -d '\n'") + .getStdout(); + + Container.ExecResult execResult = k3sContainer.execInContainer("sh", "-c", + "kubectl logs " + appPodName.trim()); + return execResult.getStdout(); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java new file mode 100644 index 0000000000..2c9db477d4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; + +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +final class PollingReloadConfigMapMountDelegate { + + private PollingReloadConfigMapMountDelegate() { + + } + + /** + *
+	 *     - we have "spring.config.import: kubernetes", which means we will 'locate' property sources
+	 *       from config maps.
+	 *     - the property above means that at the moment we will be searching for config maps that only
+	 *       match the application name, in this specific test there is no such config map.
+	 *     - what we will also read, is 'spring.cloud.kubernetes.config.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see BODY_FIVE)
+	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
+	 *
+	 *     - we then change the config map content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + static void testPollingReloadConfigMapMount(String deploymentName, K3sContainer k3sContainer, Util util, + String imageName) throws Exception { + + K8sClientConfigMapReloadITUtil.patchFive(deploymentName, "default", imageName); + + // (1) + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", k3sContainer, + deploymentName); + + // (2) + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", k3sContainer, + deploymentName); + + // (3) + WebClient webClient = K8sClientConfigMapReloadITUtil.builder().baseUrl("http://localhost/mount").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block(); + + // we first read the initial value from the configmap + Assertions.assertEquals("as-mount-initial", result); + + // replace data in configmap and wait for k8s to pick it up + // our polling will detect that and restart the app + V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); + configMap.setData(Map.of("application.properties", "from.properties.key=as-mount-changed")); + new CoreV1Api().replaceNamespacedConfigMap("poll-reload-as-mount", "default", configMap, null, null, null, + null); + + await().timeout(Duration.ofSeconds(180)) + .until(() -> webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()).block().equals("as-mount-changed")); + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java new file mode 100644 index 0000000000..bb100878b9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.secret; + +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.builder; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.retrySpec; + +final class DataChangesInSecretsReloadDelegate { + + private static final String NAMESPACE = "default"; + + /** + *
+	 *     - secret with no labels and data: from.properties.key = initial exists in namespace default
+	 *     - we assert that we can read it correctly first, by invoking localhost/key.
+	 *
+	 *     - then we change the secret by adding a label, this in turn does not
+	 *       change the result of localhost/key, because the data has not changed.
+	 *
+	 *     - then we change data inside the secret, and we must see the updated value.
+	 * 
+ */ + static void testDataChangesInSecretsReload(K3sContainer k3sContainer, String deploymentName) { + Commons.assertReloadLogStatements("added secret informer for namespace", + "added configmap informer for namespace", deploymentName); + + WebClient webClient = builder().baseUrl("http://localhost/key").build(); + String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()) + .block(); + + // we first read the initial value from the secret + Assertions.assertEquals("initial", result); + + // then deploy a new version of left-configmap, but without changing its data, + // only add a label + V1Secret secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace(NAMESPACE) + .withName("event-reload").build()) + .withData(Map.of("application.properties", "from.properties.key=initial".getBytes())).build(); + + replaceSecret(secret, "event-reload"); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "initial".equals(innerResult); + }); + + Commons.waitForLogStatement("Secret event-reload was updated in namespace default", k3sContainer, + deploymentName); + Commons.waitForLogStatement("data in secret has not changed, will not reload", k3sContainer, deploymentName); + + // change data + secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")).withNamespace(NAMESPACE) + .withName("event-reload").build()) + .withData(Map.of("application.properties", "from.properties.key=change-initial".getBytes())).build(); + + replaceSecret(secret, "event-reload"); + + await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { + WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); + String innerResult = innerWebClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + return "change-initial".equals(innerResult); + }); + + } + + private static void replaceSecret(V1Secret secret, String name) { + try { + new CoreV1Api().replaceNamespacedSecret(name, NAMESPACE, secret, null, null, null, null); + } + catch (ApiException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java similarity index 71% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java index 64fe5a5603..84cc869a0d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,13 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.secrets.event.reload; +package org.springframework.cloud.kubernetes.k8s.client.reload.secret; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1EnvVar; import io.kubernetes.client.openapi.models.V1Ingress; import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1Service; @@ -33,30 +28,34 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.DataChangesInSecretsReloadDelegate.testDataChangesInSecretsReload; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.builder; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.patchOne; +import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.retrySpec; /** * @author wind57 */ -class SecretsEventReloadIT { +class K8sClientSecretsReloadIT { private static final String PROPERTY_URL = "http://localhost:80/key"; - private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-secrets-event-reload"; + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; private static final String NAMESPACE = "default"; + private static final String DEPLOYMENT_NAME = "spring-k8s-client-reload"; + + private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + Commons.pomVersion(); + private static final K3sContainer K3S = Commons.container(); private static Util util; @@ -71,30 +70,39 @@ static void setup() throws Exception { util = new Util(K3S); coreV1Api = new CoreV1Api(); util.setUp(NAMESPACE); + configK8sClientIt(Phase.CREATE); } @AfterAll static void afterAll() throws Exception { + configK8sClientIt(Phase.DELETE); Commons.cleanUp(IMAGE_NAME, K3S); Commons.systemPrune(); } @Test void testSecretReload() throws Exception { - configK8sClientIt(Phase.CREATE, false); Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); + "added configmap informer for namespace", DEPLOYMENT_NAME); testSecretEventReload(); - configK8sClientIt(Phase.DELETE, false); + + testAllOther(); + } + + private void testAllOther() throws Exception { + recreateSecret(); + patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); + testSecretReloadConfigDisabled(); + + recreateSecret(); + patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); + testDataChangesInSecretsReload(K3S, DEPLOYMENT_NAME); } - @Test void testSecretReloadConfigDisabled() throws Exception { - configK8sClientIt(Phase.CREATE, true); Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", IMAGE_NAME); + "added configmap informer for namespace", DEPLOYMENT_NAME); testSecretEventReload(); - configK8sClientIt(Phase.DELETE, true); } void testSecretEventReload() throws Exception { @@ -117,22 +125,18 @@ void testSecretEventReload() throws Exception { .retryWhen(retrySpec()).block().equals("after-change")); } - private void configK8sClientIt(Phase phase, boolean configDisabled) { - V1Deployment deployment = (V1Deployment) util.yaml("deployment.yaml"); + private void recreateSecret() { + V1Secret secret = (V1Secret) util.yaml("secret.yaml"); + util.deleteAndWait(NAMESPACE, null, secret); + util.createAndWait(NAMESPACE, null, secret); + } + + private static void configK8sClientIt(Phase phase) { + V1Deployment deployment = (V1Deployment) util.yaml("deployment-with-secret.yaml"); V1Service service = (V1Service) util.yaml("service.yaml"); V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); V1Secret secret = (V1Secret) util.yaml("secret.yaml"); - List envVars = new ArrayList<>( - Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) - .orElse(List.of())); - - if (configDisabled) { - V1EnvVar disableConfig = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED").value("FALSE"); - envVars.add(disableConfig); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - } - if (phase.equals(Phase.CREATE)) { util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); util.createAndWait(NAMESPACE, null, secret); @@ -143,12 +147,4 @@ else if (phase.equals(Phase.DELETE)) { } } - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java new file mode 100644 index 0000000000..b5b3039adc --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.secret; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; + +/** + * @author wind57 + */ +final class K8sClientSecretsReloadITUtil { + + private static final Map POD_LABELS = Map.of("app", "spring-k8s-client-reload"); + + private static final String BODY_ONE = """ + { + "spec": { + "template": { + "spec": { + "containers": [{ + "name": "spring-k8s-client-reload", + "image": "image_name_here", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/liveness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/actuator/health/readiness", + "port": 8080, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED", + "value": "FALSE" + }, + { + "name": "SPRING_PROFILES_ACTIVE", + "value": "with-secret" + } + ] + }] + } + } + } + } + """; + + private K8sClientSecretsReloadITUtil() { + + } + + static WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + static RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(60, Duration.ofSeconds(2)).filter(Objects::nonNull); + } + + static void patchOne(String deploymentName, String namespace, String imageName) { + patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/configmap-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-polling-reload/src/test/resources/configmap-mount.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml similarity index 65% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml index 116475e408..e1e6ac3f1b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-client-secrets-deployment-event-reload + name: spring-k8s-client-reload spec: selector: matchLabels: - app: spring-cloud-kubernetes-client-secrets-event-reload + app: spring-k8s-client-reload template: metadata: labels: - app: spring-cloud-kubernetes-client-secrets-event-reload + app: spring-k8s-client-reload spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-client-secrets-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-client-secrets-event-reload + - name: spring-k8s-client-reload + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -29,3 +29,5 @@ spec: env: - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD value: DEBUG + - name: SPRING_PROFILES_ACTIVE + value: "with-secret" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/one/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml similarity index 66% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/one/deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml index 81c754c2aa..dde4e3ff9b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/one/deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-cloud-kubernetes-client-configmap-deployment-event-reload + name: spring-k8s-client-reload spec: selector: matchLabels: - app: spring-cloud-kubernetes-client-configmap-event-reload + app: spring-k8s-client-reload template: metadata: labels: - app: spring-cloud-kubernetes-client-configmap-event-reload + app: spring-k8s-client-reload spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-cloud-kubernetes-client-configmap-event-reload - image: docker.io/springcloud/spring-cloud-kubernetes-client-configmap-event-reload + - name: spring-k8s-client-reload + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -31,3 +31,5 @@ spec: value: one - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD value: DEBUG + - name: SPRING_CLOUD_BOOTSTRAP_ENABLED + value: "TRUE" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml similarity index 75% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-ingress.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml index 770915a5bd..4beef36c5e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-core-k8s-client-it/src/test/resources/spring-cloud-kubernetes-core-k8s-client-it-ingress.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: it-ingress + name: spring-k8s-client-ingress-reload namespace: default spec: rules: @@ -11,6 +11,6 @@ spec: pathType: Prefix backend: service: - name: spring-cloud-kubernetes-core-k8s-client-it + name: spring-k8s-client-reload port: number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/left-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml similarity index 75% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/left-configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml index 560954befa..a72d8af5dc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/left-configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml @@ -4,4 +4,4 @@ metadata: name: left-configmap namespace: left data: - left.value: left-initial + left.value: "left-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps/spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app/src/test/resources/logback-test.xml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap-with-label.yaml similarity index 76% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap-with-label.yaml index 56531a6352..658ebca4c2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/resources/right-configmap-with-label.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap-with-label.yaml @@ -6,4 +6,4 @@ metadata: labels: spring.cloud.kubernetes.config.informer.enabled: true data: - right.with.label.value: right-with-label-initial + right.with.label.value: "right-with-label-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap.yaml similarity index 74% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap.yaml index a14273279b..0ede471da5 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-event-reload/src/test/resources/right-configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/right-configmap.yaml @@ -4,4 +4,4 @@ metadata: name: right-configmap namespace: right data: - right.value: right-initial + right.value: "right-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml similarity index 100% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/secret.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml similarity index 50% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-service.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml index 892ac4b435..fae9311ffd 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/resources/istio-service.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml @@ -2,13 +2,13 @@ apiVersion: v1 kind: Service metadata: labels: - app: spring-cloud-kubernetes-fabric8-istio-it - name: spring-cloud-kubernetes-fabric8-istio-it + app: spring-k8s-client-reload + name: spring-k8s-client-reload spec: ports: - name: http port: 8080 targetPort: 8080 selector: - app: spring-cloud-kubernetes-fabric8-istio-it + app: spring-k8s-client-reload type: ClusterIP diff --git a/spring-cloud-kubernetes-test-support/pom.xml b/spring-cloud-kubernetes-test-support/pom.xml index 653e690628..a8f2b92222 100644 --- a/spring-cloud-kubernetes-test-support/pom.xml +++ b/spring-cloud-kubernetes-test-support/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 @@ -49,24 +49,33 @@
- - - - org.codehaus.mojo - exec-maven-plugin - 3.1.0 - - - - java - - - - - org.springframework.cloud.kubernetes.tests.discovery.TestsDiscovery - - - - + + + + run-on-github-actions + + false + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + java + + + + + org.springframework.cloud.kubernetes.tests.discovery.TestsDiscovery + + + + + + diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java index f22caa9b70..3b44f35c80 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java @@ -116,7 +116,7 @@ public static void assertReloadLogStatements(String left, String right, String a LOG.info("appPodName : ->" + appPodName + "<-"); // we issue a pollDelay to let the logs sync in, otherwise the results are not // going to be correctly asserted - await().pollDelay(20, TimeUnit.SECONDS).pollInterval(Duration.ofSeconds(5)).atMost(Duration.ofSeconds(600)) + await().pollDelay(20, TimeUnit.SECONDS).pollInterval(Duration.ofSeconds(5)).atMost(Duration.ofSeconds(120)) .until(() -> { Container.ExecResult result = CONTAINER.execInContainer("sh", "-c", @@ -158,7 +158,15 @@ public static void loadImage(String image, String tag, String tarName, K3sContai Files.copy(imageStream, imagePath); // import image with ctr. this works because TEMP_FOLDER is mounted in the // container - container.execInContainer("ctr", "i", "import", TEMP_FOLDER + "/" + tarName + ".tar"); + await().atMost(Duration.ofMinutes(2)).pollInterval(Duration.ofSeconds(1)).until(() -> { + Container.ExecResult result = container.execInContainer("ctr", "i", "import", + TEMP_FOLDER + "/" + tarName + ".tar"); + boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty(); + if (!noErrors) { + LOG.info("error is : " + result.getStderr()); + } + return noErrors; + }); } } @@ -232,13 +240,13 @@ public static String pomVersion() { /** * the assumption is that there is only a single pod that is 'Running'. */ - public static void waitForLogStatement(String message, K3sContainer k3sContainer, String imageName) { + public static void waitForLogStatement(String message, K3sContainer k3sContainer, String appLabelValue) { try { await().atMost(Duration.ofMinutes(2)).pollInterval(Duration.ofSeconds(4)).until(() -> { String appPodName = k3sContainer.execInContainer("sh", "-c", - "kubectl get pods -l app=" + imageName + "kubectl get pods -l app=" + appLabelValue + " -o custom-columns=POD:metadata.name,STATUS:status.phase" + " | grep -i 'running' | awk '{print $1}' | tr -d '\n' ") .getStdout(); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K8SUtils.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K8SUtils.java index dee9bf7fc4..747c2e433a 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K8SUtils.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/K8SUtils.java @@ -48,8 +48,8 @@ import io.kubernetes.client.openapi.models.V1EndpointsList; import io.kubernetes.client.openapi.models.V1EnvVar; import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1LoadBalancerIngress; -import io.kubernetes.client.openapi.models.V1LoadBalancerStatus; +import io.kubernetes.client.openapi.models.V1IngressLoadBalancerIngress; +import io.kubernetes.client.openapi.models.V1IngressLoadBalancerStatus; import io.kubernetes.client.openapi.models.V1ReplicationController; import io.kubernetes.client.openapi.models.V1ReplicationControllerList; import io.kubernetes.client.openapi.models.V1Role; @@ -196,7 +196,7 @@ public void waitForEndpointReady(String name, String namespace) { public boolean isEndpointReady(String name, String namespace) throws ApiException { V1EndpointsList endpoints = api.listNamespacedEndpoints(namespace, null, null, null, "metadata.name=" + name, - null, null, null, null, null, null); + null, null, null, null, null, null, null); if (endpoints.getItems().isEmpty()) { fail("no endpoints for " + name); } @@ -211,7 +211,7 @@ public void waitForReplicationController(String name, String namespace) { public boolean isReplicationControllerReady(String name, String namespace) throws ApiException { V1ReplicationControllerList controllerList = api.listNamespacedReplicationController(namespace, null, null, - null, "metadata.name=" + name, null, null, null, null, null, null); + null, "metadata.name=" + name, null, null, null, null, null, null, null); if (controllerList.getItems().size() < 1) { fail("Replication controller with name " + name + "could not be found"); } @@ -231,7 +231,7 @@ public void waitForDeployment(String deploymentName, String namespace) { public void waitForIngress(String ingressName, String namespace) { await().timeout(Duration.ofSeconds(90)).pollInterval(Duration.ofSeconds(3)).until(() -> { try { - V1LoadBalancerStatus status = networkingApi.readNamespacedIngress(ingressName, namespace, null) + V1IngressLoadBalancerStatus status = networkingApi.readNamespacedIngress(ingressName, namespace, null) .getStatus().getLoadBalancer(); if (status == null) { @@ -239,7 +239,7 @@ public void waitForIngress(String ingressName, String namespace) { return false; } - List loadBalancerIngress = status.getIngress(); + List loadBalancerIngress = status.getIngress(); if (loadBalancerIngress == null) { log.info("ingress : " + ingressName + " not ready yet (loadbalancer ingress not yet present)"); return false; @@ -280,7 +280,7 @@ public void waitForDeploymentToBeDeleted(String deploymentName, String namespace public boolean isDeploymentReady(String deploymentName, String namespace) throws ApiException { V1DeploymentList deployments = appsApi.listNamespacedDeployment(namespace, null, null, null, - "metadata.name=" + deploymentName, null, null, null, null, null, null); + "metadata.name=" + deploymentName, null, null, null, null, null, null, null); if (deployments.getItems().size() < 1) { fail("No deployments with the name " + deploymentName); } @@ -313,8 +313,8 @@ public void deleteNamespace(String name) throws Exception { api.deleteNamespace(name, null, null, null, null, null, null); await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS) - .until(() -> api.listNamespace(null, null, null, null, null, null, null, null, null, null).getItems() - .stream().noneMatch(x -> x.getMetadata().getName().equals(name))); + .until(() -> api.listNamespace(null, null, null, null, null, null, null, null, null, null, null) + .getItems().stream().noneMatch(x -> x.getMetadata().getName().equals(name))); } public void setUpClusterWide(String serviceAccountNamespace, Set namespaces) throws Exception { @@ -385,7 +385,8 @@ public void deployWiremock(String namespace, boolean rootPath, K3sContainer cont */ public void cleanUpWiremock(String namespace) throws Exception { appsApi.deleteCollectionNamespacedDeployment(namespace, null, null, null, - "metadata.name=" + WIREMOCK_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null, null); + "metadata.name=" + WIREMOCK_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null, null, + null); api.deleteNamespacedService(WIREMOCK_APP_NAME, namespace, null, null, null, null, null, null); networkingApi.deleteNamespacedIngress("wiremock-ingress", namespace, null, null, null, null, null, null); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java index 5b08c1265c..738870119a 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java @@ -261,22 +261,36 @@ private void waitForConfigMap(String namespace, ConfigMap configMap, Phase phase } public void wiremock(String namespace, String path, Phase phase) { + wiremock(namespace, path, phase, true); + } + + public void wiremock(String namespace, String path, Phase phase, boolean withIngress) { InputStream deploymentStream = inputStream("wiremock/wiremock-deployment.yaml"); InputStream serviceStream = inputStream("wiremock/wiremock-service.yaml"); InputStream ingressStream = inputStream("wiremock/wiremock-ingress.yaml"); Deployment deployment = client.apps().deployments().load(deploymentStream).item(); Service service = client.services().load(serviceStream).item(); - Ingress ingress = client.network().v1().ingresses().load(ingressStream).item(); + Ingress ingress = null; if (phase.equals(Phase.CREATE)) { + + if (withIngress) { + ingress = client.network().v1().ingresses().load(ingressStream).get(); + ingress.getMetadata().setNamespace(namespace); + ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).setPath(path); + } + deployment.getMetadata().setNamespace(namespace); service.getMetadata().setNamespace(namespace); - ingress.getMetadata().setNamespace(namespace); - ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).setPath(path); createAndWait(namespace, "wiremock", deployment, service, ingress, false); } else { + + if (withIngress) { + ingress = client.network().v1().ingresses().load(ingressStream).get(); + } + deleteAndWait(namespace, deployment, service, ingress); } @@ -400,7 +414,8 @@ private boolean isDeploymentReadyAfterPatch(String deploymentName, String namesp fail("No deployment with name " + deploymentName); } - Deployment deployment = deployments.getItems().get(0); + Deployment deployment = deployments.getItems().stream() + .filter(x -> x.getMetadata().getName().equals(deploymentName)).findFirst().orElseThrow(); // if no replicas are defined, it means only 1 is needed int replicas = Optional.ofNullable(deployment.getSpec().getReplicas()).orElse(1); diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java index 097b95eb6d..6dd5c90046 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java @@ -41,10 +41,11 @@ import io.kubernetes.client.openapi.models.V1ClusterRoleBinding; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1DeploymentCondition; import io.kubernetes.client.openapi.models.V1DeploymentList; import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1LoadBalancerIngress; -import io.kubernetes.client.openapi.models.V1LoadBalancerStatus; +import io.kubernetes.client.openapi.models.V1IngressLoadBalancerIngress; +import io.kubernetes.client.openapi.models.V1IngressLoadBalancerStatus; import io.kubernetes.client.openapi.models.V1NamespaceBuilder; import io.kubernetes.client.openapi.models.V1Role; import io.kubernetes.client.openapi.models.V1RoleBinding; @@ -115,20 +116,25 @@ public void createAndWait(String namespace, String name, V1Deployment deployment @Nullable V1Ingress ingress, boolean changeVersion) { try { - String imageFromDeployment = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); - if (changeVersion) { - deployment.getSpec().getTemplate().getSpec().getContainers().get(0) - .setImage(imageFromDeployment + ":" + pomVersion()); - } - else { - String[] image = imageFromDeployment.split(":", 2); - pullImage(image[0], image[1], container); - loadImage(image[0], image[1], name, container); + coreV1Api.createNamespacedService(namespace, service, null, null, null, null); + + if (deployment != null) { + String imageFromDeployment = deployment.getSpec().getTemplate().getSpec().getContainers().get(0) + .getImage(); + if (changeVersion) { + deployment.getSpec().getTemplate().getSpec().getContainers().get(0) + .setImage(imageFromDeployment + ":" + pomVersion()); + } + else { + String[] image = imageFromDeployment.split(":", 2); + pullImage(image[0], image[1], container); + loadImage(image[0], image[1], name, container); + } + + appsV1Api.createNamespacedDeployment(namespace, deployment, null, null, null, null); + waitForDeployment(namespace, deployment); } - appsV1Api.createNamespacedDeployment(namespace, deployment, null, null, null, null); - coreV1Api.createNamespacedService(namespace, service, null, null, null, null); - waitForDeployment(namespace, deployment); if (ingress != null) { networkingV1Api.createNamespacedIngress(namespace, ingress, null, null, null, null); waitForIngress(namespace, ingress); @@ -192,20 +198,27 @@ public void createNamespace(String name) { public void deleteAndWait(String namespace, V1Deployment deployment, V1Service service, @Nullable V1Ingress ingress) { - String deploymentName = deploymentName(deployment); - String serviceName = serviceName(service); - try { - Map podLabels = appsV1Api.readNamespacedDeployment(deploymentName, namespace, null) - .getSpec().getTemplate().getMetadata().getLabels(); + if (deployment != null) { + try { + String deploymentName = deploymentName(deployment); + Map podLabels = appsV1Api.readNamespacedDeployment(deploymentName, namespace, null) + .getSpec().getTemplate().getMetadata().getLabels(); + appsV1Api.deleteNamespacedDeployment(deploymentName, namespace, null, null, null, null, null, null); + coreV1Api.deleteCollectionNamespacedPod(namespace, null, null, null, null, null, + labelSelector(podLabels), null, null, null, null, null, null, null, null); + waitForDeploymentToBeDeleted(deploymentName, namespace); + waitForDeploymentPodsToBeDeleted(podLabels, namespace); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + } - appsV1Api.deleteNamespacedDeployment(deploymentName, namespace, null, null, null, null, null, null); + String serviceName = serviceName(service); + try { coreV1Api.deleteNamespacedService(serviceName, namespace, null, null, null, null, null, null); - coreV1Api.deleteCollectionNamespacedPod(namespace, null, null, null, null, null, labelSelector(podLabels), - null, null, null, null, null, null, null); - waitForDeploymentToBeDeleted(deploymentName, namespace); - waitForDeploymentPodsToBeDeleted(podLabels, namespace); - if (ingress != null) { String ingressName = ingressName(ingress); networkingV1Api.deleteNamespacedIngress(ingressName, namespace, null, null, null, null, null, null); @@ -232,11 +245,14 @@ else if (phase.equals(Phase.DELETE)) { public void kafka(String namespace, Phase phase) { V1Deployment deployment = (V1Deployment) yaml("kafka/kafka-deployment.yaml"); V1Service service = (V1Service) yaml("kafka/kafka-service.yaml"); + V1ConfigMap configMap = (V1ConfigMap) yaml("kafka/kafka-configmap-startup-script.yaml"); if (phase.equals(Phase.CREATE)) { + createAndWait(namespace, configMap, null); createAndWait(namespace, "kafka", deployment, service, null, false); } else if (phase.equals(Phase.DELETE)) { + deleteAndWait(namespace, configMap, null); deleteAndWait(namespace, deployment, service, null); } } @@ -253,18 +269,6 @@ else if (phase.equals(Phase.DELETE)) { } } - public void zookeeper(String namespace, Phase phase) { - V1Deployment deployment = (V1Deployment) yaml("zookeeper/zookeeper-deployment.yaml"); - V1Service service = (V1Service) yaml("zookeeper/zookeeper-service.yaml"); - - if (phase.equals(Phase.CREATE)) { - createAndWait(namespace, "zookeeper", deployment, service, null, false); - } - else if (phase.equals(Phase.DELETE)) { - deleteAndWait(namespace, deployment, service, null); - } - } - /** * reads a yaml from classpath, fails if not found. */ @@ -416,29 +420,43 @@ public void deleteNamespace(String name) { } await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS) - .until(() -> coreV1Api.listNamespace(null, null, null, null, null, null, null, null, null, null) + .until(() -> coreV1Api.listNamespace(null, null, null, null, null, null, null, null, null, null, null) .getItems().stream().noneMatch(x -> x.getMetadata().getName().equals(name))); } public void wiremock(String namespace, String path, Phase phase) { + wiremock(namespace, path, phase, true); + } + + public void wiremock(String namespace, String path, Phase phase, boolean withIngress) { V1Deployment deployment = (V1Deployment) yaml("wiremock/wiremock-deployment.yaml"); V1Service service = (V1Service) yaml("wiremock/wiremock-service.yaml"); - V1Ingress ingress = (V1Ingress) yaml("wiremock/wiremock-ingress.yaml"); + + V1Ingress ingress = null; if (phase.equals(Phase.CREATE)) { + + if (withIngress) { + ingress = (V1Ingress) yaml("wiremock/wiremock-ingress.yaml"); + ingress.getMetadata().setNamespace(namespace); + ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).setPath(path); + } + deployment.getMetadata().setNamespace(namespace); service.getMetadata().setNamespace(namespace); - ingress.getMetadata().setNamespace(namespace); - ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).setPath(path); createAndWait(namespace, "wiremock", deployment, service, ingress, false); } else { + if (withIngress) { + ingress = (V1Ingress) yaml("wiremock/wiremock-ingress.yaml"); + } deleteAndWait(namespace, deployment, service, ingress); } } - public static void patchWithMerge(String deploymentName, String namespace, String patchBody) { + public static void patchWithMerge(String deploymentName, String namespace, String patchBody, + Map podLabels) { try { PatchUtils.patch(V1Deployment.class, () -> new AppsV1Api().patchNamespacedDeploymentCall(deploymentName, namespace, @@ -450,10 +468,11 @@ public static void patchWithMerge(String deploymentName, String namespace, Strin throw new RuntimeException(e); } - waitForDeploymentAfterPatch(deploymentName, namespace); + waitForDeploymentAfterPatch(deploymentName, namespace, podLabels); } - public static void patchWithReplace(String imageName, String deploymentName, String namespace, String patchBody) { + public static void patchWithReplace(String imageName, String deploymentName, String namespace, String patchBody, + Map podLabels) { String body = patchBody.replace("image_name_here", imageName); try { @@ -467,7 +486,7 @@ public static void patchWithReplace(String imageName, String deploymentName, Str throw new RuntimeException(e); } - waitForDeploymentAfterPatch(deploymentName, namespace); + waitForDeploymentAfterPatch(deploymentName, namespace, podLabels); } @@ -534,7 +553,7 @@ private void waitForIngress(String namespace, V1Ingress ingress) { String ingressName = ingressName(ingress); await().timeout(Duration.ofSeconds(90)).pollInterval(Duration.ofSeconds(3)).until(() -> { try { - V1LoadBalancerStatus status = networkingV1Api.readNamespacedIngress(ingressName, namespace, null) + V1IngressLoadBalancerStatus status = networkingV1Api.readNamespacedIngress(ingressName, namespace, null) .getStatus().getLoadBalancer(); if (status == null) { @@ -542,7 +561,7 @@ private void waitForIngress(String namespace, V1Ingress ingress) { return false; } - List loadBalancerIngress = status.getIngress(); + List loadBalancerIngress = status.getIngress(); if (loadBalancerIngress == null) { LOG.info("ingress : " + ingressName + " not ready yet (loadbalancer ingress not yet present)"); return false; @@ -585,7 +604,7 @@ private void waitForDeploymentPodsToBeDeleted(Map labels, String await().timeout(Duration.ofSeconds(180)).until(() -> { try { int currentNumberOfPods = coreV1Api.listNamespacedPod(namespace, null, null, null, null, - labelSelector(labels), null, null, null, null, null).getItems().size(); + labelSelector(labels), null, null, null, null, null, null).getItems().size(); return currentNumberOfPods == 0; } catch (ApiException e) { @@ -614,21 +633,39 @@ private void waitForIngressToBeDeleted(String ingressName, String namespace) { private boolean isDeploymentReady(String deploymentName, String namespace) throws ApiException { V1DeploymentList deployments = appsV1Api.listNamespacedDeployment(namespace, null, null, null, - "metadata.name=" + deploymentName, null, null, null, null, null, null); - if (deployments.getItems().size() < 1) { + "metadata.name=" + deploymentName, null, null, null, null, null, null, null); + if (deployments.getItems().isEmpty()) { fail("No deployments with the name " + deploymentName); } V1Deployment deployment = deployments.getItems().get(0); - Integer availableReplicas = deployment.getStatus().getAvailableReplicas(); - LOG.info("Available replicas for " + deploymentName + ": " - + (availableReplicas == null ? 0 : availableReplicas)); - return availableReplicas != null && availableReplicas >= 1; + if (deployment.getStatus() != null) { + Integer availableReplicas = deployment.getStatus().getAvailableReplicas(); + logDeploymentConditions(deployment.getStatus().getConditions()); + LOG.info("Available replicas for " + deploymentName + ": " + + (availableReplicas == null ? 0 : availableReplicas)); + return availableReplicas != null && availableReplicas >= 1; + } + else { + return false; + } } - private static void waitForDeploymentAfterPatch(String deploymentName, String namespace) { + private void logDeploymentConditions(List conditions) { + if (conditions != null) { + for (V1DeploymentCondition condition : conditions) { + LOG.info("Deployment Condition Type: " + condition.getType()); + LOG.info("Deployment Condition Status: " + condition.getStatus()); + LOG.info("Deployment Condition Message: " + condition.getMessage()); + LOG.info("Deployment Condition Reason: " + condition.getReason()); + } + } + } + + private static void waitForDeploymentAfterPatch(String deploymentName, String namespace, + Map podLabels) { try { await().pollDelay(Duration.ofSeconds(4)).pollInterval(Duration.ofSeconds(3)).atMost(60, TimeUnit.SECONDS) - .until(() -> isDeploymentReadyAfterPatch(deploymentName, namespace)); + .until(() -> isDeploymentReadyAfterPatch(deploymentName, namespace, podLabels)); } catch (Exception e) { if (e instanceof ApiException apiException) { @@ -640,10 +677,11 @@ private static void waitForDeploymentAfterPatch(String deploymentName, String na } - private static boolean isDeploymentReadyAfterPatch(String deploymentName, String namespace) throws ApiException { + private static boolean isDeploymentReadyAfterPatch(String deploymentName, String namespace, + Map podLabels) throws ApiException { V1DeploymentList deployments = new AppsV1Api().listNamespacedDeployment(namespace, null, null, null, - "metadata.name=" + deploymentName, null, null, null, null, null, null); + "metadata.name=" + deploymentName, null, null, null, null, null, null, null); if (deployments.getItems().isEmpty()) { fail("No deployment with name " + deploymentName); } @@ -654,6 +692,14 @@ private static boolean isDeploymentReadyAfterPatch(String deploymentName, String int readyReplicas = Optional.ofNullable(deployment.getStatus().getReadyReplicas()).orElse(0); if (readyReplicas != replicas) { + LOG.info("ready replicas not yet same as replicas"); + return false; + } + + int pods = new CoreV1Api().listNamespacedPod(namespace, null, null, null, null, labelSelector(podLabels), null, + null, null, null, null, null).getItems().size(); + + if (pods != replicas) { LOG.info("number of pods not yet stabilized"); return false; } diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/tests/discovery/TestsDiscovery.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/tests/discovery/TestsDiscovery.java index 528613c37b..f3605769e8 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/tests/discovery/TestsDiscovery.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/tests/discovery/TestsDiscovery.java @@ -31,6 +31,7 @@ import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; @@ -42,9 +43,10 @@ public class TestsDiscovery { public static void main(String[] args) throws Exception { - List targetClasses = entireClasspath().stream().filter(x -> x.contains("target/classes")).toList(); + List classpathEntries = entireClasspath(); + List targetClasses = classpathEntries.stream().filter(x -> x.contains("target/classes")).toList(); List targetTestClasses = targetClasses.stream().map(x -> x.replace("classes", "test-classes")).toList(); - List jars = entireClasspath().stream().filter(x -> x.contains(".jar")).toList(); + List jars = classpathEntries.stream().filter(x -> x.contains(".jar")).toList(); List urls = Stream.of(targetClasses, targetTestClasses, jars).flatMap(List::stream) .map(x -> toURL(new File(x).toPath().toUri())).toList(); @@ -57,12 +59,14 @@ public static void main(String[] args) throws Exception { LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(DiscoverySelectors.selectClasspathRoots(paths)).build(); - Launcher launcher = LauncherFactory.openSession().getLauncher(); - TestPlan testPlan = launcher.discover(request); - testPlan.getRoots().stream().flatMap(x -> testPlan.getChildren(x).stream()) - .map(TestIdentifier::getLegacyReportingName).sorted().forEach(test -> { - System.out.println("spring.cloud.k8s.test.to.run -> " + test); - }); + try (LauncherSession session = LauncherFactory.openSession()) { + Launcher launcher = session.getLauncher(); + TestPlan testPlan = launcher.discover(request); + testPlan.getRoots().stream().flatMap(x -> testPlan.getChildren(x).stream()) + .map(TestIdentifier::getLegacyReportingName).sorted() + .forEach(test -> System.out.println("spring.cloud.k8s.test.to.run -> " + test)); + } + } private static void replaceClassloader(List classpathURLs) { @@ -71,9 +75,11 @@ private static void replaceClassloader(List classpathURLs) { Thread.currentThread().setContextClassLoader(classLoader); } - // /tmp/deps.txt are created by the pipeline + // /tmp/deps.txt is created by the pipeline private static List entireClasspath() throws Exception { - return Files.lines(Paths.get("/tmp/deps.txt")).distinct().collect(Collectors.toList()); + try (Stream lines = Files.lines(Paths.get("/tmp/deps.txt"))) { + return lines.distinct().collect(Collectors.toList()); + } } private static URL toURL(URI uri) { diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-configmap-startup-script.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-configmap-startup-script.yaml new file mode 100644 index 0000000000..de6ffe2139 --- /dev/null +++ b/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-configmap-startup-script.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kafka-start-config-script +data: + kafka_start.sh: | + #!/bin/sh + sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + KAFKA_CLUSTER_ID="$(kafka-storage random-uuid)" + echo "kafka-storage format --ignore-formatted -t $KAFKA_CLUSTER_ID -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure + /etc/confluent/docker/run diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-deployment.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-deployment.yaml index 6cce9e9181..f0870b68c4 100644 --- a/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-deployment.yaml +++ b/spring-cloud-kubernetes-test-support/src/main/resources/kafka/kafka-deployment.yaml @@ -21,34 +21,39 @@ spec: # and this will cause this problem: https://github.com/confluentinc/cp-docker-images/blob/master/debian/kafka/include/etc/confluent/docker/configure#L58-L62 # Another solution is to rename the service. enableServiceLinks: false + volumes: + - name: kafka-start-config-script + configMap: + name: kafka-start-config-script + defaultMode: 0755 containers: - - name: kafka + - name: kafka-start-config-script + volumeMounts: + - name: kafka-start-config-script + mountPath: /tmp + command: + - /tmp/kafka_start.sh image: confluentinc/cp-kafka:7.2.1 ports: - containerPort: 9092 env: - - name: KAFKA_LISTENERS - value: "INTERNAL://0.0.0.0:9092,OUTSIDE://0.0.0.0:9094" - + - name: KAFKA_BROKER_ID + value: "1" - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP - value: "INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT" - + value: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" - name: KAFKA_ADVERTISED_LISTENERS - value: "INTERNAL://kafka:9092,OUTSIDE://localhost:9094" - - - name: KAFKA_INTER_BROKER_LISTENER_NAME - value: "INTERNAL" - - - name: KAFKA_ADVERTISED_HOST_NAME - valueFrom: - fieldRef: - fieldPath: status.podIP - - - name: KAFKA_ZOOKEEPER_CONNECT - value: zookeeper:2181 - - # we have enabled auto creation of topics and when this happens there is a replication factor of 3 - # that is set automatically. Since we don't have that many, producers will fail. - # This setting ensures that there is just one replication + value: "PLAINTEXT://kafka:9092" - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR value: "1" + - name: KAFKA_PROCESS_ROLES + value: "broker,controller" + - name: KAFKA_NODE_ID + value: "1" + - name: KAFKA_LISTENERS + value: "PLAINTEXT://:9092,CONTROLLER://:9093" + - name: KAFKA_INTER_BROKER_LISTENER_NAME + value: "PLAINTEXT" + - name: KAFKA_CONTROLLER_LISTENER_NAMES + value: "CONTROLLER" + - name: KAFKA_CONTROLLER_QUORUM_VOTERS + value: "1@localhost:9093" diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-deployment.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-deployment.yaml deleted file mode 100644 index 3c285c30f4..0000000000 --- a/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: kafka - component: zookeeper - name: zookeeper -spec: - replicas: 1 - selector: - matchLabels: - app: kafka - component: zookeeper - template: - metadata: - labels: - app: kafka - component: zookeeper - spec: - containers: - - name: zookeeper - image: confluentinc/cp-zookeeper:7.2.1 - ports: - - containerPort: 2181 - env: - - name: ZOOKEEPER_ID - value: "1" - - name: ZOOKEEPER_SERVER_1 - value: zookeeper - - name: ZOOKEEPER_CLIENT_PORT - value: 2181 diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-service.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-service.yaml deleted file mode 100644 index eb025d3c4a..0000000000 --- a/spring-cloud-kubernetes-test-support/src/main/resources/zookeeper/zookeeper-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: zookeeper - labels: - app: kafka - component: zookeeper -spec: - ports: - - port: 2181 - name: zookeeper-port - targetPort: 2181 - protocol: TCP - selector: - app: kafka - component: zookeeper diff --git a/spring-cloud-starter-kubernetes-client-all/pom.xml b/spring-cloud-starter-kubernetes-client-all/pom.xml index fa19803995..c4a2d454c0 100644 --- a/spring-cloud-starter-kubernetes-client-all/pom.xml +++ b/spring-cloud-starter-kubernetes-client-all/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client-config/pom.xml b/spring-cloud-starter-kubernetes-client-config/pom.xml index f938fb5f3f..7858ac4941 100644 --- a/spring-cloud-starter-kubernetes-client-config/pom.xml +++ b/spring-cloud-starter-kubernetes-client-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml index a7cb9e31ce..11c855b504 100644 --- a/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml +++ b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client/pom.xml b/spring-cloud-starter-kubernetes-client/pom.xml index afc70fcde2..beca89e298 100644 --- a/spring-cloud-starter-kubernetes-client/pom.xml +++ b/spring-cloud-starter-kubernetes-client/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-discoveryclient/pom.xml b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml index 07942943c1..a342bbd74f 100644 --- a/spring-cloud-starter-kubernetes-discoveryclient/pom.xml +++ b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-all/pom.xml b/spring-cloud-starter-kubernetes-fabric8-all/pom.xml index ba613b7e25..8cf55e854b 100644 --- a/spring-cloud-starter-kubernetes-fabric8-all/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-all/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-config/pom.xml b/spring-cloud-starter-kubernetes-fabric8-config/pom.xml index 418ae22577..8d104d3ddf 100644 --- a/spring-cloud-starter-kubernetes-fabric8-config/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml index 6f3228e3db..aef09adfee 100644 --- a/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8/pom.xml b/spring-cloud-starter-kubernetes-fabric8/pom.xml index e9fab4b3a4..8cd51c5d44 100644 --- a/spring-cloud-starter-kubernetes-fabric8/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.0-SNAPSHOT + 3.1.1-SNAPSHOT 4.0.0