diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 9478370a6b..cfcb56c6a9 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2025.2.0",
+ "version": "2026.1.2",
"commands": [
"jb"
],
diff --git a/.editorconfig b/.editorconfig
index adf8ab64ba..5177c71a54 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -159,6 +159,3 @@ dotnet_diagnostic.IDE0270.severity = silent
# JSON002: Probable JSON string detected
dotnet_diagnostic.JSON002.severity = silent
-
-# CA1848: Use the LoggerMessage delegates (depends on https://github.com/SteeltoeOSS/Steeltoe/issues/969)
-dotnet_diagnostic.CA1848.severity = silent
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index d10452a60b..884bbc6e52 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -1,6 +1,6 @@
---
name: Question
-about: Select if you have a question or feedback - Also check out slack.steeltoe.io for assistance.
+about: Select if you have a question or feedback - Also check out https://github.com/SteeltoeOSS/Steeltoe/discussions for assistance.
title: "[QUESTION] "
labels: Type/question
assignees: ''
diff --git a/.github/workflows/Steeltoe.All.yml b/.github/workflows/Steeltoe.All.yml
index a7567b0dc5..6eeb9786db 100644
--- a/.github/workflows/Steeltoe.All.yml
+++ b/.github/workflows/Steeltoe.All.yml
@@ -17,17 +17,18 @@ permissions:
pull-requests: write
env:
+ STEELTOE_MACOS_DIAGNOSE_HOSTNAME_LOOKUP: true
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SOLUTION_FILE: 'src/Steeltoe.All.slnx'
COMMON_TEST_ARGS: >-
- --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}/dumps
- --settings coverlet.runsettings --blame-crash --blame-hang-timeout 3m
+ --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}/TestOutput
+ --settings coverlet.runsettings --blame-crash --blame-hang-timeout 1m
jobs:
- analyze:
+ build:
name: Build and Test
- timeout-minutes: 30
+ timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -36,11 +37,10 @@ jobs:
- os: ubuntu-latest
runDockerContainers: true
- os: windows-latest
- skipFilter: Category!=Integration
+ skipIntegrationTests: true
- os: macos-latest
- skipFilter: Category!=Integration&Category!=SkipOnMacOS
+ skipIntegrationTests: true
runs-on: ${{ matrix.os }}
- continue-on-error: true
services:
eurekaServer:
@@ -54,16 +54,24 @@ jobs:
eureka.client.serviceUrl.defaultZone: http://eurekaServer:8761/eureka
eureka.instance.hostname: localhost
eureka.instance.instanceId: localhost:configServer:8888
+ encrypt.keyStore.location: file:///workspace/server.jks
+ encrypt.keyStore.password: letmein
+ encrypt.keyStore.alias: mytestkey
+ encrypt.rsa.algorithm: OAEP
+ encrypt.rsa.salt: deadbeef
+ encrypt.rsa.strong: "false"
+ options: --name steeltoe-config
ports:
- 8888:8888
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Turn off dev certificate (macOS only)
if: ${{ matrix.os == 'macos-latest' }}
@@ -73,52 +81,51 @@ jobs:
# When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.
# and the test run fails, but without indicating which test caused it. By setting this, the causing test fails with the next message:
# Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date.
- # To prevent the causing test from failing the test run, disable it on macOS by adding [Trait("Category", "SkipOnMacOS")].
+ # To prevent the causing test from failing the test run, disable it on macOS using FactSkippedOnPlatform/TheorySkippedOnPlatform.
shell: bash
run: echo "DOTNET_GENERATE_ASPNET_CERTIFICATE=false" >> $GITHUB_ENV
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
- - name: Restore packages
- run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release --verbosity minimal
-
- - name: Build solution
- run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
-
- - name: Set skip filters for tests
+ - name: Provide jks file for Config Server container
+ if: ${{ matrix.runDockerContainers }}
+ # The Config Server container starts before checkout, when server.jks is not yet available.
+ # Copy it into the container now and restart so Config Server can pick up the keystore.
shell: bash
run: |
- echo SKIP_FILTER_NO_MEMORY_DUMPS="${{ matrix.skipFilter && format('{0}&Category!=MemoryDumps', matrix.skipFilter) || 'Category!=MemoryDumps' }}" >> $GITHUB_ENV
- echo SKIP_FILTER_WITH_MEMORY_DUMPS="${{ matrix.skipFilter && format('{0}&Category=MemoryDumps', matrix.skipFilter) || 'Category=MemoryDumps' }}" >> $GITHUB_ENV
+ docker cp src/Configuration/test/Encryption.Test/Cryptography/server.jks steeltoe-config:/workspace/server.jks
+ docker restart steeltoe-config
- - name: Test (net8.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 --filter "${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }}" ${{ env.COMMON_TEST_ARGS }}
+ - name: Restore packages
+ run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release /p:NuGetAudit=false --verbosity minimal
- - name: Test (net8.0) (memory dumps)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 --filter "${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }}" ${{ env.COMMON_TEST_ARGS }}
+ - name: Build solution
+ run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
- - name: Test (net9.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 --filter "${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }}" ${{ env.COMMON_TEST_ARGS }}
+ - name: Test
+ id: test
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "${{ matrix.skipIntegrationTests == true && 'Category!=MemoryDumps&Category!=Integration' || 'Category!=MemoryDumps' }}" ${{ env.COMMON_TEST_ARGS }}
- - name: Test (net9.0) (memory dumps)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 --filter "${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }}" ${{ env.COMMON_TEST_ARGS }}
+ - name: Test (memory dumps)
+ id: test-memory-dumps
+ run: dotnet test src/Management/test/Endpoint.Test --filter "Category=MemoryDumps" ${{ env.COMMON_TEST_ARGS }}
- name: Upload crash/hang dumps (on failure)
- if: ${{ failure() }}
- uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() && (steps.test.outcome == 'failure' || steps.test-memory-dumps.outcome == 'failure') }}
+ uses: actions/upload-artifact@v7
with:
name: FailedTestOutput-${{ matrix.os }}
path: |
- ${{ github.workspace }}/dumps/**/*.dmp
- ${{ github.workspace }}/dumps/**/Sequence_*.xml
+ ${{ github.workspace }}/TestOutput/**/*.dmp
+ ${{ github.workspace }}/TestOutput/**/Sequence_*.xml
if-no-files-found: ignore
- name: Report test results
- if: ${{ !cancelled() }}
- uses: dorny/test-reporter@v2
+ if: ${{ !cancelled() && (steps.test.outcome != 'skipped' || steps.test-memory-dumps.outcome != 'skipped') }}
+ uses: dorny/test-reporter@v3
with:
name: ${{ matrix.os }} test results
reporter: dotnet-trx
diff --git a/.github/workflows/component-common.yml b/.github/workflows/component-common.yml
index 80f6ea8e7b..4909d6736a 100644
--- a/.github/workflows/component-common.yml
+++ b/.github/workflows/component-common.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-common.yml
diff --git a/.github/workflows/component-configuration.yml b/.github/workflows/component-configuration.yml
index f0cae5d49c..3c32b2dd5e 100644
--- a/.github/workflows/component-configuration.yml
+++ b/.github/workflows/component-configuration.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-configuration.yml
@@ -24,4 +25,4 @@ jobs:
with:
component: Configuration
OS: ubuntu
- runConfigServer: true
+ runDockerContainers: true
diff --git a/.github/workflows/component-connectors.yml b/.github/workflows/component-connectors.yml
index 375c7677f9..43f173b6f4 100644
--- a/.github/workflows/component-connectors.yml
+++ b/.github/workflows/component-connectors.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-connectors.yml
diff --git a/.github/workflows/component-discovery.yml b/.github/workflows/component-discovery.yml
index 12bf4088a7..7e991eb1db 100644
--- a/.github/workflows/component-discovery.yml
+++ b/.github/workflows/component-discovery.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-discovery.yml
diff --git a/.github/workflows/component-logging.yml b/.github/workflows/component-logging.yml
index 1a453d26a8..1ad817653a 100644
--- a/.github/workflows/component-logging.yml
+++ b/.github/workflows/component-logging.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-logging.yml
diff --git a/.github/workflows/component-management.yml b/.github/workflows/component-management.yml
index e3a01e34f8..b1e13119c1 100644
--- a/.github/workflows/component-management.yml
+++ b/.github/workflows/component-management.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-management.yml
@@ -30,7 +31,6 @@ jobs:
with:
component: Management
OS: macos
- skipFilter: Category!=SkipOnMacOS
windows:
uses: ./.github/workflows/component-shared-workflow.yml
diff --git a/.github/workflows/component-security.yml b/.github/workflows/component-security.yml
index cfe3a805f5..25658195ec 100644
--- a/.github/workflows/component-security.yml
+++ b/.github/workflows/component-security.yml
@@ -8,6 +8,7 @@ on:
- stylecop.json
- '*.props'
- '*.ruleset'
+ - nuget.config
- .config/dotnet-tools.json
- .github/workflows/component-shared-workflow.yml
- .github/workflows/component-security.yml
diff --git a/.github/workflows/component-shared-workflow.yml b/.github/workflows/component-shared-workflow.yml
index 17ca300bc0..3b2514ca58 100644
--- a/.github/workflows/component-shared-workflow.yml
+++ b/.github/workflows/component-shared-workflow.yml
@@ -9,10 +9,7 @@ on:
OS:
required: true
type: string
- skipFilter:
- required: false
- type: string
- runConfigServer:
+ runDockerContainers:
required: false
type: boolean
default: false
@@ -22,16 +19,13 @@ permissions:
pull-requests: write
env:
+ STEELTOE_MACOS_DIAGNOSE_HOSTNAME_LOOKUP: true
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: src/Steeltoe.${{ inputs.component }}.slnf
+ SOLUTION_FILE: 'src/Steeltoe.${{ inputs.component }}.slnf'
COMMON_TEST_ARGS: >-
- --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}/dumps
- --settings coverlet.runsettings --blame-crash --blame-hang-timeout 3m
- SKIP_FILTER_NO_MEMORY_DUMPS: >-
- ${{ inputs.skipFilter && format('--filter "{0}&Category!=MemoryDumps"', inputs.skipFilter) || '--filter "Category!=MemoryDumps"' }}
- SKIP_FILTER_WITH_MEMORY_DUMPS: >-
- ${{ inputs.skipFilter && format('--filter "{0}&Category=MemoryDumps"', inputs.skipFilter) || '--filter "Category=MemoryDumps"' }}
+ --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}/TestOutput
+ --settings coverlet.runsettings --blame-crash --blame-hang-timeout 1m
jobs:
build:
@@ -41,26 +35,34 @@ jobs:
services:
eurekaServer:
- image: ${{ inputs.runConfigServer && 'steeltoe.azurecr.io/eureka-server' || null }}
+ image: ${{ inputs.runDockerContainers && 'steeltoe.azurecr.io/eureka-server' || null }}
ports:
- 8761:8761
configServer:
- image: ${{ inputs.runConfigServer && 'steeltoe.azurecr.io/config-server' || null }}
+ image: ${{ inputs.runDockerContainers && 'steeltoe.azurecr.io/config-server' || null }}
env:
eureka.client.enabled: true
eureka.client.serviceUrl.defaultZone: http://eurekaServer:8761/eureka
eureka.instance.hostname: localhost
eureka.instance.instanceId: localhost:configServer:8888
+ encrypt.keyStore.location: file:///workspace/server.jks
+ encrypt.keyStore.password: letmein
+ encrypt.keyStore.alias: mytestkey
+ encrypt.rsa.algorithm: OAEP
+ encrypt.rsa.salt: deadbeef
+ encrypt.rsa.strong: "false"
+ options: --name steeltoe-config
ports:
- 8888:8888
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Turn off dev certificate (macOS only)
if: ${{ inputs.OS == 'macos' }}
@@ -70,50 +72,54 @@ jobs:
# When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.
# and the test run fails, but without indicating which test caused it. By setting this, the causing test fails with the next message:
# Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date.
- # To prevent the causing test from failing the test run, disable it on macOS by adding [Trait("Category", "SkipOnMacOS")].
+ # To prevent the causing test from failing the test run, disable it on macOS using FactSkippedOnPlatform/TheorySkippedOnPlatform.
shell: bash
run: echo "DOTNET_GENERATE_ASPNET_CERTIFICATE=false" >> $GITHUB_ENV
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
+ - name: Provide jks file for Config Server container
+ if: ${{ inputs.runDockerContainers }}
+ # The Config Server container starts before checkout, when server.jks is not yet available.
+ # Copy it into the container now and restart so Config Server can pick up the keystore.
+ shell: bash
+ run: |
+ docker cp src/Configuration/test/Encryption.Test/Cryptography/server.jks steeltoe-config:/workspace/server.jks
+ docker restart steeltoe-config
+
- name: Restore packages
- run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release --verbosity minimal
+ run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release /p:NuGetAudit=false --verbosity minimal
- name: Build solution
run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
- - name: Test (net8.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 ${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
-
- - name: Test (net8.0) (memory dumps)
- if: ${{ inputs.component == 'Management' }}
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 ${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
-
- - name: Test (net9.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 ${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+ - name: Test
+ id: test
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" ${{ env.COMMON_TEST_ARGS }}
- - name: Test (net9.0) (memory dumps)
+ - name: Test (memory dumps)
+ id: test-memory-dumps
if: ${{ inputs.component == 'Management' }}
- run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 ${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+ run: dotnet test src/Management/test/Endpoint.Test --filter "Category=MemoryDumps" ${{ env.COMMON_TEST_ARGS }}
- name: Upload crash/hang dumps (on failure)
- if: ${{ failure() }}
- uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() && (steps.test.outcome == 'failure' || steps.test-memory-dumps.outcome == 'failure') }}
+ uses: actions/upload-artifact@v7
with:
- name: FailedTestOutput-${{ inputs.OS }}
+ name: FailedTestOutput-${{ inputs.OS }}-latest
path: |
- ${{ github.workspace }}/dumps/**/*.dmp
- ${{ github.workspace }}/dumps/**/Sequence_*.xml
+ ${{ github.workspace }}/TestOutput/**/*.dmp
+ ${{ github.workspace }}/TestOutput/**/Sequence_*.xml
if-no-files-found: ignore
- name: Report test results
- if: ${{ !cancelled() }}
- uses: dorny/test-reporter@v2
+ if: ${{ !cancelled() && (steps.test.outcome != 'skipped' || steps.test-memory-dumps.outcome != 'skipped') }}
+ uses: dorny/test-reporter@v3
with:
- name: ${{ inputs.OS }} test results
+ name: ${{ inputs.OS }}-latest test results
reporter: dotnet-trx
path: '**/*.trx'
fail-on-empty: 'true'
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 186049309f..39dad17f94 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -21,7 +21,7 @@ permissions:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SOLUTION_FILE: 'src/Steeltoe.All.slnx'
VERSION_FILE: 'shared-package.props'
jobs:
@@ -32,19 +32,20 @@ jobs:
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
- name: Restore packages
- run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release --verbosity minimal
+ run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release /p:NuGetAudit=false --verbosity minimal
- name: Calculate package version (for release)
if: ${{ github.event_name == 'release' }}
@@ -122,7 +123,7 @@ jobs:
run: dotnet pack ${{ env.SOLUTION_FILE }} --no-build --configuration Release --output ${{ github.workspace }}/packages /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }}
- name: Upload unsigned packages
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
if-no-files-found: error
name: unsigned-packages
@@ -140,13 +141,13 @@ jobs:
steps:
- name: Download unsigned packages
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
name: unsigned-packages
path: packages
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.*
@@ -154,7 +155,7 @@ jobs:
run: dotnet tool install --global sign --prerelease
- name: Azure login
- uses: azure/login@v2
+ uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -173,7 +174,7 @@ jobs:
--description-url 'https://steeltoe.io/'
- name: Upload signed packages
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
if-no-files-found: error
name: signed-packages
@@ -193,22 +194,22 @@ jobs:
steps:
- name: Azure login
- uses: azure/login@v2
+ uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Download signed packages
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
name: signed-packages
path: packages
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: 8.0.x
+ dotnet-version: 8.0.*
source-url: ${{ vars.AZURE_ARTIFACTS_FEED_URL }}
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -237,12 +238,12 @@ jobs:
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: 8.0.x
+ dotnet-version: 8.0.*
- name: Download signed packages
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
name: signed-packages
path: packages
@@ -262,7 +263,7 @@ jobs:
steps:
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: true
diff --git a/.github/workflows/scan-vulnerable-dependencies.yml b/.github/workflows/scan-vulnerable-dependencies.yml
index aab25e06a7..4d310a4ecc 100644
--- a/.github/workflows/scan-vulnerable-dependencies.yml
+++ b/.github/workflows/scan-vulnerable-dependencies.yml
@@ -18,7 +18,7 @@ permissions:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SOLUTION_FILE: 'src/Steeltoe.All.slnx'
jobs:
scan:
@@ -28,16 +28,57 @@ jobs:
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
- name: Report vulnerable dependencies
- run: dotnet restore ${{ env.SOLUTION_FILE }} --verbosity minimal /p:NuGetAudit=true /p:NuGetAuditMode=all /p:NuGetAuditLevel=low /p:TreatWarningsAsErrors=True
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $PSNativeCommandUseErrorActionPreference = $true
+
+ $output = dotnet list ${{ env.SOLUTION_FILE }} package --vulnerable --include-transitive --format json --output-version 1 2>&1
+ $text = ($output | Out-String).TrimEnd()
+ $json = $text | ConvertFrom-Json
+ $hasVulnerabilities = $false
+
+ foreach ($project in $json.projects) {
+ if (-not $project.frameworks) {
+ continue
+ }
+
+ $isTestProject = $project.path -like '*/test/*'
+
+ foreach ($framework in $project.frameworks) {
+ foreach ($package in $framework.topLevelPackages) {
+ $hasVulnerabilities = $true
+
+ foreach ($vulnerability in $package.vulnerabilities) {
+ Write-Host "$($project.path) ($($framework.framework)): top-level $($package.id) $($package.resolvedVersion) – $($vulnerability.severity): $($vulnerability.advisoryurl)"
+ }
+ }
+
+ if (-not $isTestProject) {
+ foreach ($package in $framework.transitivePackages) {
+ $hasVulnerabilities = $true
+
+ foreach ($vulnerability in $package.vulnerabilities) {
+ Write-Host "$($project.path) ($($framework.framework)): transitive $($package.id) $($package.resolvedVersion) – $($vulnerability.severity): $($vulnerability.advisoryurl)"
+ }
+ }
+ }
+ }
+ }
+
+ if ($hasVulnerabilities) {
+ exit 1
+ }
diff --git a/.github/workflows/sonarcube.yml b/.github/workflows/sonarcube.yml
index 34f5e0a5e7..cf7f52bc8c 100644
--- a/.github/workflows/sonarcube.yml
+++ b/.github/workflows/sonarcube.yml
@@ -18,17 +18,19 @@ permissions:
pull-requests: write
env:
+ STEELTOE_MACOS_DIAGNOSE_HOSTNAME_LOOKUP: true
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SOLUTION_FILE: 'src/Steeltoe.All.slnx'
+ NUGET_VULNERABLE_PACKAGE_WARNINGS: '"NU1901;NU1902;NU1903;NU1904"'
SONAR_TEST_ARGS: >-
- --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}
+ --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}/TestOutput
--settings coverlet.runsettings -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.UseSourceLink=false
jobs:
analyze:
name: Analyze
- timeout-minutes: 30
+ timeout-minutes: 60
runs-on: ubuntu-latest
services:
@@ -43,29 +45,42 @@ jobs:
eureka.client.serviceUrl.defaultZone: http://eurekaServer:8761/eureka
eureka.instance.hostname: localhost
eureka.instance.instanceId: localhost:configServer:8888
+ encrypt.keyStore.location: file:///workspace/server.jks
+ encrypt.keyStore.password: letmein
+ encrypt.keyStore.alias: mytestkey
+ encrypt.rsa.algorithm: OAEP
+ encrypt.rsa.salt: deadbeef
+ encrypt.rsa.strong: "false"
+ options: --name steeltoe-config
ports:
- 8888:8888
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Install Sonar .NET Scanner
run: dotnet tool install --global dotnet-sonarscanner
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
# Sonar: Shallow clones should be disabled for a better relevancy of analysis.
fetch-depth: 0
- - name: Restore packages
- run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release --verbosity minimal
+ - name: Provide jks file for Config Server container
+ # The Config Server container starts before checkout, when server.jks is not yet available.
+ # Copy it into the container now and restart so Config Server can pick up the keystore.
+ shell: bash
+ run: |
+ docker cp src/Configuration/test/Encryption.Test/Cryptography/server.jks steeltoe-config:/workspace/server.jks
+ docker restart steeltoe-config
- name: Begin Sonar .NET scanner
id: sonar_begin
@@ -75,20 +90,17 @@ jobs:
dotnet sonarscanner begin /k:"SteeltoeOSS_steeltoe" /o:"steeltoeoss" /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
/d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
- - name: Build solution
- run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
-
- - name: Test (net8.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" --framework net8.0 ${{ env.SONAR_TEST_ARGS }}
+ - name: Restore packages
+ run: dotnet restore ${{ env.SOLUTION_FILE }} --verbosity minimal /p:Configuration=Release /p:NuGetAuditLevel=low /p:WarningsNotAsErrors='${{ env.NUGET_VULNERABLE_PACKAGE_WARNINGS }}'
- - name: Test (net8.0) (memory dumps)
- run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category=MemoryDumps" --framework net8.0 ${{ env.SONAR_TEST_ARGS }}
+ - name: Build solution
+ run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal /p:NuGetAuditLevel=low /p:WarningsNotAsErrors='${{ env.NUGET_VULNERABLE_PACKAGE_WARNINGS }}'
- - name: Test (net9.0)
- run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" --framework net9.0 ${{ env.SONAR_TEST_ARGS }}
+ - name: Test
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" ${{ env.SONAR_TEST_ARGS }}
- - name: Test (net9.0) (memory dumps)
- run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category=MemoryDumps" --framework net9.0 ${{ env.SONAR_TEST_ARGS }}
+ - name: Test (memory dumps)
+ run: dotnet test src/Management/test/Endpoint.Test --filter "Category=MemoryDumps" ${{ env.SONAR_TEST_ARGS }}
- name: End Sonar .NET scanner
if: ${{ !cancelled() && steps.sonar_begin.outcome == 'success' }}
diff --git a/.github/workflows/verify-code-style.yml b/.github/workflows/verify-code-style.yml
index 07859d8a55..0cab3f9724 100644
--- a/.github/workflows/verify-code-style.yml
+++ b/.github/workflows/verify-code-style.yml
@@ -18,7 +18,7 @@ permissions:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
- SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SOLUTION_FILE: 'src/Steeltoe.All.slnx'
jobs:
verify:
@@ -27,14 +27,15 @@ jobs:
steps:
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.*
9.0.*
+ 10.0.*
- name: Git checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 2
@@ -43,7 +44,10 @@ jobs:
run: dotnet tool restore --verbosity minimal
- name: Restore packages
- run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release --verbosity minimal
+ run: dotnet restore ${{ env.SOLUTION_FILE }} /p:Configuration=Release /p:NuGetAudit=false --verbosity minimal
+
+ - name: Build
+ run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release /p:RunAnalyzers=false
- name: CleanupCode (on PR diff)
if: ${{ github.event_name == 'pull_request' }}
@@ -55,11 +59,13 @@ jobs:
$baseCommitHash = git rev-parse HEAD~1
Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request."
- dotnet regitlint -s ${{ env.SOLUTION_FILE }} --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff
+ dotnet jb cleanupcode --version
+ dotnet regitlint -s $env:SOLUTION_FILE --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --no-updates --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff
- name: CleanupCode (on branch)
- if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}
+ if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
shell: pwsh
run: |
- Write-Output "Running code cleanup on all files."
- dotnet regitlint -s ${{ env.SOLUTION_FILE }} --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN --fail-on-diff --print-diff
+ Write-Output 'Running code cleanup on all files.'
+ dotnet jb cleanupcode --version
+ dotnet regitlint -s $env:SOLUTION_FILE --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --no-updates --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN --fail-on-diff --print-diff
diff --git a/.gitignore b/.gitignore
index 1cb051b459..c056419fa6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
*.user
*.userosscache
*.sln.docstates
+*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -21,17 +22,37 @@ mono_crash.*
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
-x64/
-x86/
+
+[Dd]ebug/x64/
+[Dd]ebugPublic/x64/
+[Rr]elease/x64/
+[Rr]eleases/x64/
+bin/x64/
+obj/x64/
+
+[Dd]ebug/x86/
+[Dd]ebugPublic/x86/
+[Rr]elease/x86/
+[Rr]eleases/x86/
+bin/x86/
+obj/x86/
+
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
+[Aa][Rr][Mm]64[Ee][Cc]/
bld/
-[Bb]in/
[Oo]bj/
+[Oo]ut/
[Ll]og/
[Ll]ogs/
+# Build results on 'Bin' directories
+**/[Bb]in/*
+# Uncomment if you have tasks that rely on *.refresh files to move binaries
+# (https://github.com/github/gitignore/pull/3736)
+#!**/[Bb]in/*.refresh
+
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
@@ -43,12 +64,16 @@ Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
+*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
+# Approval Tests result files
+*.received.*
+
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
@@ -75,6 +100,7 @@ StyleCopReport.xml
*.ilk
*.meta
*.obj
+*.idb
*.iobj
*.pch
*.pdb
@@ -82,6 +108,8 @@ StyleCopReport.xml
*.pgc
*.pgd
*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
*.sbr
*.tlb
*.tli
@@ -153,6 +181,7 @@ coverage*.info
# NCrunch
_NCrunch_*
+.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
@@ -294,9 +323,6 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
-# Visual Studio 6 auto-generated project file (contains which files were open etc.)
-*.vbp
-
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
@@ -314,22 +340,22 @@ node_modules/
_Pvt_Extensions
# Paket dependency manager
-.paket/paket.exe
+**/.paket/paket.exe
paket-files/
# FAKE - F# Make
-.fake/
+**/.fake/
# CodeRush personal settings
-.cr/personal
+**/.cr/personal
# Python Tools for Visual Studio (PTVS)
-__pycache__/
+**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
+#tools/**
+#!tools/packages.config
# Tabs Studio
*.tss
@@ -351,15 +377,19 @@ ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
+MSBuild_Logs/
+
+# AWS SAM Build and Temporary Artifacts folder
+.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
-.mfractor/
+**/.mfractor/
# Local History for Visual Studio
-.localhistory/
+**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
@@ -371,7 +401,7 @@ healthchecksdb
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
-.ionide/
+**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
@@ -382,12 +412,14 @@ FodyWeavers.xsd
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
-*.code-workspace
-/.vscode/settings.json
+!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
+# Built Visual Studio Code Extensions
+*.vsix
+
# Windows Installer files from build outputs
*.cab
*.msi
@@ -395,9 +427,6 @@ FodyWeavers.xsd
*.msm
*.msp
-# JetBrains Rider
-*.sln.iml
-
#############################################
### Additions specific to this repository ###
#############################################
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..003a5b960d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,75 @@
+# Steeltoe Build and Test Guide for Developers and CI/CD Agents
+
+This document provides essential guidelines for working with the Steeltoe codebase. For detailed build and test procedures, refer to the [GitHub workflows](.github/workflows/) which serve as the source of truth.
+
+## General Guidelines
+
+### Code Review and Suggestions
+- Only make high confidence suggestions when reviewing code changes.
+- Always use the latest stable version of C#.
+- Never add or change `global.json` unless explicitly asked to.
+- Never change `NuGet.config` files unless explicitly asked to.
+
+### Null Handling
+- Declare variables non-nullable, and check for null at public API entry points.
+- For internal code, trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
+
+### Writing Tests
+- Do not emit "Act", "Arrange" or "Assert" comments in test code.
+- Tests should pass before committing and pushing changes.
+- When possible, code coverage levels should be increased or at least maintained.
+
+## Prerequisites
+
+- **.NET SDK 10.0** (latest patch version)
+- **.NET Runtime 8.0** (latest patch version)
+- **.NET Runtime 9.0** (latest patch version)
+
+Verify your installation:
+```bash
+dotnet --list-sdks
+dotnet --list-runtimes
+```
+
+## Quick Start
+
+For quick iteration during development:
+```bash
+dotnet build
+dotnet test
+```
+
+For final validation before committing changes:
+```bash
+dotnet build src/Steeltoe.All.slnx --configuration Release
+dotnet test src/Steeltoe.All.slnx --configuration Release
+```
+
+### Run Tests
+
+For detailed test procedures including environment-specific filters, test categories, and coverage collection, see [`.github/workflows/Steeltoe.All.yml`](.github/workflows/Steeltoe.All.yml).
+
+**Important context for agents:**
+- Tests run on multiple frameworks: net8.0, net9.0, and net10.0
+- Tests use xUnit trait categories: `Integration` (requires Docker services), `MemoryDumps` (generates memory dumps)
+- Platform-specific test skipping uses attributes like `[FactSkippedOnPlatform]` and `[TheorySkippedOnPlatform]` instead of trait categories
+- Integration tests require Docker containers to be running (e.g., Config Server, Eureka Server) and are primarily designed for Linux CI environments
+- When writing tests, use `[Trait("Category", "Integration")]` for tests requiring external services like Docker containers
+
+## Code Style Validation
+
+Steeltoe uses ReSharper/Rider code cleanup tools via `regitlint` to enforce consistent code style. The CI workflow (`.github/workflows/verify-code-style.yml`) automatically verifies code style on all pull requests.
+
+To run code cleanup locally:
+```powershell
+./cleanupcode.ps1 main
+```
+
+If your PR fails the code style check, run `cleanupcode.ps1` locally and commit the changes.
+
+## Additional Resources
+
+- [Steeltoe Documentation](https://steeltoe.io/)
+- [Contributing Guidelines](https://github.com/SteeltoeOSS/Steeltoe/wiki)
+- [CI Workflow](.github/workflows/Steeltoe.All.yml)
+- [Code Style Workflow](.github/workflows/verify-code-style.yml)
diff --git a/Directory.Build.targets b/Directory.Build.targets
index b59583d8cd..65ab7c7c73 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,6 +1,12 @@
+
+
+
+
+
@@ -101,7 +107,7 @@
-
+
@@ -123,7 +129,7 @@
+ Text="ConfigurationSchema.json is out of date for $(MSBuildProjectFile). Run 'dotnet build --no-incremental /p:UpdateConfigurationSchema=true' to update it." />
diff --git a/PackageReadme.md b/PackageReadme.md
index f3871f74eb..688127cfae 100644
--- a/PackageReadme.md
+++ b/PackageReadme.md
@@ -6,7 +6,7 @@ Key features include:
- External (optionally encrypted) configuration using [Spring Cloud Config Server](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)
- Service discovery with [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix) and [HashiCorp Consul](https://www.consul.io/)
-- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
+- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
- Connectivity to databases (such as [SQL Server](https://www.microsoft.com/sql-server)/[Azure SQL](https://azure.microsoft.com/products/azure-sql), [Cosmos DB](https://azure.microsoft.com/products/cosmos-db/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io/), [RabbitMQ](https://www.rabbitmq.com/), [PostgreSQL](https://www.postgresql.org/), and [MySQL](https://www.mysql.com/)), including support for [Entity Framework Core](https://learn.microsoft.com/ef/core/)
- Single sign-on, JWT and Certificate auth with [Cloud Foundry](https://www.cloudfoundry.org/)
diff --git a/README.md b/README.md
index 051a22ffab..158161aec4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Steeltoe .NET Open Source Software
-[](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml?query=branch%3Amain)
+[](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml?query=branch%3A4.x)
[](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main)
[](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main&metric=coverage&view=list)
[](https://www.nuget.org/profiles/SteeltoeOSS)
@@ -15,7 +15,7 @@ Key features include:
- External (optionally encrypted) configuration using [Spring Cloud Config Server](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)
- Service discovery with [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix) and [HashiCorp Consul](https://www.consul.io/)
-- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
+- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
- Connectivity to databases (such as [SQL Server](https://www.microsoft.com/sql-server)/[Azure SQL](https://azure.microsoft.com/products/azure-sql), [Cosmos DB](https://azure.microsoft.com/products/cosmos-db/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io/), [RabbitMQ](https://www.rabbitmq.com/), [PostgreSQL](https://www.postgresql.org/), and [MySQL](https://www.mysql.com/)), including support for [Entity Framework Core](https://learn.microsoft.com/ef/core/)
- Single sign-on, JWT and Certificate auth with [Cloud Foundry](https://www.cloudfoundry.org/)
@@ -40,7 +40,7 @@ For more details, see [Supported Versions on the Wiki](https://github.com/Steelt
## Support and Feedback
-For community support, we recommend [Steeltoe OSS Slack](https://slack.steeltoe.io), [StackOverflow](https://stackoverflow.com/questions/tagged/steeltoe), or [open an issue](https://github.com/SteeltoeOSS/Steeltoe/issues/new/choose).
+For community support, we recommend [Discussions on GitHub](https://github.com/SteeltoeOSS/Steeltoe/discussions), [StackOverflow](https://stackoverflow.com/questions/tagged/steeltoe), or [open an issue](https://github.com/SteeltoeOSS/Steeltoe/issues/new/choose).
For production support, we recommend that you contact [Broadcom Support](https://support.broadcom.com/).
diff --git a/Steeltoe.Debug.ruleset b/Steeltoe.Debug.ruleset
index 6cd9c84940..97dc2006b9 100644
--- a/Steeltoe.Debug.ruleset
+++ b/Steeltoe.Debug.ruleset
@@ -1,243 +1,243 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Steeltoe.Release.ruleset b/Steeltoe.Release.ruleset
index 343ee1b90a..24a6a00a3b 100644
--- a/Steeltoe.Release.ruleset
+++ b/Steeltoe.Release.ruleset
@@ -1,217 +1,217 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cleanupcode.ps1 b/cleanupcode.ps1
index 4f36c58367..f8c18429ab 100644
--- a/cleanupcode.ps1
+++ b/cleanupcode.ps1
@@ -1,4 +1,4 @@
-#Requires -Version 7.0
+#Requires -Version 7.4
# This script reformats (part of) the codebase to make it compliant with our coding guidelines.
@@ -7,38 +7,31 @@ param(
[string] $revision
)
-function VerifySuccessExitCode {
- if ($LastExitCode -ne 0) {
- throw "Command failed with exit code $LastExitCode."
- }
-}
+$ErrorActionPreference = "Stop"
+$PSNativeCommandUseErrorActionPreference = $true
+$solutionFile = 'src/Steeltoe.All.slnx'
dotnet tool restore
-VerifySuccessExitCode
-
-dotnet restore src
-VerifySuccessExitCode
+dotnet restore $solutionFile /p:NuGetAudit=false
+dotnet build $solutionFile --no-restore --configuration Release /p:RunAnalyzers=false
if ($revision) {
$headCommitHash = git rev-parse HEAD
- VerifySuccessExitCode
-
$baseCommitHash = git rev-parse $revision
- VerifySuccessExitCode
if ($baseCommitHash -eq $headCommitHash) {
Write-Output "Running code cleanup on staged/unstaged files."
- dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified
- VerifySuccessExitCode
+ dotnet jb cleanupcode --version
+ dotnet regitlint -s $solutionFile --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --no-updates --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified
}
else {
Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash, including staged/unstaged files."
- dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash
- VerifySuccessExitCode
+ dotnet jb cleanupcode --version
+ dotnet regitlint -s $solutionFile --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --no-updates --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash
}
}
else {
Write-Output "Running code cleanup on all files."
- dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN
- VerifySuccessExitCode
+ dotnet jb cleanupcode --version
+ dotnet regitlint -s $solutionFile --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --no-updates --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN
}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000..d3f818ab1a
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+# Starts Eureka Server and Spring Cloud Config Server for running Configuration integration tests locally.
+#
+# Usage:
+# docker compose up -d
+# Start-Sleep -Seconds 15 # wait for servers to be ready
+#
+# Run all Configuration integration tests:
+# dotnet test src/Configuration --filter "Category=Integration"
+#
+# To generate a new OAEP-encrypted test vector (for Encryption.Test):
+# Invoke-RestMethod -Method Post -Uri "http://localhost:8888/encrypt" `
+# -Body "encrypt the world" -ContentType "text/plain"
+
+services:
+ eureka-server:
+ image: steeltoe.azurecr.io/eureka-server
+ pull_policy: always
+ container_name: eureka-server
+ ports:
+ - "8761:8761"
+
+ config-server:
+ image: steeltoe.azurecr.io/config-server
+ pull_policy: always
+ container_name: steeltoe-config
+ depends_on:
+ - eureka-server
+ ports:
+ - "8888:8888"
+ volumes:
+ - ./src/Configuration/test/Encryption.Test/Cryptography/server.jks:/workspace/server.jks:ro
+ environment:
+ eureka.client.enabled: "true"
+ eureka.client.serviceUrl.defaultZone: http://eureka-server:8761/eureka
+ eureka.instance.hostname: localhost
+ eureka.instance.instanceId: localhost:configServer:8888
+ encrypt.keyStore.location: file:///workspace/server.jks
+ encrypt.keyStore.password: letmein
+ encrypt.keyStore.alias: mytestkey
+ encrypt.rsa.algorithm: OAEP
+ encrypt.rsa.salt: deadbeef
+ encrypt.rsa.strong: "false"
diff --git a/macos-dump-entitlements.plist b/macos-dump-entitlements.plist
new file mode 100644
index 0000000000..41bb93a0c7
--- /dev/null
+++ b/macos-dump-entitlements.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.cs.debugger
+
+ com.apple.security.get-task-allow
+
+
+
\ No newline at end of file
diff --git a/nuget.config b/nuget.config
index f5253c498c..128d95e590 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,8 +1,12 @@
-
+
-
-
-
+
+
+
+
+
+
+
diff --git a/shared-package.props b/shared-package.props
index 721a23607d..1ca55bd36c 100644
--- a/shared-package.props
+++ b/shared-package.props
@@ -12,7 +12,7 @@
- 4.0.0
+ 4.2.0
pre
Broadcom
PackageIcon.png
diff --git a/shared-project.props b/shared-project.props
index 2dfa2fa8de..bd037efa28 100644
--- a/shared-project.props
+++ b/shared-project.props
@@ -4,5 +4,6 @@
false
-
+
diff --git a/shared-test.props b/shared-test.props
index 070739758d..2d1e68dde3 100644
--- a/shared-test.props
+++ b/shared-test.props
@@ -1,17 +1,14 @@
Exe
- $(NoWarn);S2094;S3717;SA1602;CA1062;CA1707;NU5104
-
-
-
-
false
- true
+ $(NoWarn);S2094;S3717;SA1602;CA1062;CA1707;CA1848;NU5104
+ direct
-
+
diff --git a/shared.props b/shared.props
index a6473a3318..1e521d95a8 100644
--- a/shared.props
+++ b/shared.props
@@ -20,6 +20,13 @@
$(NoWarn);IDE0051;IDE0052
+
+
+ $(NoWarn);ASPDEPR004;ASPDEPR008
+
+
$(MSBuildThisFileDirectory)\Steeltoe.Debug.ruleset
diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs
index 2c1b85a862..5b58a286a8 100644
--- a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs
+++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs
@@ -38,7 +38,7 @@
namespace Steeltoe.Bootstrap.AutoConfiguration;
-internal sealed class BootstrapScanner
+internal sealed partial class BootstrapScanner
{
private readonly HostBuilderWrapper _wrapper;
private readonly AssemblyLoader _loader;
@@ -91,23 +91,23 @@ public void ConfigureSteeltoe()
private void WireConfigServer()
{
- _wrapper.AddConfigServer(_loggerFactory);
+ _wrapper.AddConfigServer(null, _loggerFactory);
- _logger.LogInformation("Configured Config Server configuration provider");
+ LogConfigServerConfigured();
}
private void WireCloudFoundryConfiguration()
{
_wrapper.AddCloudFoundryConfiguration(_loggerFactory);
- _logger.LogInformation("Configured Cloud Foundry configuration provider");
+ LogCloudFoundryConfigured();
}
private void WireRandomValueProvider()
{
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddRandomValueSource(_loggerFactory));
- _logger.LogInformation("Configured random value configuration provider");
+ LogRandomValueConfigured();
}
private void WireSpringBootProvider()
@@ -117,21 +117,21 @@ private void WireSpringBootProvider()
string[] args = Environment.GetCommandLineArgs().Skip(1).ToArray();
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddSpringBootFromCommandLine(args, _loggerFactory));
- _logger.LogInformation("Configured Spring Boot configuration provider");
+ LogSpringBootConfigured();
}
private void WireDecryptionProvider()
{
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddDecryption(_loggerFactory));
- _logger.LogInformation("Configured decryption configuration provider");
+ LogDecryptionConfigured();
}
private void WirePlaceholderResolver()
{
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddPlaceholderResolver(_loggerFactory));
- _logger.LogInformation("Configured placeholder configuration provider");
+ LogPlaceholderConfigured();
}
private void WireConnectors()
@@ -150,7 +150,7 @@ private void WireCosmosDbConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureCosmosDb());
_wrapper.ConfigureServices((host, services) => services.AddCosmosDb(host.Configuration));
- _logger.LogInformation("Configured CosmosDB connector");
+ LogCosmosDbConfigured();
}
private void WireMongoDbConnector()
@@ -158,7 +158,7 @@ private void WireMongoDbConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMongoDb());
_wrapper.ConfigureServices((host, services) => services.AddMongoDb(host.Configuration));
- _logger.LogInformation("Configured MongoDB connector");
+ LogMongoDbConfigured();
}
private void WireMySqlConnector()
@@ -166,7 +166,7 @@ private void WireMySqlConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMySql());
_wrapper.ConfigureServices((host, services) => services.AddMySql(host.Configuration));
- _logger.LogInformation("Configured MySQL connector");
+ LogMySqlConfigured();
}
private void WirePostgreSqlConnector()
@@ -174,7 +174,7 @@ private void WirePostgreSqlConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigurePostgreSql());
_wrapper.ConfigureServices((host, services) => services.AddPostgreSql(host.Configuration));
- _logger.LogInformation("Configured PostgreSQL connector");
+ LogPostgreSqlConfigured();
}
private void WireRabbitMQConnector()
@@ -182,7 +182,7 @@ private void WireRabbitMQConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRabbitMQ());
_wrapper.ConfigureServices((host, services) => services.AddRabbitMQ(host.Configuration));
- _logger.LogInformation("Configured RabbitMQ connector");
+ LogRabbitMQConfigured();
}
private void WireRedisConnector()
@@ -190,12 +190,12 @@ private void WireRedisConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRedis());
_wrapper.ConfigureServices((host, services) => services.AddRedis(host.Configuration));
- _logger.LogInformation("Configured StackExchange Redis connector");
+ LogRedisConfigured();
// Intentionally ignoring excluded assemblies here.
if (MicrosoftRedisPackageResolver.Default.IsAvailable())
{
- _logger.LogInformation("Configured Redis distributed cache connector");
+ LogRedisDistributedCacheConfigured();
}
}
@@ -204,63 +204,63 @@ private void WireSqlServerConnector()
_wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureSqlServer());
_wrapper.ConfigureServices((host, services) => services.AddSqlServer(host.Configuration));
- _logger.LogInformation("Configured SQL Server connector");
+ LogSqlServerConfigured();
}
private void WireDynamicSerilog()
{
_wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicSerilog());
- _logger.LogInformation("Configured dynamic console logger for Serilog");
+ LogDynamicSerilogConfigured();
}
private void WireDynamicConsole()
{
_wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicConsole());
- _logger.LogInformation("Configured dynamic console logger");
+ LogDynamicConsoleConfigured();
}
private void WireDiscoveryConfiguration()
{
_wrapper.ConfigureServices(services => services.AddConfigurationDiscoveryClient());
- _logger.LogInformation("Configured configuration discovery client");
+ LogConfigurationDiscoveryConfigured();
}
private void WireDiscoveryConsul()
{
_wrapper.ConfigureServices(services => services.AddConsulDiscoveryClient());
- _logger.LogInformation("Configured Consul discovery client");
+ LogConsulDiscoveryConfigured();
}
private void WireDiscoveryEureka()
{
_wrapper.ConfigureServices(services => services.AddEurekaDiscoveryClient());
- _logger.LogInformation("Configured Eureka discovery client");
+ LogEurekaDiscoveryConfigured();
}
private void WireAllActuators()
{
_wrapper.ConfigureServices(services => services.AddAllActuators());
- _logger.LogInformation("Configured actuators");
+ LogActuatorsConfigured();
}
private void WirePrometheus()
{
_wrapper.ConfigureServices(services => services.AddPrometheusActuator());
- _logger.LogInformation("Configured Prometheus");
+ LogPrometheusConfigured();
}
private void WireDistributedTracingLogProcessor()
{
_wrapper.ConfigureServices(services => services.AddTracingLogProcessor());
- _logger.LogInformation("Configured distributed tracing log processor");
+ LogDistributedTracingConfigured();
}
private bool WireIfLoaded(Action wireAction, string assemblyName)
@@ -281,4 +281,70 @@ private void WireIfAnyLoaded(Action wireAction, params PackageResolver[] package
wireAction();
}
}
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Config Server configuration provider.")]
+ private partial void LogConfigServerConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Cloud Foundry configuration provider.")]
+ private partial void LogCloudFoundryConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured random value configuration provider.")]
+ private partial void LogRandomValueConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Spring Boot configuration provider.")]
+ private partial void LogSpringBootConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured decryption configuration provider.")]
+ private partial void LogDecryptionConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured placeholder configuration provider.")]
+ private partial void LogPlaceholderConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured CosmosDB connector.")]
+ private partial void LogCosmosDbConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured MongoDB connector.")]
+ private partial void LogMongoDbConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured MySQL connector.")]
+ private partial void LogMySqlConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured PostgreSQL connector.")]
+ private partial void LogPostgreSqlConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured RabbitMQ connector.")]
+ private partial void LogRabbitMQConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured StackExchange Redis connector.")]
+ private partial void LogRedisConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Redis distributed cache connector.")]
+ private partial void LogRedisDistributedCacheConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured SQL Server connector.")]
+ private partial void LogSqlServerConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured dynamic console logger for Serilog.")]
+ private partial void LogDynamicSerilogConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured dynamic console logger.")]
+ private partial void LogDynamicConsoleConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured configuration discovery client.")]
+ private partial void LogConfigurationDiscoveryConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Consul discovery client.")]
+ private partial void LogConsulDiscoveryConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Eureka discovery client.")]
+ private partial void LogEurekaDiscoveryConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured actuators.")]
+ private partial void LogActuatorsConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured Prometheus.")]
+ private partial void LogPrometheusConfigured();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Configured distributed tracing log processor.")]
+ private partial void LogDistributedTracingConfigured();
}
diff --git a/src/Bootstrap/src/AutoConfiguration/ConfigurationSchema.json b/src/Bootstrap/src/AutoConfiguration/ConfigurationSchema.json
new file mode 100644
index 0000000000..d3dd0a402f
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Bootstrap": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Bootstrap.AutoConfiguration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs b/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs
index 84aa78e3ca..584c9ab7d9 100644
--- a/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs
+++ b/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs
@@ -3,5 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Bootstrap", "Steeltoe.Bootstrap.AutoConfiguration")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
diff --git a/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj
index 106df9355a..677a96c46d 100644
--- a/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj
+++ b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Automatically configure Steeltoe packages that are referenced by a project. This is not a meta package, other packages must be added separately.
autoconfiguration;automatic;configuration;application;bootstrapping;starter
true
diff --git a/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs
index 8dda994220..158756d68a 100644
--- a/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs
+++ b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs
@@ -308,7 +308,7 @@ private static void AssertConnectorsAreAutowired(HostWrapper hostWrapper)
var configuration = hostWrapper.Services.GetRequiredService();
configuration.EnumerateProviders().Should().NotBeEmpty();
- configuration.EnumerateProviders().Should().ContainSingle();
+ configuration.EnumerateProviders().Should().NotBeEmpty();
hostWrapper.Services.GetService>().Should().NotBeNull();
hostWrapper.Services.GetService>().Should().NotBeNull();
diff --git a/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj
index 16e099632f..6e06050f02 100644
--- a/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj
+++ b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj b/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj
index 19e169e9b3..8b5c6233b2 100644
--- a/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj
+++ b/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/src/Certificates/CertificateOptions.cs b/src/Common/src/Certificates/CertificateOptions.cs
index 3b062e0f38..9482b2d792 100644
--- a/src/Common/src/Certificates/CertificateOptions.cs
+++ b/src/Common/src/Certificates/CertificateOptions.cs
@@ -14,6 +14,20 @@ public sealed class CertificateOptions
internal const string ConfigurationKeyPrefix = "Certificates";
public X509Certificate2? Certificate { get; set; }
-
public IList IssuerChain { get; } = [];
+
+ internal CertificateOptions Clone()
+ {
+ var clone = new CertificateOptions
+ {
+ Certificate = Certificate
+ };
+
+ foreach (X509Certificate2 issuer in IssuerChain)
+ {
+ clone.IssuerChain.Add(issuer);
+ }
+
+ return clone;
+ }
}
diff --git a/src/Common/src/Certificates/ConfigureCertificateOptions.cs b/src/Common/src/Certificates/ConfigureCertificateOptions.cs
index e70acc06e6..e65c5bd924 100644
--- a/src/Common/src/Certificates/ConfigureCertificateOptions.cs
+++ b/src/Common/src/Certificates/ConfigureCertificateOptions.cs
@@ -10,10 +10,9 @@
namespace Steeltoe.Common.Certificates;
-internal sealed class ConfigureCertificateOptions : IConfigureNamedOptions
+internal sealed partial class ConfigureCertificateOptions : IConfigureNamedOptions
{
- private static readonly Regex CertificateRegex = new("-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+", RegexOptions.Compiled | RegexOptions.Singleline,
- TimeSpan.FromSeconds(1));
+ private const int RegexMatchTimeoutInMilliseconds = 1_000;
private readonly IConfiguration _configuration;
@@ -24,6 +23,10 @@ public ConfigureCertificateOptions(IConfiguration configuration)
_configuration = configuration;
}
+ [GeneratedRegex("-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+", RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
+ RegexMatchTimeoutInMilliseconds)]
+ private static partial Regex CertificateRegex();
+
public void Configure(CertificateOptions options)
{
Configure(Options.DefaultName, options);
@@ -42,12 +45,14 @@ public void Configure(string? name, CertificateOptions options)
string? privateKeyFilePath = _configuration.GetValue(GetConfigurationKey(name, "PrivateKeyFilePath"));
+#pragma warning disable SYSLIB0057 // Type or member is obsolete
options.Certificate = privateKeyFilePath != null && File.Exists(privateKeyFilePath)
? X509Certificate2.CreateFromPemFile(certificateFilePath, privateKeyFilePath)
: new X509Certificate2(certificateFilePath);
- X509Certificate2[] certificateChain = CertificateRegex.Matches(File.ReadAllText(certificateFilePath))
+ X509Certificate2[] certificateChain = CertificateRegex().Matches(File.ReadAllText(certificateFilePath))
.Select(x => new X509Certificate2(Encoding.ASCII.GetBytes(x.Value))).ToArray();
+#pragma warning restore SYSLIB0057 // Type or member is obsolete
foreach (X509Certificate2 issuer in certificateChain.Skip(1))
{
diff --git a/src/Common/src/Certificates/LocalCertificateWriter.cs b/src/Common/src/Certificates/LocalCertificateWriter.cs
index 8c0ded40c1..65a7c2c6c7 100644
--- a/src/Common/src/Certificates/LocalCertificateWriter.cs
+++ b/src/Common/src/Certificates/LocalCertificateWriter.cs
@@ -63,7 +63,9 @@ public void Write(Guid orgId, Guid spaceId)
}
else
{
+#pragma warning disable SYSLIB0057 // Type or member is obsolete
caCertificate = new X509Certificate2(RootCaPfxPath);
+#pragma warning restore SYSLIB0057 // Type or member is obsolete
}
// Create the intermediate certificate if it doesn't already exist (can be shared by multiple applications)
@@ -76,7 +78,9 @@ public void Write(Guid orgId, Guid spaceId)
}
else
{
+#pragma warning disable SYSLIB0057 // Type or member is obsolete
intermediateCertificate = new X509Certificate2(IntermediatePfxPath);
+#pragma warning restore SYSLIB0057 // Type or member is obsolete
}
var subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
diff --git a/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj b/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj
index 85bf2ab00d..9ed450c61d 100644
--- a/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj
+++ b/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Steeltoe shared library for working with certificates.
security;pem;certificate
true
diff --git a/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs
index 8b3b9cd4fd..97a88a65e9 100644
--- a/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs
+++ b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs
@@ -8,15 +8,17 @@
namespace Steeltoe.Common.Configuration;
-internal static class ConfigurationKeyConverter
+internal static partial class ConfigurationKeyConverter
{
private const string DotDelimiterString = ".";
private const char DotDelimiterChar = '.';
private const char UnderscoreDelimiterChar = '_';
private const char EscapeChar = '\\';
private const string EscapeString = "\\";
+ private const int RegexMatchTimeoutInMilliseconds = 1_000;
- private static readonly Regex ArrayRegex = new(@"\[(?\d+)\]", RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1));
+ [GeneratedRegex(@"\[(?\d+)\]", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)]
+ private static partial Regex ArrayRegex();
public static string AsDotNetConfigurationKey(string key)
{
@@ -82,6 +84,6 @@ static string UnEscapeString(string src)
private static string ConvertArrayKey(string key)
{
- return ArrayRegex.Replace(key, ":${digits}");
+ return ArrayRegex().Replace(key, ":${digits}");
}
}
diff --git a/src/Common/src/Common/ConfigurationSchema.json b/src/Common/src/Common/ConfigurationSchema.json
new file mode 100644
index 0000000000..2f5c0236d4
--- /dev/null
+++ b/src/Common/src/Common/ConfigurationSchema.json
@@ -0,0 +1,14 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Common/Discovery/DiscoveryInstancesFetchedEventArgs.cs b/src/Common/src/Common/Discovery/DiscoveryInstancesFetchedEventArgs.cs
new file mode 100644
index 0000000000..2f51827683
--- /dev/null
+++ b/src/Common/src/Common/Discovery/DiscoveryInstancesFetchedEventArgs.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.Discovery;
+
+///
+/// Provides data for the event.
+///
+public sealed class DiscoveryInstancesFetchedEventArgs : EventArgs
+{
+ ///
+ /// Gets the updated list of service instances, grouped by service ID.
+ ///
+ public IReadOnlyDictionary> InstancesByServiceId { get; }
+
+ public DiscoveryInstancesFetchedEventArgs(IReadOnlyDictionary> instancesByServiceId)
+ {
+ ArgumentNullException.ThrowIfNull(instancesByServiceId);
+
+ InstancesByServiceId = instancesByServiceId;
+ }
+}
diff --git a/src/Common/src/Common/Discovery/IDiscoveryClient.cs b/src/Common/src/Common/Discovery/IDiscoveryClient.cs
index 598095b2a9..07c9c21b0c 100644
--- a/src/Common/src/Common/Discovery/IDiscoveryClient.cs
+++ b/src/Common/src/Common/Discovery/IDiscoveryClient.cs
@@ -14,6 +14,11 @@ public interface IDiscoveryClient
///
string Description { get; }
+ ///
+ /// Occurs when service instances have been fetched from the discovery server.
+ ///
+ event EventHandler InstancesFetched;
+
///
/// Gets information used to register the local service instance (this app) to the discovery server.
///
diff --git a/src/Common/src/Common/Discovery/IServiceInstance.cs b/src/Common/src/Common/Discovery/IServiceInstance.cs
index 8f23b8ef3d..25a5823fcc 100644
--- a/src/Common/src/Common/Discovery/IServiceInstance.cs
+++ b/src/Common/src/Common/Discovery/IServiceInstance.cs
@@ -11,6 +11,11 @@ public interface IServiceInstance
///
string ServiceId { get; }
+ ///
+ /// Gets the instance ID as registered by the discovery client.
+ ///
+ string InstanceId { get; }
+
///
/// Gets the hostname of the registered service instance.
///
@@ -31,6 +36,16 @@ public interface IServiceInstance
///
Uri Uri { get; }
+ ///
+ /// Gets the HTTP-based resolved address of the registered service instance, if available.
+ ///
+ Uri? NonSecureUri { get; }
+
+ ///
+ /// Gets the HTTPS-based resolved address of the registered service instance, if available.
+ ///
+ Uri? SecureUri { get; }
+
///
/// Gets the key/value metadata associated with this service instance.
///
diff --git a/src/Common/src/Common/Extensions/MaskedUri.cs b/src/Common/src/Common/Extensions/MaskedUri.cs
new file mode 100644
index 0000000000..53540e436a
--- /dev/null
+++ b/src/Common/src/Common/Extensions/MaskedUri.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.Extensions;
+
+///
+/// Represents a whose username and password are masked.
+///
+internal readonly record struct MaskedUri
+{
+ private readonly Uri? _value;
+
+ public MaskedUri(Uri? value)
+ {
+ _value = value;
+ }
+
+ public override string ToString()
+ {
+ return _value == null ? string.Empty : ToMaskedString(_value);
+ }
+
+ private static string ToMaskedString(Uri source)
+ {
+ string uris = source.ToString();
+
+ if (uris.Contains(','))
+ {
+ return string.Join(',',
+ uris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(uri => Mask(new Uri(uri)).ToString()));
+ }
+
+ return Mask(source).ToString();
+ }
+
+ private static Uri Mask(Uri source)
+ {
+ if (string.IsNullOrEmpty(source.UserInfo))
+ {
+ return source;
+ }
+
+ var builder = new UriBuilder(source)
+ {
+ UserName = "****",
+#pragma warning disable S2068 // Hard-coded credentials are security-sensitive
+ Password = "****"
+#pragma warning restore S2068 // Hard-coded credentials are security-sensitive
+ };
+
+ return builder.Uri;
+ }
+
+ public static implicit operator MaskedUri(Uri? uri)
+ {
+ return new MaskedUri(uri);
+ }
+}
diff --git a/src/Common/src/Common/Extensions/UriExtensions.cs b/src/Common/src/Common/Extensions/UriExtensions.cs
index e1c400459b..e476d083bf 100644
--- a/src/Common/src/Common/Extensions/UriExtensions.cs
+++ b/src/Common/src/Common/Extensions/UriExtensions.cs
@@ -9,39 +9,6 @@ namespace Steeltoe.Common.Extensions;
internal static class UriExtensions
{
- public static string ToMaskedString(this Uri source)
- {
- ArgumentNullException.ThrowIfNull(source);
-
- string uris = source.ToString();
-
- if (uris.Contains(','))
- {
- return string.Join(',',
- uris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(uri => ToMaskedUri(new Uri(uri)).ToString()));
- }
-
- return ToMaskedUri(source).ToString();
- }
-
- private static Uri ToMaskedUri(Uri source)
- {
- if (string.IsNullOrEmpty(source.UserInfo))
- {
- return source;
- }
-
- var builder = new UriBuilder(source)
- {
- UserName = "****",
-#pragma warning disable S2068 // Hard-coded credentials are security-sensitive
- Password = "****"
-#pragma warning restore S2068 // Hard-coded credentials are security-sensitive
- };
-
- return builder.Uri;
- }
-
public static bool TryGetUsernamePassword(this Uri uri, [NotNullWhen(true)] out string? username, [NotNullWhen(true)] out string? password)
{
ArgumentNullException.ThrowIfNull(uri);
diff --git a/src/Common/src/Common/HealthChecks/HealthAggregator.cs b/src/Common/src/Common/HealthChecks/HealthAggregator.cs
index 92c1e7d88a..076928ed0b 100644
--- a/src/Common/src/Common/HealthChecks/HealthAggregator.cs
+++ b/src/Common/src/Common/HealthChecks/HealthAggregator.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Steeltoe.Common.Extensions;
using MicrosoftHealthCheckResult = Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult;
@@ -67,7 +68,10 @@ await Parallel.ForEachAsync(contributors, cancellationToken, async (contributor,
private static async Task> AggregateMicrosoftHealthChecksAsync(ICollection contributors,
ICollection healthCheckRegistrations, IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
- if (healthCheckRegistrations.Count == 0)
+ HealthCheckRegistration[] activeHealthCheckRegistrations =
+ healthCheckRegistrations.Where(registration => !registration.Tags.Contains("ExcludeFromHealthActuator")).ToArray();
+
+ if (activeHealthCheckRegistrations.Length == 0)
{
return new Dictionary();
}
@@ -75,8 +79,7 @@ private static async Task> Aggreg
var healthChecks = new ConcurrentDictionary();
var keys = new ConcurrentBag(contributors.Select(contributor => contributor.Id));
- // run all HealthCheckRegistration checks in parallel
- await Parallel.ForEachAsync(healthCheckRegistrations, cancellationToken, async (registration, _) =>
+ await Parallel.ForEachAsync(activeHealthCheckRegistrations, cancellationToken, async (registration, _) =>
{
string contributorName = GetKey(keys, registration.Name);
SteeltoeHealthCheckResult healthCheckResult;
@@ -108,7 +111,10 @@ private static async Task RunMicrosoftHealthCheckAsyn
try
{
- IHealthCheck check = registration.Factory(serviceProvider);
+ // Match the behavior of ASP.NET's HealthCheckService, which creates a scope for each check.
+ await using AsyncServiceScope serviceScope = serviceProvider.CreateAsyncScope();
+
+ IHealthCheck check = registration.Factory(serviceScope.ServiceProvider);
MicrosoftHealthCheckResult result = await check.CheckHealthAsync(context, cancellationToken);
healthCheckResult.Status = ToHealthStatus(result.Status);
diff --git a/src/Common/src/Common/HealthChecks/HealthCheckResult.cs b/src/Common/src/Common/HealthChecks/HealthCheckResult.cs
index 473c5e9936..8d967097e6 100644
--- a/src/Common/src/Common/HealthChecks/HealthCheckResult.cs
+++ b/src/Common/src/Common/HealthChecks/HealthCheckResult.cs
@@ -19,6 +19,7 @@ public sealed class HealthCheckResult
///
/// Used by the health middleware to determine the HTTP Status code.
///
+ [JsonPropertyName("status")]
[JsonConverter(typeof(SnakeCaseAllCapsEnumMemberJsonConverter))]
public HealthStatus Status { get; set; } = HealthStatus.Unknown;
@@ -28,11 +29,13 @@ public sealed class HealthCheckResult
///
/// Currently only used on check failures.
///
+ [JsonPropertyName("description")]
public string? Description { get; set; }
///
/// Gets details of the health check.
///
+ [JsonPropertyName("details")]
[JsonIgnoreEmptyCollection]
public IDictionary Details { get; } = new Dictionary();
}
diff --git a/src/Common/src/Common/Net/DomainNameResolver.cs b/src/Common/src/Common/Net/DomainNameResolver.cs
index 0b1da12ddd..30fd0e55bd 100644
--- a/src/Common/src/Common/Net/DomainNameResolver.cs
+++ b/src/Common/src/Common/Net/DomainNameResolver.cs
@@ -9,6 +9,8 @@ namespace Steeltoe.Common.Net;
internal sealed class DomainNameResolver : IDomainNameResolver
{
+ private static readonly bool IsInDiagnosticsMode = Environment.GetEnvironmentVariable("STEELTOE_MACOS_DIAGNOSE_HOSTNAME_LOOKUP") == "true";
+
public static DomainNameResolver Instance { get; } = new();
private DomainNameResolver()
@@ -42,22 +44,44 @@ private DomainNameResolver()
public string? ResolveHostName(bool throwOnError = false)
{
+ // Gather diagnostic information to investigate intermittent failures on macOS.
+ string? resultFromGetHostName = null;
+ string? resultFromGetHostEntry = null;
+ bool? workaroundApplied = null;
+
try
{
string hostName = Dns.GetHostName();
+ resultFromGetHostName = hostName;
if (string.IsNullOrEmpty(hostName))
{
// Workaround for failure when running on macOS.
// See https://github.com/actions/runner-images/issues/1335 and https://github.com/dotnet/runtime/issues/36849.
+
hostName = "localhost";
+ workaroundApplied = true;
}
IPHostEntry hostEntry = Dns.GetHostEntry(hostName);
- return hostEntry.HostName;
+ resultFromGetHostEntry = hostEntry.HostName;
+
+ if (IsInDiagnosticsMode && string.IsNullOrEmpty(resultFromGetHostEntry))
+ {
+ throw new InvalidOperationException($"IPHostEntry.HostName returned {GetTextFor(resultFromGetHostEntry)}.");
+ }
+
+ return resultFromGetHostEntry;
}
- catch (Exception)
+ catch (Exception exception)
{
+ if (IsInDiagnosticsMode)
+ {
+ throw new InvalidOperationException(
+ $"Failed to resolve hostname. GetHostName={GetTextFor(resultFromGetHostName)}, GetHostEntry={GetTextFor(resultFromGetHostEntry)}, WorkaroundApplied={workaroundApplied}",
+ exception);
+ }
+
if (throwOnError)
{
throw;
@@ -66,4 +90,14 @@ private DomainNameResolver()
return null;
}
}
+
+ private static string GetTextFor(string? value)
+ {
+ if (value == null)
+ {
+ return "(null)";
+ }
+
+ return value.Length == 0 ? "(empty)" : value;
+ }
}
diff --git a/src/Common/src/Common/Net/InetUtils.cs b/src/Common/src/Common/Net/InetUtils.cs
index e821256786..47bf5c0b79 100644
--- a/src/Common/src/Common/Net/InetUtils.cs
+++ b/src/Common/src/Common/Net/InetUtils.cs
@@ -13,8 +13,11 @@ namespace Steeltoe.Common.Net;
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
// Non-sealed because this type is mocked by tests.
-internal class InetUtils
+internal partial class InetUtils
{
+ private const RegexOptions InetRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture;
+ private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(1);
+
private readonly IDomainNameResolver _domainNameResolver;
private readonly IOptionsMonitor _optionsMonitor;
private readonly ILogger _logger;
@@ -62,7 +65,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo()
{
if (networkInterface is { OperationalStatus: OperationalStatus.Up, IsReceiveOnly: false })
{
- _logger.LogTrace("Testing interface: {Name}, {Id}", networkInterface.Name, networkInterface.Id);
+ LogTestingInterface(networkInterface.Name, networkInterface.Id);
IPInterfaceProperties properties = networkInterface.GetIPProperties();
IPv4InterfaceProperties iPv4Properties = properties.GetIPv4Properties();
@@ -84,7 +87,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo()
if (IsInet4Address(address) && !IsLoopbackAddress(address) && IsPreferredAddress(address, inetOptions))
{
- _logger.LogTrace("Found non-loopback interface: {Name}", networkInterface.Name);
+ LogNonLoopbackInterfaceFound(networkInterface.Name);
result = address;
}
}
@@ -94,7 +97,7 @@ public virtual HostInfo FindFirstNonLoopbackHostInfo()
}
catch (Exception exception)
{
- _logger.LogError(exception, "Cannot get first non-loopback address");
+ LogCannotGetNonLoopbackAddress(exception);
}
if (result != null)
@@ -123,7 +126,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions)
if (!siteLocalAddress)
{
- _logger.LogTrace("Ignoring address: {Address} [UseOnlySiteLocalInterfaces=true, this address is not]", address);
+ LogIgnoringNonSiteLocalAddress(address);
}
return siteLocalAddress;
@@ -139,7 +142,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions)
foreach (string regex in preferredNetworks)
{
string hostAddress = address.ToString();
- var matcher = new Regex(regex, RegexOptions.None, TimeSpan.FromSeconds(1));
+ var matcher = new Regex(regex, InetRegexOptions, RegexMatchTimeout);
if (matcher.IsMatch(hostAddress) || hostAddress.StartsWith(regex, StringComparison.Ordinal))
{
@@ -147,7 +150,7 @@ internal bool IsPreferredAddress(IPAddress address, InetOptions inetOptions)
}
}
- _logger.LogTrace("Ignoring address: {Address}", address);
+ LogIgnoringAddress(address);
return false;
}
@@ -160,11 +163,11 @@ internal bool IgnoreInterface(string interfaceName, InetOptions inetOptions)
foreach (string regex in inetOptions.GetIgnoredInterfaces())
{
- var matcher = new Regex(regex, RegexOptions.None, TimeSpan.FromSeconds(1));
+ var matcher = new Regex(regex, InetRegexOptions, RegexMatchTimeout);
if (matcher.IsMatch(interfaceName))
{
- _logger.LogTrace("Ignoring interface: {Name}", interfaceName);
+ LogIgnoringInterface(interfaceName);
return true;
}
}
@@ -186,7 +189,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions)
}
catch (Exception exception)
{
- _logger.LogInformation(exception, "Cannot determine local hostname.");
+ LogCannotDetermineHostname(exception);
hostname = "localhost";
}
}
@@ -220,7 +223,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions)
}
catch (Exception exception)
{
- _logger.LogWarning(exception, "Unable to resolve host address.");
+ LogUnableToResolveHostAddress(exception);
}
return result;
@@ -234,7 +237,7 @@ internal HostInfo ConvertAddress(IPAddress address, InetOptions inetOptions)
}
catch (Exception exception)
{
- _logger.LogWarning(exception, "Unable to resolve hostname.");
+ LogUnableToResolveHostname(exception);
return null;
}
}
@@ -252,4 +255,32 @@ private static bool IsSiteLocalAddress(IPAddress address)
return text.StartsWith("10.", StringComparison.Ordinal) || text.StartsWith("172.16.", StringComparison.Ordinal) ||
text.StartsWith("192.168.", StringComparison.Ordinal);
}
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Testing interface {Name} with ID {Id}.")]
+ private partial void LogTestingInterface(string name, string id);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Found non-loopback interface {Name}.")]
+ private partial void LogNonLoopbackInterfaceFound(string name);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Cannot get first non-loopback address.")]
+ private partial void LogCannotGetNonLoopbackAddress(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Trace,
+ Message = "Ignoring address {Address} because UseOnlySiteLocalInterfaces is true and this address is not site-local.")]
+ private partial void LogIgnoringNonSiteLocalAddress(IPAddress address);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Ignoring address {Address}.")]
+ private partial void LogIgnoringAddress(IPAddress address);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Ignoring interface {Name}.")]
+ private partial void LogIgnoringInterface(string name);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Cannot determine local hostname.")]
+ private partial void LogCannotDetermineHostname(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Unable to resolve host address.")]
+ private partial void LogUnableToResolveHostAddress(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Unable to resolve hostname.")]
+ private partial void LogUnableToResolveHostname(Exception exception);
}
diff --git a/src/Common/src/Common/Properties/AssemblyInfo.cs b/src/Common/src/Common/Properties/AssemblyInfo.cs
index 61e3dec787..cb106ca1a6 100644
--- a/src/Common/src/Common/Properties/AssemblyInfo.cs
+++ b/src/Common/src/Common/Properties/AssemblyInfo.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
@@ -11,13 +12,18 @@
[assembly: InternalsVisibleTo("Steeltoe.Common.Hosting")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Hosting.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Http")]
+[assembly: InternalsVisibleTo("Steeltoe.Common.Logging")]
+[assembly: InternalsVisibleTo("Steeltoe.Common.Net")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Test")]
+[assembly: InternalsVisibleTo("Steeltoe.Configuration.Abstractions")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.Encryption")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.Kubernetes.ServiceBindings")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.Placeholder")]
+[assembly: InternalsVisibleTo("Steeltoe.Configuration.RandomValue")]
+[assembly: InternalsVisibleTo("Steeltoe.Configuration.SpringBoot")]
[assembly: InternalsVisibleTo("Steeltoe.Connectors")]
[assembly: InternalsVisibleTo("Steeltoe.Connectors.EntityFrameworkCore")]
[assembly: InternalsVisibleTo("Steeltoe.Discovery.Configuration")]
@@ -38,6 +44,11 @@
[assembly: InternalsVisibleTo("Steeltoe.Management.Tasks")]
[assembly: InternalsVisibleTo("Steeltoe.Management.Tracing")]
[assembly: InternalsVisibleTo("Steeltoe.Management.Tracing.Test")]
+[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.JwtBearer")]
[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.CloudFoundry.Test")]
+[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.OpenIdConnect")]
[assembly: InternalsVisibleTo("Steeltoe.Security.Authorization.Certificate")]
+[assembly: InternalsVisibleTo("Steeltoe.Security.DataProtection.Redis")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common")]
diff --git a/src/Common/src/Common/PublicAPI.Shipped.txt b/src/Common/src/Common/PublicAPI.Shipped.txt
index abdf747864..a9e918a0f5 100644
--- a/src/Common/src/Common/PublicAPI.Shipped.txt
+++ b/src/Common/src/Common/PublicAPI.Shipped.txt
@@ -19,17 +19,24 @@ Steeltoe.Common.CasingConventions.SnakeCaseAllCapsEnumMemberJsonConverter.SnakeC
Steeltoe.Common.CasingConventions.SnakeCaseStyle
Steeltoe.Common.CasingConventions.SnakeCaseStyle.AllCaps = 0 -> Steeltoe.Common.CasingConventions.SnakeCaseStyle
Steeltoe.Common.CasingConventions.SnakeCaseStyle.NoCaps = 1 -> Steeltoe.Common.CasingConventions.SnakeCaseStyle
+Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs
+Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs.DiscoveryInstancesFetchedEventArgs(System.Collections.Generic.IReadOnlyDictionary!>! instancesByServiceId) -> void
+Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs.InstancesByServiceId.get -> System.Collections.Generic.IReadOnlyDictionary!>!
Steeltoe.Common.Discovery.IDiscoveryClient
Steeltoe.Common.Discovery.IDiscoveryClient.Description.get -> string!
Steeltoe.Common.Discovery.IDiscoveryClient.GetInstancesAsync(string! serviceId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>!
Steeltoe.Common.Discovery.IDiscoveryClient.GetLocalServiceInstance() -> Steeltoe.Common.Discovery.IServiceInstance?
Steeltoe.Common.Discovery.IDiscoveryClient.GetServiceIdsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>!
+Steeltoe.Common.Discovery.IDiscoveryClient.InstancesFetched -> System.EventHandler!
Steeltoe.Common.Discovery.IDiscoveryClient.ShutdownAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Steeltoe.Common.Discovery.IServiceInstance
Steeltoe.Common.Discovery.IServiceInstance.Host.get -> string!
+Steeltoe.Common.Discovery.IServiceInstance.InstanceId.get -> string!
Steeltoe.Common.Discovery.IServiceInstance.IsSecure.get -> bool
Steeltoe.Common.Discovery.IServiceInstance.Metadata.get -> System.Collections.Generic.IReadOnlyDictionary!
+Steeltoe.Common.Discovery.IServiceInstance.NonSecureUri.get -> System.Uri?
Steeltoe.Common.Discovery.IServiceInstance.Port.get -> int
+Steeltoe.Common.Discovery.IServiceInstance.SecureUri.get -> System.Uri?
Steeltoe.Common.Discovery.IServiceInstance.ServiceId.get -> string!
Steeltoe.Common.Discovery.IServiceInstance.Uri.get -> System.Uri!
Steeltoe.Common.Extensions.ServiceCollectionExtensions
diff --git a/src/Common/src/Common/Steeltoe.Common.csproj b/src/Common/src/Common/Steeltoe.Common.csproj
index 902abac125..5df742c940 100644
--- a/src/Common/src/Common/Steeltoe.Common.csproj
+++ b/src/Common/src/Common/Steeltoe.Common.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Abstractions and code shared by many Steeltoe components.
common;utility
true
diff --git a/src/Common/src/Hosting/ConfigurationSchema.json b/src/Common/src/Hosting/ConfigurationSchema.json
new file mode 100644
index 0000000000..3ca1828da6
--- /dev/null
+++ b/src/Common/src/Hosting/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common.Hosting": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Hosting/HostBuilderWrapper.cs b/src/Common/src/Hosting/HostBuilderWrapper.cs
index 0766d64d03..07ee6af1c8 100644
--- a/src/Common/src/Hosting/HostBuilderWrapper.cs
+++ b/src/Common/src/Hosting/HostBuilderWrapper.cs
@@ -86,14 +86,14 @@ private static void InvokeDeferredActions(IEnumerable configureAction)
+ public void ConfigureServices(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
- return ConfigureServices((_, services) => configureAction(services));
+ ConfigureServices((_, services) => configureAction(services));
}
- public HostBuilderWrapper ConfigureServices(Action configureAction)
+ public void ConfigureServices(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
@@ -106,18 +106,16 @@ public HostBuilderWrapper ConfigureServices(Action configureAction)
+ public void ConfigureAppConfiguration(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
- return ConfigureAppConfiguration((_, configurationBuilder) => configureAction(configurationBuilder));
+ ConfigureAppConfiguration((_, configurationBuilder) => configureAction(configurationBuilder));
}
- public HostBuilderWrapper ConfigureAppConfiguration(Action configureAction)
+ public void ConfigureAppConfiguration(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
@@ -130,18 +128,16 @@ public HostBuilderWrapper ConfigureAppConfiguration(Action configureAction)
+ public void ConfigureLogging(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
- return ConfigureLogging((_, configurationBuilder) => configureAction(configurationBuilder));
+ ConfigureLogging((_, configurationBuilder) => configureAction(configurationBuilder));
}
- public HostBuilderWrapper ConfigureLogging(Action configureAction)
+ public void ConfigureLogging(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
@@ -156,11 +152,9 @@ public HostBuilderWrapper ConfigureLogging(Action collection.AddLogging(builder => configureAction(contextWrapper, builder)));
#pragma warning restore S4792 // Configuring loggers is security-sensitive
}
-
- return this;
}
- public HostBuilderWrapper ConfigureWebHost(Action configureAction)
+ public void ConfigureWebHost(Action configureAction)
{
ArgumentNullException.ThrowIfNull(configureAction);
@@ -184,7 +178,5 @@ public HostBuilderWrapper ConfigureWebHost(Action configureActi
{
throw new NotSupportedException($"Unknown host builder type '{_innerBuilder.GetType()}'.");
}
-
- return this;
}
}
diff --git a/src/Common/src/Hosting/Properties/AssemblyInfo.cs b/src/Common/src/Hosting/Properties/AssemblyInfo.cs
index c073f1765b..cdcc9e9e37 100644
--- a/src/Common/src/Hosting/Properties/AssemblyInfo.cs
+++ b/src/Common/src/Hosting/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common", "Steeltoe.Common.Hosting")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Hosting.Test")]
diff --git a/src/Common/src/Hosting/Steeltoe.Common.Hosting.csproj b/src/Common/src/Hosting/Steeltoe.Common.Hosting.csproj
index aec68f1ecc..329b5c48bb 100644
--- a/src/Common/src/Hosting/Steeltoe.Common.Hosting.csproj
+++ b/src/Common/src/Hosting/Steeltoe.Common.Hosting.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Steeltoe library for commonly-used ASP.NET Core hosting-related functions.
hosting
true
diff --git a/src/Common/src/Http/ConfigurationSchema.json b/src/Common/src/Http/ConfigurationSchema.json
new file mode 100644
index 0000000000..be28e95f0f
--- /dev/null
+++ b/src/Common/src/Http/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common.Http": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Http/HttpClientExtensions.cs b/src/Common/src/Http/HttpClientExtensions.cs
index e00b96413a..9888094f25 100644
--- a/src/Common/src/Http/HttpClientExtensions.cs
+++ b/src/Common/src/Http/HttpClientExtensions.cs
@@ -73,7 +73,8 @@ public static async Task GetAccessTokenAsync(this HttpClient httpClient,
if (string.IsNullOrEmpty(accessToken))
{
- throw new HttpRequestException($"No access token was returned from '{accessTokenUri.ToMaskedString()}'.", null, response.StatusCode);
+ MaskedUri masked = accessTokenUri;
+ throw new HttpRequestException($"No access token was returned from '{masked}'.", null, response.StatusCode);
}
return accessToken;
diff --git a/src/Common/src/Http/Properties/AssemblyInfo.cs b/src/Common/src/Http/Properties/AssemblyInfo.cs
index 313de07940..0978e3a5c1 100644
--- a/src/Common/src/Http/Properties/AssemblyInfo.cs
+++ b/src/Common/src/Http/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common", "Steeltoe.Common.Http")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Http.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer")]
diff --git a/src/Common/src/Http/Steeltoe.Common.Http.csproj b/src/Common/src/Http/Steeltoe.Common.Http.csproj
index 21c4ac1934..0a6b1fbc0c 100644
--- a/src/Common/src/Http/Steeltoe.Common.Http.csproj
+++ b/src/Common/src/Http/Steeltoe.Common.Http.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Shared code related to HTTP, primarily for working with HttpClient.
http
true
diff --git a/src/Common/src/Logging/BootstrapLoggerFactory.cs b/src/Common/src/Logging/BootstrapLoggerFactory.cs
index 3e74ffd9a1..2ad4ad0ad8 100644
--- a/src/Common/src/Logging/BootstrapLoggerFactory.cs
+++ b/src/Common/src/Logging/BootstrapLoggerFactory.cs
@@ -4,6 +4,13 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using LockPrimitive =
+#if NET10_0_OR_GREATER
+ System.Threading.Lock
+#else
+ object
+#endif
+ ;
namespace Steeltoe.Common.Logging;
@@ -30,7 +37,7 @@ public sealed class BootstrapLoggerFactory : ILoggerFactory
loggingBuilder.AddConfiguration(configuration);
};
- private readonly object _lock = new();
+ private readonly LockPrimitive _lock = new();
private readonly Dictionary _loggersByCategoryName = [];
private ILoggerFactory _innerFactory;
@@ -51,7 +58,7 @@ public static BootstrapLoggerFactory CreateConsole()
/// Creates a new that writes to the console.
///
///
- /// Enables to further configure the bootstrap logger from code.
+ /// Enables further configuring the bootstrap logger from code.
///
public static BootstrapLoggerFactory CreateConsole(Action configure)
{
@@ -68,7 +75,7 @@ public static BootstrapLoggerFactory CreateConsole(Action confi
/// Creates a new empty .
///
///
- /// Enables to fully configure the bootstrap logger from code.
+ /// Enables fully configuring the bootstrap logger from code.
///
public static BootstrapLoggerFactory CreateEmpty(Action configure)
{
diff --git a/src/Common/src/Logging/ConfigurationSchema.json b/src/Common/src/Logging/ConfigurationSchema.json
new file mode 100644
index 0000000000..4dc17d7a1f
--- /dev/null
+++ b/src/Common/src/Logging/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common.Logging": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Logging/Properties/AssemblyInfo.cs b/src/Common/src/Logging/Properties/AssemblyInfo.cs
index d6487d6f38..156a9a3d45 100644
--- a/src/Common/src/Logging/Properties/AssemblyInfo.cs
+++ b/src/Common/src/Logging/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common", "Steeltoe.Common.Logging")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Logging.Test")]
diff --git a/src/Common/src/Logging/Steeltoe.Common.Logging.csproj b/src/Common/src/Logging/Steeltoe.Common.Logging.csproj
index 7525f85ba4..43274bcead 100644
--- a/src/Common/src/Logging/Steeltoe.Common.Logging.csproj
+++ b/src/Common/src/Logging/Steeltoe.Common.Logging.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Abstractions and code for bootstrap logging, before the IoC container is built. Bootstrap logging is replaced by the runtime logging (what the rest of your application is using) after the IoC container is built.
bootstrap;logging
true
diff --git a/src/Common/src/Net/ConfigurationSchema.json b/src/Common/src/Net/ConfigurationSchema.json
new file mode 100644
index 0000000000..d1b98cae74
--- /dev/null
+++ b/src/Common/src/Net/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common.Net": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Net/Properties/AssemblyInfo.cs b/src/Common/src/Net/Properties/AssemblyInfo.cs
index c2d054e7ae..ac25e802b1 100644
--- a/src/Common/src/Net/Properties/AssemblyInfo.cs
+++ b/src/Common/src/Net/Properties/AssemblyInfo.cs
@@ -3,5 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common", "Steeltoe.Common.Net")]
[assembly: InternalsVisibleTo("Steeltoe.Common.Net.Test")]
diff --git a/src/Common/src/Net/Steeltoe.Common.Net.csproj b/src/Common/src/Net/Steeltoe.Common.Net.csproj
index 7e03a8fcb4..89b4174c1e 100644
--- a/src/Common/src/Net/Steeltoe.Common.Net.csproj
+++ b/src/Common/src/Net/Steeltoe.Common.Net.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Abstractions and code for using Windows network files shares.
Windows-file-sharing;SMB;CIFS
true
diff --git a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs
index 876c1566a6..080e005b87 100644
--- a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs
+++ b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs
@@ -2,9 +2,9 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -50,10 +50,9 @@ public void ConfigureCertificateOptions_BadPath_NoCertificate(string certificate
options.Certificate.Should().BeNull();
}
- [Theory]
+ [TheorySkippedOnPlatform(nameof(OSPlatform.OSX))]
[InlineData("")]
[InlineData(CertificateName)]
- [Trait("Category", "SkipOnMacOS")]
public void ConfigureCertificateOptions_ThrowsOnEmptyFile(string certificateName)
{
var appSettings = new Dictionary
@@ -149,7 +148,7 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica
string secondPrivateKeyContent = await File.ReadAllTextAsync("secondInstance.key", TestContext.Current.CancellationToken);
using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key");
string appSettings = BuildAppSettingsJson(certificateName, certificateFilePath, privateKeyFilePath);
- string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
+ string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings);
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile(appSettingsPath, false, true);
IConfiguration configuration = configurationBuilder.Build();
@@ -164,11 +163,11 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica
var optionsMonitor = serviceProvider.GetRequiredService>();
optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509);
- await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken);
- await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken);
-
- using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken);
- await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () =>
+ {
+ await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken);
+ await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken);
+ });
optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509);
}
@@ -186,7 +185,7 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa
string firstPrivateKeyFilePath = sandbox.CreateFile(Guid.NewGuid() + ".key", firstPrivateKeyContent);
using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key");
string appSettings = BuildAppSettingsJson(certificateName, firstCertificateFilePath, firstPrivateKeyFilePath);
- string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
+ string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings);
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile(appSettingsPath, false, true);
IConfiguration configuration = configurationBuilder.Build();
@@ -201,42 +200,56 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa
var optionsMonitor = serviceProvider.GetRequiredService>();
optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509);
- appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key");
- await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken);
-
- using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken);
- await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () =>
+ {
+ appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key");
+ await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken);
+ });
optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509);
}
private static string BuildAppSettingsJson(string certificateName, string certificatePath, string keyPath)
{
- string certificateBlock = $"""
- "CertificateFilePath": {JsonSerializer.Serialize(certificatePath)},
- "PrivateKeyFilePath": {JsonSerializer.Serialize(keyPath)}
- """;
+ string escapedCertificatePath = certificatePath.Replace(@"\", @"\\", StringComparison.Ordinal);
+ string escapedKeyPath = keyPath.Replace(@"\", @"\\", StringComparison.Ordinal);
- string namedCertificateSection = string.IsNullOrEmpty(certificateName)
- ? certificateBlock
- : $"{JsonSerializer.Serialize(certificateName)}: {{ {certificateBlock} }}";
-
- return $$"""
+ return string.IsNullOrEmpty(certificateName)
+ ? $$"""
{
"Certificates": {
- {{namedCertificateSection}}
+ "CertificateFilePath": "{{escapedCertificatePath}}",
+ "PrivateKeyFilePath": "{{escapedKeyPath}}"
+ }
+ }
+ """
+ : $$"""
+ {
+ "Certificates": {
+ "{{certificateName}}": {
+ "CertificateFilePath": "{{escapedCertificatePath}}",
+ "PrivateKeyFilePath": "{{escapedKeyPath}}"
+ }
}
}
""";
}
- private static async Task WaitUntilCertificateChangedToAsync(X509Certificate2 expectedCertificate, IOptionsMonitor optionsMonitor,
- string certificateName, CancellationToken cancellationToken)
+ private static async Task WaitUntilCertificateChangedToAsync(string certificateName, X509Certificate2 expectedCertificate,
+ IOptionsMonitor optionsMonitor, Func triggerAction)
{
- while (!Equals(optionsMonitor.Get(certificateName).Certificate, expectedCertificate))
+ var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ using IDisposable? changeListener = optionsMonitor.OnChange((options, name) =>
{
- await Task.Delay(50, cancellationToken);
- }
+ if (name == certificateName && Equals(options.Certificate, expectedCertificate))
+ {
+ completionSource.TrySetResult();
+ }
+ });
+
+ await triggerAction();
+ await completionSource.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
private static string GetConfigurationKey(string? optionName, string propertyName)
diff --git a/src/Common/test/Certificates.Test/Steeltoe.Common.Certificates.Test.csproj b/src/Common/test/Certificates.Test/Steeltoe.Common.Certificates.Test.csproj
index b20e9f161e..3397fd9de8 100644
--- a/src/Common/test/Certificates.Test/Steeltoe.Common.Certificates.Test.csproj
+++ b/src/Common/test/Certificates.Test/Steeltoe.Common.Certificates.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs
index 94f5c04d4b..b38457aaf7 100644
--- a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs
+++ b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs
@@ -7,7 +7,6 @@
using Microsoft.Extensions.DependencyInjection;
using Steeltoe.Common.Extensions;
using Steeltoe.Common.TestResources;
-using Steeltoe.Common.TestResources.IO;
namespace Steeltoe.Common.Test;
@@ -24,7 +23,7 @@ public void ConstructorSetsDefaults()
[Fact]
public async Task ReadsApplicationConfiguration()
{
- const string configJson = """
+ const string appSettings = """
{
"Spring": {
"Application": {
@@ -34,13 +33,11 @@ public async Task ReadsApplicationConfiguration()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, configJson);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
+
var builder = new ConfigurationBuilder();
- builder.SetBasePath(directory);
- builder.AddJsonFile(fileName);
+ builder.AddInMemoryAppSettingsJsonFile(fileProvider);
IConfiguration configuration = builder.Build();
var services = new ServiceCollection();
diff --git a/src/Common/test/Common.Test/Extensions/MaskedUriTest.cs b/src/Common/test/Common.Test/Extensions/MaskedUriTest.cs
new file mode 100644
index 0000000000..780116a78a
--- /dev/null
+++ b/src/Common/test/Common.Test/Extensions/MaskedUriTest.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Steeltoe.Common.Extensions;
+
+namespace Steeltoe.Common.Test.Extensions;
+
+public sealed class MaskedUriTest
+{
+ [Fact]
+ public void MaskSingleBasicAuthentication()
+ {
+ const string source = "http://username:password@www.example.com/";
+ const string expected = "http://****:****@www.example.com/";
+
+ MaskedUri uri = new Uri(source);
+
+ uri.ToString().Should().Be(expected);
+ }
+
+ [Fact]
+ public void MaskMultiBasicAuthentication()
+ {
+ const string source = "http://username:password@www.example.com/,http://user2:pass2@www.other.com/";
+ const string expected = "http://****:****@www.example.com/,http://****:****@www.other.com/";
+
+ MaskedUri uri = new Uri(source);
+
+ uri.ToString().Should().Be(expected);
+ }
+
+ [Fact]
+ public void DoNotMaskIfNoBasicAuthentication()
+ {
+ const string source = "http://www.example.com/";
+ const string expected = "http://www.example.com/";
+
+ MaskedUri uri = new Uri(source);
+
+ uri.ToString().Should().Be(expected);
+ }
+}
diff --git a/src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs b/src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs
index fbfa1a9062..dc660af72c 100644
--- a/src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs
+++ b/src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs
@@ -8,39 +8,6 @@ namespace Steeltoe.Common.Test.Extensions;
public sealed class UriExtensionsTest
{
- [Fact]
- public void MaskSingleBasicAuthentication()
- {
- var uri = new Uri("http://username:password@www.example.com/");
- const string expected = "http://****:****@www.example.com/";
-
- string masked = uri.ToMaskedString();
-
- masked.Should().Be(expected);
- }
-
- [Fact]
- public void MaskMultiBasicAuthentication()
- {
- var uri = new Uri("http://username:password@www.example.com/,http://user2:pass2@www.other.com/");
- const string expected = "http://****:****@www.example.com/,http://****:****@www.other.com/";
-
- string masked = uri.ToMaskedString();
-
- masked.Should().Be(expected);
- }
-
- [Fact]
- public void DoNotMaskIfNoBasicAuthentication()
- {
- var uri = new Uri("http://www.example.com/");
- string expected = uri.ToString();
-
- string masked = uri.ToMaskedString();
-
- masked.Should().Be(expected);
- }
-
[Fact]
public void TryGetUsernamePassword_AllowsColonInPassword()
{
diff --git a/src/Common/test/Common.Test/Steeltoe.Common.Test.csproj b/src/Common/test/Common.Test/Steeltoe.Common.Test.csproj
index 8180cf05ca..11c155dabe 100644
--- a/src/Common/test/Common.Test/Steeltoe.Common.Test.csproj
+++ b/src/Common/test/Common.Test/Steeltoe.Common.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/Hosting.Test/HostBuilderWrapperTest.cs b/src/Common/test/Hosting.Test/HostBuilderWrapperTest.cs
index e9bebf9794..65e138ca66 100644
--- a/src/Common/test/Hosting.Test/HostBuilderWrapperTest.cs
+++ b/src/Common/test/Hosting.Test/HostBuilderWrapperTest.cs
@@ -22,13 +22,14 @@ public async Task WebApplicationBuilder_Wraps()
["foo"] = "bar"
};
- var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
+ using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
wrapper.ConfigureServices(services => services.AddSingleton());
wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings));
+ // ReSharper disable once AccessToDisposedClosure
wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddProvider(capturingLoggerProvider));
wrapper.ConfigureWebHost(hostBuilder => hostBuilder.UseUrls("http://*:8888"));
wrapper.ConfigureServices((contextWrapper, _) => contextWrapper.HostEnvironment.ApplicationName = "TestApp");
@@ -57,13 +58,14 @@ public void HostApplicationBuilder_Wraps()
["foo"] = "bar"
};
- var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
+ using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
HostApplicationBuilder builder = TestHostApplicationBuilderFactory.Create();
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
wrapper.ConfigureServices(services => services.AddSingleton());
wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings));
+ // ReSharper disable once AccessToDisposedClosure
wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddProvider(capturingLoggerProvider));
wrapper.ConfigureServices((contextWrapper, _) => contextWrapper.HostEnvironment.ApplicationName = "TestApp");
@@ -91,13 +93,14 @@ public void WebHostBuilder_Wraps()
["foo"] = "bar"
};
- var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
+ using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
WebHostBuilder builder = TestWebHostBuilderFactory.Create();
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
wrapper.ConfigureServices(services => services.AddSingleton());
wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings));
+ // ReSharper disable once AccessToDisposedClosure
wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddProvider(capturingLoggerProvider));
wrapper.ConfigureWebHost(hostBuilder => hostBuilder.UseUrls("http://*:8888"));
wrapper.ConfigureServices((contextWrapper, _) => contextWrapper.HostEnvironment.ApplicationName = "TestApp");
@@ -128,13 +131,14 @@ public void GenericHostBuilder_Wraps()
["foo"] = "bar"
};
- var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
+ using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Test", StringComparison.Ordinal));
HostBuilder builder = TestHostBuilderFactory.CreateWeb();
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
wrapper.ConfigureServices(services => services.AddSingleton());
wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings));
+ // ReSharper disable once AccessToDisposedClosure
wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddProvider(capturingLoggerProvider));
wrapper.ConfigureWebHost(hostBuilder => hostBuilder.UseUrls("http://*:8888"));
wrapper.ConfigureServices((contextWrapper, _) => contextWrapper.HostEnvironment.ApplicationName = "TestApp");
diff --git a/src/Common/test/Hosting.Test/Steeltoe.Common.Hosting.Test.csproj b/src/Common/test/Hosting.Test/Steeltoe.Common.Hosting.Test.csproj
index ff20f8c687..c64bb90b71 100644
--- a/src/Common/test/Hosting.Test/Steeltoe.Common.Hosting.Test.csproj
+++ b/src/Common/test/Hosting.Test/Steeltoe.Common.Hosting.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/Http.Test/Steeltoe.Common.Http.Test.csproj b/src/Common/test/Http.Test/Steeltoe.Common.Http.Test.csproj
index eb735987a2..b119974f75 100644
--- a/src/Common/test/Http.Test/Steeltoe.Common.Http.Test.csproj
+++ b/src/Common/test/Http.Test/Steeltoe.Common.Http.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/Logging.Test/BootstrapperLoggerFactoryTest.cs b/src/Common/test/Logging.Test/BootstrapperLoggerFactoryTest.cs
index 71bc7278f8..79ea85acc6 100644
--- a/src/Common/test/Logging.Test/BootstrapperLoggerFactoryTest.cs
+++ b/src/Common/test/Logging.Test/BootstrapperLoggerFactoryTest.cs
@@ -15,11 +15,12 @@ public sealed class BootstrapperLoggerFactoryTest
[Fact]
public async Task Upgrades_existing_loggers()
{
- var capturingLoggerProvider = new CapturingLoggerProvider(categoryName => categoryName.StartsWith("Test", StringComparison.Ordinal));
+ using var capturingLoggerProvider = new CapturingLoggerProvider(categoryName => categoryName.StartsWith("Test", StringComparison.Ordinal));
var bootstrapLoggerFactory = BootstrapLoggerFactory.CreateEmpty(loggingBuilder =>
{
loggingBuilder.SetMinimumLevel(LogLevel.Trace);
+ // ReSharper disable once AccessToDisposedClosure
loggingBuilder.AddProvider(capturingLoggerProvider);
});
diff --git a/src/Common/test/Logging.Test/Steeltoe.Common.Logging.Test.csproj b/src/Common/test/Logging.Test/Steeltoe.Common.Logging.Test.csproj
index c7ba7f6ae8..d6a1a9d34e 100644
--- a/src/Common/test/Logging.Test/Steeltoe.Common.Logging.Test.csproj
+++ b/src/Common/test/Logging.Test/Steeltoe.Common.Logging.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/Net.Test/Steeltoe.Common.Net.Test.csproj b/src/Common/test/Net.Test/Steeltoe.Common.Net.Test.csproj
index ede213de15..1b0953f58f 100644
--- a/src/Common/test/Net.Test/Steeltoe.Common.Net.Test.csproj
+++ b/src/Common/test/Net.Test/Steeltoe.Common.Net.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Common/test/TestResources/CapturingLoggerProvider.cs b/src/Common/test/TestResources/CapturingLoggerProvider.cs
index 38825a5a19..5fa4262460 100644
--- a/src/Common/test/TestResources/CapturingLoggerProvider.cs
+++ b/src/Common/test/TestResources/CapturingLoggerProvider.cs
@@ -8,7 +8,7 @@
namespace Steeltoe.Common.TestResources;
///
-/// Enables to capture log messages in tests.
+/// Enables capturing log messages in tests.
///
public sealed class CapturingLoggerProvider : ILoggerProvider
{
diff --git a/src/Common/test/TestResources/EmptyDisposable.cs b/src/Common/test/TestResources/EmptyDisposable.cs
index af2bdd15cd..20f90e22e0 100644
--- a/src/Common/test/TestResources/EmptyDisposable.cs
+++ b/src/Common/test/TestResources/EmptyDisposable.cs
@@ -7,7 +7,7 @@ namespace Steeltoe.Common.TestResources;
///
/// Implements without doing anything.
///
-public sealed class EmptyDisposable : IDisposable
+internal sealed class EmptyDisposable : IDisposable
{
public static IDisposable Instance { get; } = new EmptyDisposable();
diff --git a/src/Common/test/TestResources/EnvironmentVariableScope.cs b/src/Common/test/TestResources/EnvironmentVariableScope.cs
index 8925d7436a..6d93899ff1 100644
--- a/src/Common/test/TestResources/EnvironmentVariableScope.cs
+++ b/src/Common/test/TestResources/EnvironmentVariableScope.cs
@@ -5,7 +5,7 @@
namespace Steeltoe.Common.TestResources;
///
-/// Enables to temporarily set/change an environment variable from a test. The original value is restored when disposed.
+/// Enables temporarily setting/changing an environment variable from a test. The original value is restored when disposed.
///
public sealed class EnvironmentVariableScope : IDisposable
{
diff --git a/src/Common/test/TestResources/FactSkippedOnPlatformAttribute.cs b/src/Common/test/TestResources/FactSkippedOnPlatformAttribute.cs
new file mode 100644
index 0000000000..b96559158c
--- /dev/null
+++ b/src/Common/test/TestResources/FactSkippedOnPlatformAttribute.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+using Xunit;
+using Xunit.v3;
+
+namespace Steeltoe.Common.TestResources;
+
+///
+/// Skips running the decorated test on the specified platforms.
+///
+///
+/// A common reason for skipping tests on macOS is because the ASP.NET dev certificate is not trusted. See
+/// https://github.com/dotnet/aspnetcore/issues/42273.
+///
+[XunitTestCaseDiscoverer(typeof(FactDiscoverer))]
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class FactSkippedOnPlatformAttribute : FactAttribute
+{
+ public FactSkippedOnPlatformAttribute(params string[] platformNames)
+ {
+ foreach (OSPlatform platform in platformNames.Select(OSPlatform.Create))
+ {
+ if (RuntimeInformation.IsOSPlatform(platform))
+ {
+ Skip = $"Skipping test on incompatible platform {platform}.";
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Common/test/TestResources/FluentAssertionsExtensions.cs b/src/Common/test/TestResources/FluentAssertionsExtensions.cs
index ef96ceb3b3..2b41838878 100644
--- a/src/Common/test/TestResources/FluentAssertionsExtensions.cs
+++ b/src/Common/test/TestResources/FluentAssertionsExtensions.cs
@@ -52,4 +52,48 @@ private static string ToJsonString(JsonDocument document)
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
+
+ ///
+ /// Same as the built-in Be() method, but allows specifying a custom comparer.
+ ///
+ ///
+ /// The source text to assert on.
+ ///
+ ///
+ /// The expected text.
+ ///
+ ///
+ /// An equality comparer to compare values.
+ ///
+ [CustomAssertion]
+ public static void Be(this StringAssertions source, string expected, IEqualityComparer comparer)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(expected);
+ ArgumentNullException.ThrowIfNull(comparer);
+
+ object subject = source.Subject;
+ subject.Should().Be(expected, comparer);
+ }
+
+ ///
+ /// Same as the built-in Contain() method, but normalizes line endings upfront.
+ ///
+ ///
+ /// The source text to assert on.
+ ///
+ ///
+ /// The expected text.
+ ///
+ [CustomAssertion]
+ public static void ContainLines(this StringAssertions source, string expected)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(expected);
+
+ string sourceText = source.Subject.ReplaceLineEndings();
+ string expectedText = expected.ReplaceLineEndings();
+
+ sourceText.Should().Contain(expectedText);
+ }
}
diff --git a/src/Common/test/TestResources/IO/Sandbox.cs b/src/Common/test/TestResources/IO/Sandbox.cs
index bdc91ba063..3faa1578f6 100644
--- a/src/Common/test/TestResources/IO/Sandbox.cs
+++ b/src/Common/test/TestResources/IO/Sandbox.cs
@@ -29,30 +29,13 @@ public Sandbox()
///
/// The physical path.
///
- public string ResolvePath(string path)
+ private string ResolvePath(string path)
{
ArgumentNullException.ThrowIfNull(path);
return Path.Combine(FullPath, path);
}
- ///
- /// Creates a directory at the specified path within the sandbox.
- ///
- ///
- /// The directory path.
- ///
- ///
- /// The physical path of the created directory.
- ///
- public string CreateDirectory(string path)
- {
- ArgumentNullException.ThrowIfNull(path);
-
- DirectoryInfo directoryInfo = Directory.CreateDirectory(ResolvePath(path));
- return directoryInfo.FullName;
- }
-
///
/// Creates a file at the specified path within the sandbox.
///
diff --git a/src/Common/test/TestResources/IO/TempDirectory.cs b/src/Common/test/TestResources/IO/TempDirectory.cs
index 3d3238a412..0d353f0cdb 100644
--- a/src/Common/test/TestResources/IO/TempDirectory.cs
+++ b/src/Common/test/TestResources/IO/TempDirectory.cs
@@ -9,21 +9,13 @@ namespace Steeltoe.Common.TestResources.IO;
///
public class TempDirectory : TempPath
{
- ///
- /// Initializes a new instance of the class.
- ///
- public TempDirectory()
- : base(string.Empty)
- {
- }
-
///
/// Initializes a new instance of the class.
///
///
/// Directory name prefix.
///
- public TempDirectory(string prefix)
+ protected TempDirectory(string prefix)
: base(prefix)
{
}
diff --git a/src/Common/test/TestResources/IO/TempFile.cs b/src/Common/test/TestResources/IO/TempFile.cs
index 56e6b90da4..0b9a352a28 100644
--- a/src/Common/test/TestResources/IO/TempFile.cs
+++ b/src/Common/test/TestResources/IO/TempFile.cs
@@ -17,17 +17,6 @@ public TempFile()
{
}
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// File name prefix.
- ///
- public TempFile(string prefix)
- : base(prefix)
- {
- }
-
///
/// Creates the temporary file.
///
diff --git a/src/Common/test/TestResources/MemoryFileProvider.cs b/src/Common/test/TestResources/MemoryFileProvider.cs
index d8141e3a8a..e28f45a91d 100644
--- a/src/Common/test/TestResources/MemoryFileProvider.cs
+++ b/src/Common/test/TestResources/MemoryFileProvider.cs
@@ -12,8 +12,6 @@ namespace Steeltoe.Common.TestResources;
public sealed class MemoryFileProvider : IFileProvider
{
- public const string DefaultAppSettingsFileName = "appsettings.json";
-
private static readonly char[] DirectorySeparators =
[
Path.DirectorySeparatorChar,
diff --git a/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs
new file mode 100644
index 0000000000..de791453a4
--- /dev/null
+++ b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.TestResources;
+
+public static class MemoryFileProviderAppSettingsExtensions
+{
+ public static void IncludeAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents)
+ {
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents);
+ }
+
+ public static void IncludeAppSettingsXmlFile(this MemoryFileProvider fileProvider, string contents)
+ {
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsXmlFileName, contents);
+ }
+
+ public static void IncludeAppSettingsIniFile(this MemoryFileProvider fileProvider, string contents)
+ {
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsIniFileName, contents);
+ }
+
+ public static void ReplaceAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents)
+ {
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ fileProvider.ReplaceFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents);
+ }
+}
diff --git a/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs
new file mode 100644
index 0000000000..fcec5b0ef4
--- /dev/null
+++ b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.Ini;
+using Microsoft.Extensions.Configuration.Json;
+using Microsoft.Extensions.Configuration.Xml;
+
+namespace Steeltoe.Common.TestResources;
+
+public static class MemoryFileProviderConfigurationBuilderExtensions
+{
+ internal const string AppSettingsJsonFileName = "appsettings.json";
+ internal const string AppSettingsXmlFileName = "appsettings.xml";
+ internal const string AppSettingsIniFileName = "appsettings.ini";
+
+ public static void AddInMemoryAppSettingsJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider)
+ {
+ AddInMemoryJsonFile(builder, fileProvider, AppSettingsJsonFileName);
+ }
+
+ public static void AddInMemoryJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider, string path)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(fileProvider);
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ var source = new JsonConfigurationSource
+ {
+ FileProvider = fileProvider,
+ Path = path,
+ Optional = false,
+ ReloadOnChange = true,
+ // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests.
+ ReloadDelay = 0
+ };
+
+ builder.Add(source);
+ }
+
+ public static void AddInMemoryAppSettingsXmlFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ var source = new XmlConfigurationSource
+ {
+ FileProvider = fileProvider,
+ Path = AppSettingsXmlFileName,
+ Optional = false,
+ ReloadOnChange = true,
+ // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests.
+ ReloadDelay = 0
+ };
+
+ builder.Add(source);
+ }
+
+ public static void AddInMemoryAppSettingsIniFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(fileProvider);
+
+ var source = new IniConfigurationSource
+ {
+ FileProvider = fileProvider,
+ Path = AppSettingsIniFileName,
+ Optional = false,
+ ReloadOnChange = true,
+ // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests.
+ ReloadDelay = 0
+ };
+
+ builder.Add(source);
+ }
+}
diff --git a/src/Common/test/TestResources/Steeltoe.Common.TestResources.csproj b/src/Common/test/TestResources/Steeltoe.Common.TestResources.csproj
index 0eb15372e0..a258d5c8ba 100644
--- a/src/Common/test/TestResources/Steeltoe.Common.TestResources.csproj
+++ b/src/Common/test/TestResources/Steeltoe.Common.TestResources.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
false
@@ -11,6 +11,7 @@
+
diff --git a/src/Common/test/TestResources/TestFailureTracer.cs b/src/Common/test/TestResources/TestFailureTracer.cs
index c686a6e4ff..ef763e7d38 100644
--- a/src/Common/test/TestResources/TestFailureTracer.cs
+++ b/src/Common/test/TestResources/TestFailureTracer.cs
@@ -8,7 +8,7 @@
namespace Steeltoe.Common.TestResources;
///
-/// Enables to capture log output in failing tests. Call or use to hook up. When an assertion fails,
+/// Enables capturing log output in failing tests. Call or use to hook up. When an assertion fails,
/// the log output is included in the exception message.
///
public sealed class TestFailureTracer : IDisposable
diff --git a/src/Common/test/TestResources/TestHostBuilderFactory.cs b/src/Common/test/TestResources/TestHostBuilderFactory.cs
index 5821d0c7ae..a4ddb1c0aa 100644
--- a/src/Common/test/TestResources/TestHostBuilderFactory.cs
+++ b/src/Common/test/TestResources/TestHostBuilderFactory.cs
@@ -48,6 +48,7 @@ private static void ConfigureBuilder(HostBuilder builder, bool configureWebHost,
{
builder.ConfigureWebHostDefaults(webHostBuilder =>
{
+ webHostBuilder.UseDefaultServiceProvider(ConfigureServiceProvider);
webHostBuilder.Configure(EmptyAction);
if (useTestServer)
diff --git a/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs b/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs
index 5583766952..f86e176836 100644
--- a/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs
+++ b/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs
@@ -8,6 +8,7 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
namespace Steeltoe.Common.TestResources;
@@ -89,6 +90,7 @@ public static WebApplicationBuilder CreateDefault(bool useTestServer)
private static void ConfigureBuilder(WebApplicationBuilder builder, bool useTestServer, bool deactivateDiagnostics)
{
+ builder.Host.UseDefaultServiceProvider(ConfigureServiceProvider);
builder.WebHost.UseDefaultServiceProvider(ConfigureServiceProvider);
if (useTestServer)
diff --git a/src/Common/test/TestResources/TheorySkippedOnPlatformAttribute.cs b/src/Common/test/TestResources/TheorySkippedOnPlatformAttribute.cs
new file mode 100644
index 0000000000..3e834c1cc1
--- /dev/null
+++ b/src/Common/test/TestResources/TheorySkippedOnPlatformAttribute.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+using Xunit;
+using Xunit.v3;
+
+namespace Steeltoe.Common.TestResources;
+
+///
+/// Skips running the decorated test on the specified platforms.
+///
+///
+/// A common reason for skipping tests on macOS is because the ASP.NET dev certificate is not trusted. See
+/// https://github.com/dotnet/aspnetcore/issues/42273.
+///
+[XunitTestCaseDiscoverer(typeof(TheoryDiscoverer))]
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class TheorySkippedOnPlatformAttribute : TheoryAttribute
+{
+ public TheorySkippedOnPlatformAttribute(params string[] platformNames)
+ {
+ foreach (OSPlatform platform in platformNames.Select(OSPlatform.Create))
+ {
+ if (RuntimeInformation.IsOSPlatform(platform))
+ {
+ Skip = $"Skipping test on incompatible platform {platform}.";
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs
index 2dc24eb071..d7d5d245a2 100644
--- a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs
+++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs
@@ -53,12 +53,10 @@ private void Load(bool isReload)
public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath)
{
- string[] earlierKeysArray = earlierKeys as string[] ?? earlierKeys.ToArray();
-#pragma warning disable S3236 // Caller information arguments should not be provided explicitly
- ArgumentNullException.ThrowIfNull(earlierKeysArray, nameof(earlierKeys));
-#pragma warning restore S3236 // Caller information arguments should not be provided explicitly
+ ArgumentNullException.ThrowIfNull(earlierKeys);
- LogGetChildKeys(GetType().Name, earlierKeysArray, parentPath);
+ string[] earlierKeysArray = earlierKeys as string[] ?? earlierKeys.ToArray();
+ ExpensiveLogGetChildKeys(earlierKeysArray, parentPath);
IConfiguration? section = parentPath == null ? ConfigurationRoot : ConfigurationRoot?.GetSection(parentPath);
@@ -74,14 +72,28 @@ public IEnumerable GetChildKeys(IEnumerable earlierKeys, string?
return keys;
}
+ private void ExpensiveLogGetChildKeys(string[] earlierKeysArray, string? parentPath)
+ {
+ if (_logger.IsEnabled(LogLevel.Trace))
+ {
+ string earlierKeyNames = string.Join(", ", earlierKeysArray.Select(key => $"'{key}'"));
+ LogGetChildKeys(GetType().Name, earlierKeyNames, parentPath);
+ }
+ }
+
public virtual bool TryGet(string key, out string? value)
{
ArgumentNullException.ThrowIfNull(key);
LogTryGet(GetType().Name, key);
- value = ConfigurationRoot?.GetValue(key);
- bool found = value != null;
+ if (ConfigurationRoot == null)
+ {
+ value = null;
+ return false;
+ }
+
+ bool found = InnerTryGet(ConfigurationRoot, key, out value);
if (found)
{
@@ -91,15 +103,37 @@ public virtual bool TryGet(string key, out string? value)
return found;
}
+ private static bool InnerTryGet(IConfigurationRoot root, string key, out string? value)
+ {
+ IList providers = root.Providers as IList ?? root.Providers.ToList();
+
+ for (int index = providers.Count - 1; index >= 0; index--)
+ {
+ IConfigurationProvider provider = providers[index];
+
+ try
+ {
+ if (provider.TryGet(key, out value))
+ {
+ return true;
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // Skip disposed providers to avoid exceptions during access.
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
public void Set(string key, string? value)
{
ArgumentNullException.ThrowIfNull(key);
LogSet(GetType().Name, key, value);
-
-#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
ConfigurationRoot?[key] = value;
-#pragma warning restore S1121 // Assignments should not be made from within sub-expressions
}
public void Dispose()
@@ -128,8 +162,9 @@ protected virtual void Dispose(bool disposing)
[LoggerMessage(Level = LogLevel.Trace, Message = "CreateConfigurationRoot from {Type} with {ProviderCount} providers.")]
private partial void LogCreateConfigurationRoot(string type, int providerCount);
- [LoggerMessage(Level = LogLevel.Trace, Message = "GetChildKeys from {Type} with earlierKeys [{EarlierKeys}] and parentPath '{ParentPath}'.")]
- private partial void LogGetChildKeys(string type, string[] earlierKeys, string? parentPath);
+ [LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true,
+ Message = "GetChildKeys from {Type} with earlierKeys [{EarlierKeys}] and parentPath '{ParentPath}'.")]
+ private partial void LogGetChildKeys(string type, string earlierKeys, string? parentPath);
[LoggerMessage(Level = LogLevel.Trace, Message = "TryGet from {Type} with key '{Key}'.")]
private partial void LogTryGet(string type, string key);
diff --git a/src/Configuration/src/Abstractions/ConfigurationDictionaryMapper.cs b/src/Configuration/src/Abstractions/ConfigurationDictionaryMapper.cs
index 47da88df85..f012711aca 100644
--- a/src/Configuration/src/Abstractions/ConfigurationDictionaryMapper.cs
+++ b/src/Configuration/src/Abstractions/ConfigurationDictionaryMapper.cs
@@ -101,7 +101,7 @@ protected ConfigurationDictionaryMapper(IDictionary configurati
string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using (StreamWriter writer = File.CreateText(tempPath))
+ using (StreamWriter writer = CreateTempFile(tempPath))
{
writer.Write(value);
}
@@ -110,6 +110,26 @@ protected ConfigurationDictionaryMapper(IDictionary configurati
return tempPath;
}
+ private static StreamWriter CreateTempFile(string path)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ // Windows doesn't support setting umask.
+ return File.CreateText(path);
+ }
+
+ var options = new FileStreamOptions
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.Write,
+ Share = FileShare.Read,
+ UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite
+ };
+
+ var fileStream = new FileStream(path, options);
+ return new StreamWriter(fileStream);
+ }
+
public void SetToValue(string toKey, string? value)
{
string key = $"{ToPrefix}{toKey}";
diff --git a/src/Configuration/src/Abstractions/ConfigurationSchema.json b/src/Configuration/src/Abstractions/ConfigurationSchema.json
new file mode 100644
index 0000000000..4e42c281ea
--- /dev/null
+++ b/src/Configuration/src/Abstractions/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.Abstractions": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/Abstractions/PostProcessorConfigurationProvider.cs b/src/Configuration/src/Abstractions/PostProcessorConfigurationProvider.cs
index cfff66a355..89aee900cd 100644
--- a/src/Configuration/src/Abstractions/PostProcessorConfigurationProvider.cs
+++ b/src/Configuration/src/Abstractions/PostProcessorConfigurationProvider.cs
@@ -4,9 +4,11 @@
using Microsoft.Extensions.Configuration;
+#pragma warning disable S3881 // "IDisposable" should be implemented correctly
+
namespace Steeltoe.Configuration;
-internal abstract class PostProcessorConfigurationProvider : ConfigurationProvider
+internal abstract class PostProcessorConfigurationProvider : ConfigurationProvider, IDisposable
{
public PostProcessorConfigurationSource Source { get; }
@@ -24,4 +26,15 @@ protected virtual void PostProcessConfiguration()
processor.PostProcessConfiguration(this, Data);
}
}
+
+ public virtual void Dispose()
+ {
+ foreach (IConfigurationPostProcessor processor in Source.PostProcessors)
+ {
+ if (processor is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
}
diff --git a/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs b/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs
index 0140d8c883..296b2a6cce 100644
--- a/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.Abstractions")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry")]
diff --git a/src/Configuration/src/Abstractions/Steeltoe.Configuration.Abstractions.csproj b/src/Configuration/src/Abstractions/Steeltoe.Configuration.Abstractions.csproj
index f4aaf7ddd5..9efd29ee97 100644
--- a/src/Configuration/src/Abstractions/Steeltoe.Configuration.Abstractions.csproj
+++ b/src/Configuration/src/Abstractions/Steeltoe.Configuration.Abstractions.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Steeltoe.Configuration
Abstractions and code shared by Steeltoe Configuration libraries.
abstractions;configuration
diff --git a/src/Configuration/src/CloudFoundry/CloudFoundryServiceCollectionExtensions.cs b/src/Configuration/src/CloudFoundry/CloudFoundryServiceCollectionExtensions.cs
index 4c7147867f..a53699ed6f 100644
--- a/src/Configuration/src/CloudFoundry/CloudFoundryServiceCollectionExtensions.cs
+++ b/src/Configuration/src/CloudFoundry/CloudFoundryServiceCollectionExtensions.cs
@@ -28,17 +28,25 @@ public static IServiceCollection AddCloudFoundryOptions(this IServiceCollection
{
ArgumentNullException.ThrowIfNull(services);
- services.AddOptions().BindConfiguration("vcap");
+ if (!IsRegistered(services))
+ {
+ services.AddOptions().BindConfiguration("vcap");
- services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureApplicationInstanceInfo>());
- services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCloudFoundryApplicationOptions>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureApplicationInstanceInfo>());
+ services.AddSingleton, ConfigureCloudFoundryApplicationOptions>();
- services.Replace(ServiceDescriptor.Singleton(serviceProvider =>
- {
- var optionsMonitor = serviceProvider.GetRequiredService>();
- return optionsMonitor.CurrentValue;
- }));
+ services.Replace(ServiceDescriptor.Singleton(serviceProvider =>
+ {
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+ return optionsMonitor.CurrentValue;
+ }));
+ }
return services;
}
+
+ private static bool IsRegistered(IServiceCollection services)
+ {
+ return services.Any(descriptor => descriptor.SafeGetImplementationType() == typeof(ConfigureCloudFoundryApplicationOptions));
+ }
}
diff --git a/src/Configuration/src/CloudFoundry/ConfigurationSchema.json b/src/Configuration/src/CloudFoundry/ConfigurationSchema.json
new file mode 100644
index 0000000000..c44c85ae27
--- /dev/null
+++ b/src/Configuration/src/CloudFoundry/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.CloudFoundry": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/CloudFoundry/Properties/AssemblyInfo.cs b/src/Configuration/src/CloudFoundry/Properties/AssemblyInfo.cs
index e15ccb54fe..a9b0b36089 100644
--- a/src/Configuration/src/CloudFoundry/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/CloudFoundry/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.CloudFoundry")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
diff --git a/src/Configuration/src/CloudFoundry/PublicAPI.Shipped.txt b/src/Configuration/src/CloudFoundry/PublicAPI.Shipped.txt
index 0108cbc881..f68e3d8770 100644
--- a/src/Configuration/src/CloudFoundry/PublicAPI.Shipped.txt
+++ b/src/Configuration/src/CloudFoundry/PublicAPI.Shipped.txt
@@ -11,8 +11,10 @@ static Steeltoe.Configuration.CloudFoundry.CloudFoundryHostBuilderExtensions.Add
static Steeltoe.Configuration.CloudFoundry.CloudFoundryHostBuilderExtensions.AddCloudFoundryConfiguration(this Microsoft.Extensions.Hosting.IHostBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder!
static Steeltoe.Configuration.CloudFoundry.CloudFoundryHostBuilderExtensions.AddCloudFoundryConfiguration(this Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHostBuilder!
static Steeltoe.Configuration.CloudFoundry.CloudFoundryServiceCollectionExtensions.AddCloudFoundryOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions.AddCloudFoundryServiceBindings(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes brokerTypes) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions.AddCloudFoundryServiceBindings(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader! serviceBindingsReader) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions.AddCloudFoundryServiceBindings(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Predicate! ignoreKeyPredicate, Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader! serviceBindingsReader, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions.AddCloudFoundryServiceBindings(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Predicate! ignoreKeyPredicate, Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader? serviceBindingsReader, Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes brokerTypes, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions.AddCloudFoundryServiceBindings(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
Steeltoe.Configuration.CloudFoundry.ApplicationLimits
Steeltoe.Configuration.CloudFoundry.ApplicationLimits.ApplicationLimits() -> void
@@ -89,6 +91,17 @@ Steeltoe.Configuration.CloudFoundry.ICloudFoundrySettingsReader.InstanceInternal
Steeltoe.Configuration.CloudFoundry.ICloudFoundrySettingsReader.InstanceIP.get -> string?
Steeltoe.Configuration.CloudFoundry.ICloudFoundrySettingsReader.InstancePort.get -> string?
Steeltoe.Configuration.CloudFoundry.ICloudFoundrySettingsReader.ServicesJson.get -> string?
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.All = Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Eureka | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Identity | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.MongoDb | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.MySql | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.PostgreSql | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.RabbitMQ | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Redis | Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.SqlServer -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Eureka = 1 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Identity = 2 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.MongoDb = 4 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.MySql = 8 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.None = 0 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.PostgreSql = 16 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.RabbitMQ = 32 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.Redis = 64 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
+Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes.SqlServer = 128 -> Steeltoe.Configuration.CloudFoundry.ServiceBindings.CloudFoundryServiceBrokerTypes
Steeltoe.Configuration.CloudFoundry.ServiceBindings.ConfigurationBuilderExtensions
Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader
Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader.GetServiceBindingsJson() -> string?
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBindingConfigurationSource.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBindingConfigurationSource.cs
index ade7b18591..812960c4ce 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBindingConfigurationSource.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBindingConfigurationSource.cs
@@ -2,19 +2,24 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
+using System.Diagnostics;
using Microsoft.Extensions.Configuration;
namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings;
+[DebuggerDisplay("{DebuggerToString(),nq}")]
internal sealed class CloudFoundryServiceBindingConfigurationSource : PostProcessorConfigurationSource, IConfigurationSource
{
private readonly IServiceBindingsReader _serviceBindingsReader;
- public CloudFoundryServiceBindingConfigurationSource(IServiceBindingsReader serviceBindingsReader)
+ public CloudFoundryServiceBrokerTypes BrokerTypes { get; }
+
+ public CloudFoundryServiceBindingConfigurationSource(IServiceBindingsReader serviceBindingsReader, CloudFoundryServiceBrokerTypes brokerTypes)
{
ArgumentNullException.ThrowIfNull(serviceBindingsReader);
_serviceBindingsReader = serviceBindingsReader;
+ BrokerTypes = brokerTypes;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
@@ -24,4 +29,9 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
CaptureConfigurationBuilder(builder);
return new CloudFoundryServiceBindingConfigurationProvider(this, _serviceBindingsReader);
}
+
+ private string DebuggerToString()
+ {
+ return $"{GetType().FullName} ({BrokerTypes})";
+ }
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBrokerTypes.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBrokerTypes.cs
new file mode 100644
index 0000000000..187b30de3b
--- /dev/null
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/CloudFoundryServiceBrokerTypes.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings;
+
+///
+/// Lists the built-in Cloud Foundry service brokers.
+///
+[Flags]
+public enum CloudFoundryServiceBrokerTypes
+{
+ ///
+ /// Don't use any of the built-in brokers.
+ ///
+ None = 0x0,
+
+ ///
+ /// Use the built-in brokers for Netflix Eureka.
+ ///
+ Eureka = 0x1,
+
+ ///
+ /// Use the built-in brokers for JWT and OpenID Connect.
+ ///
+ Identity = 0x2,
+
+ ///
+ /// Use the built-in brokers for MongoDB.
+ ///
+ MongoDb = 0x4,
+
+ ///
+ /// Use the built-in brokers for MySQL.
+ ///
+ MySql = 0x8,
+
+ ///
+ /// Use the built-in brokers for PostgreSQL.
+ ///
+ PostgreSql = 0x10,
+
+ ///
+ /// Use the built-in brokers for RabbitMQ.
+ ///
+ RabbitMQ = 0x20,
+
+ ///
+ /// Use the built-in brokers for Redis/Valkey.
+ ///
+ Redis = 0x40,
+
+ ///
+ /// Use the built-in brokers for Microsoft SQL Server.
+ ///
+ SqlServer = 0x80,
+
+ ///
+ /// Use all built-in brokers.
+ ///
+ All = Eureka | Identity | MongoDb | MySql | PostgreSql | RabbitMQ | Redis | SqlServer
+}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/ConfigurationBuilderExtensions.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/ConfigurationBuilderExtensions.cs
index a7756115f9..8cdebbc329 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/ConfigurationBuilderExtensions.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/ConfigurationBuilderExtensions.cs
@@ -29,7 +29,24 @@ public static class ConfigurationBuilderExtensions
///
public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigurationBuilder builder)
{
- return builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, DefaultReader, NullLoggerFactory.Instance);
+ return builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, null, CloudFoundryServiceBrokerTypes.All, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Adds CloudFoundry service bindings from the JSON provided by the specified reader.
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// The set of broker types to read service bindings for.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigurationBuilder builder, CloudFoundryServiceBrokerTypes brokerTypes)
+ {
+ return builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, null, brokerTypes, NullLoggerFactory.Instance);
}
///
@@ -46,7 +63,8 @@ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigu
///
public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigurationBuilder builder, IServiceBindingsReader serviceBindingsReader)
{
- return builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, serviceBindingsReader, NullLoggerFactory.Instance);
+ return builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, serviceBindingsReader, CloudFoundryServiceBrokerTypes.All,
+ NullLoggerFactory.Instance);
}
///
@@ -56,7 +74,7 @@ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigu
/// The to add configuration to.
///
///
- /// A predicate which is called before adding a key to the configuration. If it returns false, the key will be ignored.
+ /// A predicate that is called before adding a key to the configuration. If it returns false, the key will be ignored.
///
///
/// The source to read JSON service bindings from.
@@ -69,15 +87,43 @@ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigu
///
public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigurationBuilder builder, Predicate ignoreKeyPredicate,
IServiceBindingsReader serviceBindingsReader, ILoggerFactory loggerFactory)
+ {
+ return AddCloudFoundryServiceBindings(builder, ignoreKeyPredicate, serviceBindingsReader, CloudFoundryServiceBrokerTypes.All, loggerFactory);
+ }
+
+ ///
+ /// Adds CloudFoundry service bindings from the JSON provided by the specified reader.
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// A predicate that is called before adding a key to the configuration. If it returns false, the key will be ignored.
+ ///
+ ///
+ /// The source to read JSON service bindings from.
+ ///
+ ///
+ /// The set of broker types to read service bindings for.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigurationBuilder builder, Predicate ignoreKeyPredicate,
+ IServiceBindingsReader? serviceBindingsReader, CloudFoundryServiceBrokerTypes brokerTypes, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(ignoreKeyPredicate);
- ArgumentNullException.ThrowIfNull(serviceBindingsReader);
ArgumentNullException.ThrowIfNull(loggerFactory);
- if (!builder.EnumerateSources().Any())
+ CloudFoundryServiceBrokerTypes missingBrokerTypes = GetMissingBrokerTypes(builder, brokerTypes);
+
+ if (missingBrokerTypes != CloudFoundryServiceBrokerTypes.None)
{
- var source = new CloudFoundryServiceBindingConfigurationSource(serviceBindingsReader)
+ var source = new CloudFoundryServiceBindingConfigurationSource(serviceBindingsReader ?? DefaultReader, missingBrokerTypes)
{
IgnoreKeyPredicate = ignoreKeyPredicate
};
@@ -86,25 +132,71 @@ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigu
// WebApplicationBuilder immediately builds the configuration provider and loads it, which executes the post-processors.
// Therefore, adding post-processors afterward is a no-op.
- RegisterPostProcessors(source, loggerFactory);
+ RegisterPostProcessors(source, missingBrokerTypes, loggerFactory);
builder.Add(source);
}
return builder;
}
- private static void RegisterPostProcessors(CloudFoundryServiceBindingConfigurationSource source, ILoggerFactory loggerFactory)
+ private static CloudFoundryServiceBrokerTypes GetMissingBrokerTypes(IConfigurationBuilder builder, CloudFoundryServiceBrokerTypes brokerTypesRequested)
{
- ILogger eurekaLogger = loggerFactory.CreateLogger();
- ILogger identityLogger = loggerFactory.CreateLogger();
-
- source.RegisterPostProcessor(new EurekaCloudFoundryPostProcessor(eurekaLogger));
- source.RegisterPostProcessor(new IdentityCloudFoundryPostProcessor(identityLogger));
- source.RegisterPostProcessor(new MongoDbCloudFoundryPostProcessor());
- source.RegisterPostProcessor(new MySqlCloudFoundryPostProcessor());
- source.RegisterPostProcessor(new PostgreSqlCloudFoundryPostProcessor());
- source.RegisterPostProcessor(new RabbitMQCloudFoundryPostProcessor());
- source.RegisterPostProcessor(new RedisCloudFoundryPostProcessor());
- source.RegisterPostProcessor(new SqlServerCloudFoundryPostProcessor());
+ CloudFoundryServiceBrokerTypes missingBrokerTypes = brokerTypesRequested;
+
+ if (brokerTypesRequested != CloudFoundryServiceBrokerTypes.None)
+ {
+ foreach (CloudFoundryServiceBindingConfigurationSource existingSource in builder.EnumerateSources())
+ {
+ missingBrokerTypes &= ~existingSource.BrokerTypes;
+ }
+ }
+
+ return missingBrokerTypes;
+ }
+
+ private static void RegisterPostProcessors(CloudFoundryServiceBindingConfigurationSource source, CloudFoundryServiceBrokerTypes brokerTypes,
+ ILoggerFactory loggerFactory)
+ {
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.Eureka))
+ {
+ ILogger eurekaLogger = loggerFactory.CreateLogger();
+ source.RegisterPostProcessor(new EurekaCloudFoundryPostProcessor(eurekaLogger));
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.Identity))
+ {
+ ILogger identityLogger = loggerFactory.CreateLogger();
+ source.RegisterPostProcessor(new IdentityCloudFoundryPostProcessor(identityLogger));
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.MongoDb))
+ {
+ source.RegisterPostProcessor(new MongoDbCloudFoundryPostProcessor());
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.MySql))
+ {
+ source.RegisterPostProcessor(new MySqlCloudFoundryPostProcessor());
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.PostgreSql))
+ {
+ source.RegisterPostProcessor(new PostgreSqlCloudFoundryPostProcessor());
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.RabbitMQ))
+ {
+ source.RegisterPostProcessor(new RabbitMQCloudFoundryPostProcessor());
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.Redis))
+ {
+ source.RegisterPostProcessor(new RedisCloudFoundryPostProcessor());
+ }
+
+ if (brokerTypes.HasFlag(CloudFoundryServiceBrokerTypes.SqlServer))
+ {
+ source.RegisterPostProcessor(new SqlServerCloudFoundryPostProcessor());
+ }
}
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs
index dbf1c2a71e..3f5077ff5f 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/CloudFoundryPostProcessor.cs
@@ -5,15 +5,22 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
+#pragma warning disable S3881 // "IDisposable" should be implemented correctly
+
namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors;
-internal abstract class CloudFoundryPostProcessor : IConfigurationPostProcessor
+internal abstract partial class CloudFoundryPostProcessor : IConfigurationPostProcessor, IDisposable
{
- private static readonly Regex TagsConfigurationKeyRegex =
- new("^vcap:services:[^:]+:[0-9]+:tags:[0-9]+", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1));
+ private const int RegexMatchTimeoutInMilliseconds = 1_000;
+ private readonly HashSet _tempFilePaths = [];
+
+ [GeneratedRegex("^vcap:services:[^:]+:[0-9]+:tags:[0-9]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
+ RegexMatchTimeoutInMilliseconds)]
+ private static partial Regex TagsConfigurationKeyRegex();
- private static readonly Regex LabelConfigurationKeyRegex =
- new("^vcap:services:[^:]+:[0-9]+:label+", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1));
+ [GeneratedRegex("^vcap:services:[^:]+:[0-9]+:label+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
+ RegexMatchTimeoutInMilliseconds)]
+ private static partial Regex LabelConfigurationKeyRegex();
public abstract void PostProcessConfiguration(PostProcessorConfigurationProvider provider, IDictionary configurationData);
@@ -23,7 +30,7 @@ protected ICollection FilterKeys(IDictionary configurat
foreach ((string key, string? value) in configurationData)
{
- if ((sources & KeyFilterSources.Tag) != 0 && TagsConfigurationKeyRegex.IsMatch(key) &&
+ if ((sources & KeyFilterSources.Tag) != 0 && TagsConfigurationKeyRegex().IsMatch(key) &&
string.Equals(value, valueToFind, StringComparison.OrdinalIgnoreCase))
{
string? parentKey = ConfigurationPath.GetParentPath(key);
@@ -39,7 +46,7 @@ protected ICollection FilterKeys(IDictionary configurat
}
}
- if ((sources & KeyFilterSources.Label) != 0 && LabelConfigurationKeyRegex.IsMatch(key) &&
+ if ((sources & KeyFilterSources.Label) != 0 && LabelConfigurationKeyRegex().IsMatch(key) &&
string.Equals(value, valueToFind, StringComparison.OrdinalIgnoreCase))
{
string? serviceBindingKey = ConfigurationPath.GetParentPath(key);
@@ -54,6 +61,32 @@ protected ICollection FilterKeys(IDictionary configurat
return keys;
}
+ protected void TrackTempFiles(params IEnumerable paths)
+ {
+ foreach (string? path in paths)
+ {
+ if (path != null)
+ {
+ _tempFilePaths.Add(path);
+ }
+ }
+ }
+
+ public virtual void Dispose()
+ {
+ foreach (string path in _tempFilePaths)
+ {
+ try
+ {
+ File.Delete(path);
+ }
+ catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or NotSupportedException or ArgumentException)
+ {
+ // Intentionally left empty.
+ }
+ }
+ }
+
[Flags]
internal enum KeyFilterSources
{
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs
index ae87402a15..c7713086af 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/EurekaCloudFoundryPostProcessor.cs
@@ -6,7 +6,7 @@
namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors;
-internal sealed class EurekaCloudFoundryPostProcessor : CloudFoundryPostProcessor
+internal sealed partial class EurekaCloudFoundryPostProcessor : CloudFoundryPostProcessor
{
internal const string BindingType = "eureka";
internal const string EurekaConfigurationKeyPrefix = "eureka:client";
@@ -27,7 +27,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
{
if (hasMapped)
{
- _logger.LogWarning("Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.");
+ LogMultipleEurekaBindings();
break;
}
@@ -52,4 +52,8 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
hasMapped = true;
}
}
+
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message = "Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.")]
+ private partial void LogMultipleEurekaBindings();
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs
index 93a96da8e7..068ff27384 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/IdentityCloudFoundryPostProcessor.cs
@@ -6,7 +6,7 @@
namespace Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors;
-internal sealed class IdentityCloudFoundryPostProcessor : CloudFoundryPostProcessor
+internal sealed partial class IdentityCloudFoundryPostProcessor : CloudFoundryPostProcessor
{
internal const string BindingType = "p-identity";
internal const string AuthenticationConfigurationKeyPrefix = "Authentication:Schemes";
@@ -34,7 +34,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
{
if (hasMapped)
{
- _logger.LogWarning("Multiple identity service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.");
+ LogMultipleIdentityBindings();
break;
}
@@ -50,4 +50,8 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
hasMapped = true;
}
}
+
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message = "Multiple identity service bindings found, which is not supported. Using the first binding from VCAP_SERVICES.")]
+ private partial void LogMultipleIdentityBindings();
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MongoDbCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MongoDbCloudFoundryPostProcessor.cs
index 604e3c1fcd..85426606fe 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MongoDbCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MongoDbCloudFoundryPostProcessor.cs
@@ -16,7 +16,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-microsoft-azure/1-13/csb-azure/reference-azure-cosmosdb-mongo.html#binding-creds
+ // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-azure/1-13/csb-azure/reference-azure-cosmosdb-mongo.html#binding-creds
mapper.MapFromTo("credentials:uri", "url");
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MySqlCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MySqlCloudFoundryPostProcessor.cs
index b70f530628..73a27ab6b7 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MySqlCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/MySqlCloudFoundryPostProcessor.cs
@@ -16,18 +16,20 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-mysql-on-cloud-foundry/10-0/mysql-for-tpcf/use.html#vcap
- // - GCP Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-gcp/1-9/csb-gcp/reference-gcp-mysql.html#binding-creds
- // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-aws/1-14/csb-aws/reference-aws-mysql.html#binding-creds
+ // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/tanzu-mysql-tanzu-platform/10-1/mysql-tp/use.html#vcap
+ // - GCP Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-gcp/1-8/csb-gcp/reference-gcp-mysql.html#binding-creds
+ // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-aws/1-15/csb-aws/reference-aws-mysql.html#binding-creds
mapper.MapFromTo("credentials:hostname", "host");
mapper.MapFromTo("credentials:port", "port");
mapper.MapFromTo("credentials:name", "database");
mapper.MapFromTo("credentials:username", "username");
mapper.MapFromTo("credentials:password", "password");
- mapper.MapFromToFile("credentials:sslCert", "ssl-cert");
- mapper.MapFromToFile("credentials:sslKey", "ssl-key");
- mapper.MapFromToFile("credentials:sslRootCert", "ssl-ca");
+ string? sslCertFile = mapper.MapFromToFile("credentials:sslCert", "ssl-cert");
+ string? sslKeyFile = mapper.MapFromToFile("credentials:sslKey", "ssl-key");
+ string? sslRootCertFile = mapper.MapFromToFile("credentials:sslRootCert", "ssl-ca");
+
+ TrackTempFiles(sslCertFile, sslKeyFile, sslRootCertFile);
}
}
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/PostgreSqlCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/PostgreSqlCloudFoundryPostProcessor.cs
index e044a20892..76c353c30c 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/PostgreSqlCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/PostgreSqlCloudFoundryPostProcessor.cs
@@ -22,9 +22,9 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-cloud-foundry/10-1/postgres/app-setup-single-instance-service-guide.html
- // - GCP Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-gcp/1-9/csb-gcp/reference-gcp-postgresql.html#binding-creds
- // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-aws/1-14/csb-aws/reference-aws-postgres.html#binding-creds
+ // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/tanzu-postgres-tanzu-platform/10-2/postgres-tp/app-setup-single-instance-service-guide.html
+ // - GCP Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-gcp/1-8/csb-gcp/reference-gcp-postgresql.html#binding-creds
+ // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-aws/1-15/csb-aws/reference-aws-postgres.html#binding-creds
string? hosts = mapper.MapArrayFromTo("credentials:hosts", "host", HostsSeparator);
@@ -62,9 +62,11 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
}
mapper.MapFromTo("credentials:password", "password");
- mapper.MapFromToFile("credentials:sslcert", "SSL Certificate");
- mapper.MapFromToFile("credentials:sslkey", "SSL Key");
- mapper.MapFromToFile("credentials:sslrootcert", "Root Certificate");
+ string? sslCertFile = mapper.MapFromToFile("credentials:sslcert", "SSL Certificate");
+ string? sslKeyFile = mapper.MapFromToFile("credentials:sslkey", "SSL Key");
+ string? sslRootCertFile = mapper.MapFromToFile("credentials:sslrootcert", "Root Certificate");
+
+ TrackTempFiles(sslCertFile, sslKeyFile, sslRootCertFile);
}
}
}
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RabbitMQCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RabbitMQCloudFoundryPostProcessor.cs
index 8849fc6598..b5afd85194 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RabbitMQCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RabbitMQCloudFoundryPostProcessor.cs
@@ -16,7 +16,7 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-rabbitmq-on-cloud-foundry/10-0/tanzu-rabbitmq-cloud-foundry/reference.html
+ // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/tanzu-rabbitmq-tanzu-platform/10-0/rabbitmq-tp/use.html#call
string? useTlsValue = mapper.MapFromTo("credentials:ssl", "useTls");
string fromProtocol = bool.TryParse(useTlsValue, out bool useTls) && useTls ? "amqp+ssl" : "amqp";
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RedisCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RedisCloudFoundryPostProcessor.cs
index 81880bc28c..56ceb8890d 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RedisCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/RedisCloudFoundryPostProcessor.cs
@@ -16,10 +16,9 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Tanzu Broker (Redis): https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/redis-for-tanzu-application-service/3-5/redis-for-tas/using.html#use-redis-service-in-app
- // - Tanzu Broker (Valkey): https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-valkey-on-cloud-foundry/4-0/valkey-on-cf/using.html#use-valkey-service-in-app
- // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-microsoft-azure/1-13/csb-azure/reference-azure-redis.html#binding-creds
- // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-aws/1-14/csb-aws/reference-aws-redis.html#binding-creds
+ // - Tanzu Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/tanzu-valkey-tanzu-platform/10-2/valkey-tp/using.html#use-valkey-service-in-app
+ // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-azure/1-13/csb-azure/reference-azure-redis.html
+ // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-aws/1-15/csb-aws/reference-aws-redis.html
mapper.MapFromTo("credentials:host", "host");
mapper.MapFromTo("credentials:port", "port");
diff --git a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/SqlServerCloudFoundryPostProcessor.cs b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/SqlServerCloudFoundryPostProcessor.cs
index 4b595cf7d6..f4f38827fa 100644
--- a/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/SqlServerCloudFoundryPostProcessor.cs
+++ b/src/Configuration/src/CloudFoundry/ServiceBindings/PostProcessors/SqlServerCloudFoundryPostProcessor.cs
@@ -16,8 +16,8 @@ public override void PostProcessConfiguration(PostProcessorConfigurationProvider
// Mapping from CloudFoundry service binding credentials to driver-specific connection string parameters.
// The available credentials are documented at:
- // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-microsoft-azure/1-13/csb-azure/reference-azure-mssql-db.html#binding-creds
- // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/tanzu-cloud-service-broker-for-aws/1-14/csb-aws/reference-aws-mssql.html#binding-creds
+ // - Azure Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-azure/1-13/csb-azure/reference-azure-mssql-db.html#binding-creds
+ // - AWS Service Broker: https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/cloud-service-broker-aws/1-15/csb-aws/reference-aws-mssql.html#binding-creds
mapper.MapFromTo("credentials:hostname", "Data Source");
mapper.MapFromAppendTo("credentials:port", "Data Source", ",");
diff --git a/src/Configuration/src/CloudFoundry/Steeltoe.Configuration.CloudFoundry.csproj b/src/Configuration/src/CloudFoundry/Steeltoe.Configuration.CloudFoundry.csproj
index 64f150d345..80c40aa143 100644
--- a/src/Configuration/src/CloudFoundry/Steeltoe.Configuration.CloudFoundry.csproj
+++ b/src/Configuration/src/CloudFoundry/Steeltoe.Configuration.CloudFoundry.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider and IOptions support for reading Cloud Foundry environment variables.
configuration;ConfigurationProvider;CloudFoundry;vcap;vcap_application;vcap_services;tanzu
true
diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs
index d9ce59f505..f8be0b3b41 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs
@@ -17,10 +17,14 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions
private const char CommaDelimiter = ',';
internal const string ConfigurationPrefix = "spring:cloud:config";
- internal CertificateOptions ClientCertificate { get; } = new();
internal TimeSpan HttpTimeout => TimeSpan.FromMilliseconds(Timeout);
internal bool IsMultiServerConfiguration => Uri != null && Uri.Contains(CommaDelimiter);
+ ///
+ /// Gets or sets the client certificate used for mutual TLS authentication with the Config Server.
+ ///
+ internal CertificateOptions ClientCertificate { get; set; } = new();
+
///
/// Gets or sets a value indicating whether the Config Server provider is enabled. Default value: true.
///
@@ -35,7 +39,7 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions
/// Gets or sets a comma-separated list of environments used when accessing configuration data. Default value: "Production".
///
[ConfigurationKeyName("Env")]
- public string? Environment { get; set; } = "Production";
+ public string? Environment { get; set; }
///
/// Gets or sets a comma-separated list of labels to request from the server.
@@ -96,17 +100,17 @@ public bool ValidateCertificatesAlt
///
/// Gets retry settings.
///
- public ConfigServerRetryOptions Retry { get; } = new();
+ public ConfigServerRetryOptions Retry { get; private set; } = new();
///
/// Gets service discovery settings.
///
- public ConfigServerDiscoveryOptions Discovery { get; } = new();
+ public ConfigServerDiscoveryOptions Discovery { get; private set; } = new();
///
/// Gets health check settings.
///
- public ConfigServerHealthOptions Health { get; } = new();
+ public ConfigServerHealthOptions Health { get; private set; } = new();
///
/// Gets or sets the address used by the provider to obtain a OAuth Access Token.
@@ -129,7 +133,7 @@ public bool ValidateCertificatesAlt
public int TokenTtl { get; set; } = 300_000;
///
- /// Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute).
+ /// Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute).
///
public int TokenRenewRate { get; set; } = 60_000;
@@ -141,7 +145,52 @@ public bool ValidateCertificatesAlt
///
/// Gets headers that will be added to the Config Server request.
///
- public IDictionary Headers { get; } = new Dictionary();
+ public IDictionary Headers { get; private set; } = new Dictionary();
+
+ internal ConfigServerClientOptions Clone()
+ {
+ return new ConfigServerClientOptions
+ {
+ ClientCertificate = ClientCertificate.Clone(),
+ Enabled = Enabled,
+ FailFast = FailFast,
+ Environment = Environment,
+ Label = Label,
+ Name = Name,
+ Uri = Uri,
+ Username = Username,
+ Password = Password,
+ Token = Token,
+ Timeout = Timeout,
+ PollingInterval = PollingInterval,
+ ValidateCertificates = ValidateCertificates,
+ Retry = new ConfigServerRetryOptions
+ {
+ Enabled = Retry.Enabled,
+ InitialInterval = Retry.InitialInterval,
+ MaxInterval = Retry.MaxInterval,
+ Multiplier = Retry.Multiplier,
+ MaxAttempts = Retry.MaxAttempts
+ },
+ Discovery = new ConfigServerDiscoveryOptions
+ {
+ Enabled = Discovery.Enabled,
+ ServiceId = Discovery.ServiceId
+ },
+ Health = new ConfigServerHealthOptions
+ {
+ Enabled = Health.Enabled,
+ TimeToLive = Health.TimeToLive
+ },
+ AccessTokenUri = AccessTokenUri,
+ ClientSecret = ClientSecret,
+ ClientId = ClientId,
+ TokenTtl = TokenTtl,
+ TokenRenewRate = TokenRenewRate,
+ DisableTokenRenewal = DisableTokenRenewal,
+ Headers = new Dictionary(Headers)
+ };
+ }
internal List GetUris()
{
diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs
index f0869e72c3..19b9ade24e 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs
@@ -26,7 +26,7 @@ public static class ConfigServerConfigurationBuilderExtensions
///
public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder)
{
- return AddConfigServer(builder, NullLoggerFactory.Instance);
+ return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, NullLoggerFactory.Instance);
}
///
@@ -43,9 +43,24 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b
///
public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ILoggerFactory loggerFactory)
{
- var options = new ConfigServerClientOptions();
+ return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, loggerFactory);
+ }
- return AddConfigServer(builder, options, loggerFactory);
+ ///
+ /// Adds a configuration source for Config Server to the .
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// The default options, whose values are overridden from .
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options)
+ {
+ return AddConfigServer(builder, options, null, null, NullLoggerFactory.Instance);
}
///
@@ -55,7 +70,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b
/// The to add configuration to.
///
///
- /// Enables to configure Config Server from code.
+ /// The default options, whose values are overridden from .
///
///
/// Used for internal logging. Pass to disable logging.
@@ -64,6 +79,50 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b
/// The incoming so that additional calls can be chained.
///
public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, ILoggerFactory loggerFactory)
+ {
+ return AddConfigServer(builder, options, null, null, loggerFactory);
+ }
+
+ ///
+ /// Adds a configuration source for Config Server to the .
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure)
+ {
+ return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Adds a configuration source for Config Server to the .
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure,
+ ILoggerFactory loggerFactory)
+ {
+ return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, loggerFactory);
+ }
+
+ internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options,
+ Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(options);
@@ -74,10 +133,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b
builder.AddCloudFoundry();
builder.AddKubernetesServiceBindings();
- ConfigServerConfigurationSource source = builder is IConfiguration configuration
- ? new ConfigServerConfigurationSource(options, configuration, loggerFactory)
- : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, loggerFactory);
-
+ var source = new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, configure, createHttpClientHandler, loggerFactory);
builder.Add(source);
}
diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs
index cbb723915d..da98d39f44 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs
@@ -7,6 +7,7 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Sockets;
+using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -14,38 +15,51 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
using Steeltoe.Common.Configuration;
using Steeltoe.Common.Discovery;
using Steeltoe.Common.Extensions;
using Steeltoe.Common.Http;
using Steeltoe.Common.Http.HttpClientPooling;
+using LockPrimitive =
+#if NET10_0_OR_GREATER
+ System.Threading.Lock
+#else
+ object
+#endif
+ ;
namespace Steeltoe.Configuration.ConfigServer;
///
/// A Spring Cloud Config Server based .
///
-internal sealed class ConfigServerConfigurationProvider : ConfigurationProvider, IDisposable
+internal sealed partial class ConfigServerConfigurationProvider : ConfigurationProvider, IDisposable
{
private const string VaultRenewPath = "vault/v1/auth/token/renew-self";
private const string VaultTokenHeader = "X-Vault-Token";
private const char CommaDelimiter = ',';
-
internal const string TokenHeader = "X-Config-Token";
-
private static readonly string[] EmptyLabels = [string.Empty];
- private readonly ILoggerFactory _loggerFactory;
- private readonly ILogger _logger;
- private readonly IConfiguration _configuration;
- private readonly bool _hasConfiguration;
- private readonly bool _ownsHttpClientHandler;
+ private readonly ILogger _logger;
+ private readonly Func _createHttpClientHandler;
+ private readonly bool _disposeHttpClientHandler;
private readonly ConfigureConfigServerClientOptions _configurer;
- private HttpClientHandler? _httpClientHandler;
-
- private ConfigServerDiscoveryService? _configServerDiscoveryService;
- private Timer? _refreshTimer;
- private SemaphoreSlim? _timerTickLock = new(1, 1);
+ private readonly ConfigServerClientOptions _defaultOptions;
+ private readonly LockPrimitive _lifecycleLock = new();
+ private readonly LockPrimitive _configurationReloadTickLock = new();
+ private readonly LockPrimitive _vaultRenewTickLock = new();
+ private readonly ConfigServerDiscoveryService _configServerDiscoveryService;
+ private readonly IDisposable _changeTokenRegistration;
+ private readonly CancellationTokenSource _shutdownTokenSource = new();
+ private readonly CancellationToken _shutdownToken;
+
+ private Timer? _configurationReloadTimer;
+ private Timer? _vaultRenewTimer;
+ private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult;
+ private volatile ConfigServerClientOptions _clientOptions;
+ private long _isReload;
internal static JsonSerializerOptions SerializerOptions { get; } = new()
{
@@ -54,12 +68,13 @@ internal sealed class ConfigServerConfigurationProvider : ConfigurationProvider,
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate
};
- internal IDictionary Properties => Data;
+ internal IDictionary InnerData => Data;
///
- /// Gets the configuration settings the provider uses when accessing the server.
+ /// Gets the settings used to access Config Server, excluding information found during service discovery (so that a provider (re)load properly observes
+ /// changes and triggers its change token). Returns a cloned snapshot to prevent tearing during reads/writes.
///
- public ConfigServerClientOptions ClientOptions { get; }
+ internal ConfigServerClientOptions ClientOptions => _clientOptions.Clone();
///
/// Initializes a new instance of the class from a .
@@ -71,225 +86,329 @@ internal sealed class ConfigServerConfigurationProvider : ConfigurationProvider,
/// Used for internal logging. Pass to disable logging.
///
public ConfigServerConfigurationProvider(ConfigServerConfigurationSource source, ILoggerFactory loggerFactory)
- : this(source.DefaultOptions, source.Configuration, null, loggerFactory)
+ : this(source.DefaultOptions, source.Configuration, source.Configure, source.CreateHttpClientHandler, loggerFactory)
{
}
- internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, HttpClientHandler? httpClientHandler,
- ILoggerFactory loggerFactory)
+ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration,
+ Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(clientOptions);
ArgumentNullException.ThrowIfNull(loggerFactory);
- _loggerFactory = loggerFactory;
- _logger = _loggerFactory.CreateLogger();
+ _logger = loggerFactory.CreateLogger();
+ _shutdownToken = _shutdownTokenSource.Token; // Don't inline: the token survives disposal, while the source does not.
+ IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build();
+ _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration, configure);
+ _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory);
+
+ _defaultOptions = clientOptions.Clone();
+ _clientOptions = _defaultOptions;
- if (configuration != null)
+ if (createHttpClientHandler != null)
{
- _configuration = configuration;
- _hasConfiguration = true;
+ _createHttpClientHandler = createHttpClientHandler;
+ _disposeHttpClientHandler = false;
}
else
{
- _configuration = new ConfigurationBuilder().Build();
- _hasConfiguration = false;
+ _createHttpClientHandler = static () => new HttpClientHandler();
+ _disposeHttpClientHandler = true;
}
- _configurer = new ConfigureConfigServerClientOptions(_configuration);
+ _changeTokenRegistration = ChangeToken.OnChange(effectiveConfiguration.GetReloadToken, () => OnSettingsChanged(true));
+ }
+
+ private void OnSettingsChanged(bool skipTimerDueTime)
+ {
+ LogEnteringOnSettingsChanged();
- ClientOptions = clientOptions;
+ ConfigServerClientOptions newOptions = _defaultOptions.Clone();
- if (httpClientHandler == null)
+ try
{
- _httpClientHandler = new HttpClientHandler();
- _ownsHttpClientHandler = true;
+ _configurer.Configure(newOptions);
}
- else
+ catch (Exception exception)
{
- _httpClientHandler = httpClientHandler;
+ LogBindSettingsFailed(exception);
+ throw;
}
- OnSettingsChanged();
- }
+ lock (_lifecycleLock)
+ {
+ if (_shutdownToken.IsCancellationRequested)
+ {
+ return;
+ }
- private void OnSettingsChanged()
- {
- TimeSpan existingPollingInterval = ClientOptions.PollingInterval;
+ TimeSpan previousPollingInterval = _clientOptions.PollingInterval;
+ int previousTokenRenewRate = _clientOptions.TokenRenewRate;
- _configurer.Configure(ClientOptions);
+ _clientOptions = newOptions;
- if (_hasConfiguration)
+ UpdateConfigurationReloadTimer(newOptions, previousPollingInterval, skipTimerDueTime);
+ UpdateVaultRenewTimer(newOptions, previousTokenRenewRate, skipTimerDueTime);
+ }
+ }
+
+ private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSnapshot, TimeSpan previousPollingInterval, bool skipDueTime)
+ {
+ if (optionsSnapshot.PollingInterval == TimeSpan.Zero || !optionsSnapshot.Enabled)
+ {
+ _configurationReloadTimer?.Dispose();
+ _configurationReloadTimer = null;
+ }
+ else if (_configurationReloadTimer == null)
+ {
+ _configurationReloadTimer = new Timer(_ => ConfigurationReloadTimerTick(), null, skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval,
+ optionsSnapshot.PollingInterval);
+ }
+ else if (previousPollingInterval != optionsSnapshot.PollingInterval)
{
- _configuration.GetReloadToken().RegisterChangeCallback(_ => OnSettingsChanged(), null);
+ _configurationReloadTimer.Change(skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval, optionsSnapshot.PollingInterval);
}
+ }
- if (ClientOptions.PollingInterval == TimeSpan.Zero || !ClientOptions.Enabled)
+ private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, int previousTokenRenewRate, bool skipDueTime)
+ {
+ if (string.IsNullOrEmpty(optionsSnapshot.Token) || optionsSnapshot.DisableTokenRenewal ||
+ optionsSnapshot is not { Uri: not null, IsMultiServerConfiguration: false })
{
- _refreshTimer?.Dispose();
- _refreshTimer = null;
+ _vaultRenewTimer?.Dispose();
+ _vaultRenewTimer = null;
}
- else if (ClientOptions.Enabled)
+ else if (_vaultRenewTimer == null)
{
- if (_refreshTimer == null)
- {
-#pragma warning disable S4462 // Calls to "async" methods should not be blocking
- // Justification: Configuration sources and providers don't support async.
- _refreshTimer = new Timer(_ => DoPolledLoadAsync().GetAwaiter().GetResult(), null, TimeSpan.Zero, ClientOptions.PollingInterval);
-#pragma warning restore S4462 // Calls to "async" methods should not be blocking
- }
- else if (existingPollingInterval != ClientOptions.PollingInterval)
- {
- _refreshTimer.Change(TimeSpan.Zero, ClientOptions.PollingInterval);
- }
+ TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate);
+ _vaultRenewTimer = new Timer(_ => VaultRenewTimerTick(), null, skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval);
+ }
+ else if (previousTokenRenewRate != optionsSnapshot.TokenRenewRate)
+ {
+ TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate);
+ _vaultRenewTimer.Change(skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval);
}
}
///
- /// DoPolledLoad is called by a Timer callback, so must catch all exceptions.
+ /// ConfigurationReloadTimerTick is called by a Timer callback, so must catch all exceptions.
///
- private async Task DoPolledLoadAsync()
+ private void ConfigurationReloadTimerTick()
{
- _logger.LogTrace("Entering timer cycle");
- bool lockTaken = false;
+ LogEnteringConfigurationReloadCycle();
+
+#if NET10_0_OR_GREATER
+ bool lockTaken = _configurationReloadTickLock.TryEnter();
+#else
+ bool lockTaken = Monitor.TryEnter(_configurationReloadTickLock);
+#endif
try
{
- lockTaken = _timerTickLock != null && await _timerTickLock.WaitAsync(0);
+ if (!lockTaken || _shutdownToken.IsCancellationRequested)
+ {
+ LogSkippingConfigurationReloadCycle();
+ return;
+ }
+
+ LogConfigurationReloadCycleLockObtained();
+ ConfigServerClientOptions optionsSnapshot = ClientOptions;
+
+#pragma warning disable S4462 // Calls to "async" methods should not be blocking
+ // Justification: Configuration sources and providers don't support async.
+ UpdateDiscoveryAsync(optionsSnapshot, false, _shutdownToken).GetAwaiter().GetResult();
+ DoLoadAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult();
+#pragma warning restore S4462 // Calls to "async" methods should not be blocking
+
+ LogConfigurationReloadCycleCompleted();
}
- catch (ObjectDisposedException)
+ catch (Exception exception)
{
- // Ignore exception originating from potential race condition.
+ if (!_shutdownToken.IsCancellationRequested)
+ {
+ LogConfigurationReloadCycleFailed(exception);
+ }
}
-
- try
+ finally
{
if (lockTaken)
{
- _logger.LogTrace("Exclusive lock obtained");
- await DoLoadAsync(true, CancellationToken.None);
+#if NET10_0_OR_GREATER
+ _configurationReloadTickLock.Exit();
+#else
+ Monitor.Exit(_configurationReloadTickLock);
+#endif
}
- else
+ }
+ }
+
+ ///
+ /// VaultRenewTimerTick is called by a Timer callback, so must catch all exceptions.
+ ///
+ private void VaultRenewTimerTick()
+ {
+ LogEnteringVaultRenewCycle();
+
+#if NET10_0_OR_GREATER
+ bool lockTaken = _vaultRenewTickLock.TryEnter();
+#else
+ bool lockTaken = Monitor.TryEnter(_vaultRenewTickLock);
+#endif
+
+ try
+ {
+ if (!lockTaken || _shutdownToken.IsCancellationRequested)
{
- _logger.LogTrace("Previous cycle is still running, or already disposed; skipping this cycle");
+ LogSkippingVaultRenewCycle();
+ return;
}
+
+ LogVaultRenewCycleLockObtained();
+
+#pragma warning disable S4462 // Calls to "async" methods should not be blocking
+ // Justification: Configuration sources and providers don't support async.
+ RefreshVaultTokenAsync(ClientOptions, _shutdownToken).GetAwaiter().GetResult();
+#pragma warning restore S4462 // Calls to "async" methods should not be blocking
+
+ LogVaultRenewCycleCompleted();
}
catch (Exception exception)
{
- _logger.LogWarning(exception, "Could not reload configuration during polling");
+ if (!_shutdownToken.IsCancellationRequested)
+ {
+ LogVaultRenewCycleFailed(exception);
+ }
}
finally
{
if (lockTaken)
{
- _logger.LogTrace("Timer cycle completed, releasing exclusive lock");
-
- try
- {
- _timerTickLock?.Release();
- }
- catch (ObjectDisposedException)
- {
- // Ignore exception originating from potential race condition.
- }
+#if NET10_0_OR_GREATER
+ _vaultRenewTickLock.Exit();
+#else
+ Monitor.Exit(_vaultRenewTickLock);
+#endif
}
}
}
///
- /// Loads configuration data from the Spring Cloud Configuration Server as specified by the .
+ /// Loads configuration data from the Spring Cloud Config Server as specified by the .
///
public override void Load()
{
+ long previousIsReload = Interlocked.CompareExchange(ref _isReload, 1, 0);
+
+ if (previousIsReload == 0)
+ {
+ OnSettingsChanged(false);
+ }
+
+ ConfigServerClientOptions optionsSnapshot = ClientOptions;
+
+ try
+ {
#pragma warning disable S4462 // Calls to "async" methods should not be blocking
- // Justification: Configuration sources and providers don't support async.
- LoadInternalAsync(true, CancellationToken.None).GetAwaiter().GetResult();
+ // Justification: Configuration sources and providers don't support async.
+ LoadInternalAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult();
#pragma warning restore S4462 // Calls to "async" methods should not be blocking
+ }
+ catch (OperationCanceledException) when (_shutdownToken.IsCancellationRequested)
+ {
+ // Expected during disposal; silently ignore.
+ }
+ catch (ConfigServerException exception)
+ {
+ if (optionsSnapshot.FailFast)
+ {
+ throw;
+ }
+
+ LogFetchingRemoteConfigurationFailed(exception);
+ }
}
- internal async Task LoadInternalAsync(bool updateDictionary, CancellationToken cancellationToken)
+ internal async Task LoadInternalAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary,
+ CancellationToken cancellationToken)
{
- if (!ClientOptions.Enabled)
+ if (!optionsSnapshot.Enabled)
{
- _logger.LogInformation("Config Server client disabled, did not fetch configuration!");
+ LogConfigServerClientDisabled();
return null;
}
- if (IsDiscoveryFirstEnabled())
- {
- _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, ClientOptions, _loggerFactory);
- await DiscoverServerInstancesAsync(_configServerDiscoveryService, cancellationToken);
- }
+ await UpdateDiscoveryAsync(optionsSnapshot, optionsSnapshot.FailFast, cancellationToken);
- // Adds client settings (e.g. spring:cloud:config:uri, etc.) to the Data dictionary
- AddConfigServerClientOptions();
-
- if (ClientOptions is { Retry.Enabled: true, FailFast: true })
+ if (optionsSnapshot is { Retry.Enabled: true, FailFast: true })
{
int attempts = 0;
- int backOff = ClientOptions.Retry.InitialInterval;
+ int backOff = optionsSnapshot.Retry.InitialInterval;
+ List errors = [];
do
{
- _logger.LogDebug("Fetching configuration from server(s).");
-
try
{
- return await DoLoadAsync(updateDictionary, cancellationToken);
+ return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken);
}
catch (ConfigServerException exception)
{
- _logger.LogWarning(exception, "Failed fetching configuration from server(s).");
+ errors.Add(exception);
attempts++;
- if (attempts < ClientOptions.Retry.MaxAttempts)
+ if (attempts < optionsSnapshot.Retry.MaxAttempts)
{
- Thread.CurrentThread.Join(backOff);
- int nextBackOff = (int)(backOff * ClientOptions.Retry.Multiplier);
- backOff = Math.Min(nextBackOff, ClientOptions.Retry.MaxInterval);
+ await Task.Delay(backOff, cancellationToken);
+ int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier);
+ backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval);
}
else
{
- throw;
+ throw new ConfigServerException($"Failed fetching remote configuration from server(s) after {attempts} attempts.",
+ new AggregateException(null, errors));
}
}
}
while (true);
}
- _logger.LogDebug("Fetching configuration from server(s).");
- return await DoLoadAsync(updateDictionary, cancellationToken);
+ return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken);
}
- internal async Task DoLoadAsync(bool updateDictionary, CancellationToken cancellationToken)
+ private async Task DoLoadAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, CancellationToken cancellationToken)
{
+ LogFetchingRemoteConfiguration();
+
+ ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot);
+
Exception? error = null;
// Get list of Config Server uris to check
- List uris = ClientOptions.GetUris();
+ List uris = optionsSnapshot.GetUris();
try
{
- foreach (string label in GetLabels())
+ foreach (string label in GetLabels(optionsSnapshot))
{
- _logger.LogTrace("Processing label '{Label}'", label);
+ LogProcessingLabel(label);
if (uris.Count > 1)
{
- _logger.LogDebug("Multiple Config Server Uris listed.");
+ LogMultipleConfigServerUris();
}
// Invoke Config Servers
- ConfigEnvironment? env = await RemoteLoadAsync(uris, label, cancellationToken);
+ ConfigEnvironment? env = await RemoteLoadAsync(optionsSnapshot, uris, label, cancellationToken);
// Update configuration Data dictionary with any results
if (env != null)
{
- _logger.LogDebug("Located environment name: {Name}, profiles: {Profiles}, labels: {Label}, version: {Version}, state: {State}", env.Name,
- env.Profiles, env.Label, env.Version, env.State);
+ ExpensiveLogEnvironmentLocated(env);
if (updateDictionary)
{
var data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ CopyLastDiscoveryLookupResultToData(data, optionsSnapshot.Discovery.Enabled);
if (!string.IsNullOrEmpty(env.State))
{
@@ -302,25 +421,21 @@ public override void Load()
}
IList sources = env.PropertySources;
- int index = sources.Count - 1;
- for (; index >= 0; index--)
+ for (int index = sources.Count - 1; index >= 0; index--)
{
AddPropertySource(sources[index], data);
}
- // Adds client settings (e.g. spring:cloud:config:uri, etc.) back to the (new) Data dictionary
- AddConfigServerClientOptions(data);
-
if (!AreDictionariesEqual(Data, data))
{
- _logger.LogTrace("Data has changed, raising configuration reload");
+ LogDataChanged();
Data = data;
OnReload();
}
else
{
- _logger.LogTrace("Data has not changed");
+ LogDataNotChanged();
}
}
@@ -333,15 +448,56 @@ public override void Load()
error = exception;
}
- _logger.LogWarning(error, "Could not locate PropertySource");
+ throw new ConfigServerException("Failed fetching remote configuration from server(s).", error);
+ }
- if (ClientOptions.FailFast)
+ private void ExpensiveLogEnvironmentLocated(ConfigEnvironment environment)
+ {
+ if (_logger.IsEnabled(LogLevel.Debug))
{
- _logger.LogTrace(error, "Failure with FailFast enabled, throwing ConfigServerException");
- throw new ConfigServerException("Could not locate PropertySource, fail fast property is set, failing", error);
+ string profiles = string.Join(", ", environment.Profiles.Select(profile => $"'{profile}'"));
+ LogEnvironmentLocated(environment.Name, profiles, environment.Label, environment.Version, environment.State);
}
+ }
- return null;
+ internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot)
+ {
+ DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult;
+
+ if (lastResult != null && optionsSnapshot.Discovery.Enabled)
+ {
+ optionsSnapshot.Uri = lastResult.ConfigServerUri;
+
+ if (lastResult.Username != null)
+ {
+ optionsSnapshot.Username = lastResult.Username;
+ }
+
+ if (lastResult.Password != null)
+ {
+ optionsSnapshot.Password = lastResult.Password;
+ }
+ }
+ }
+
+ private void CopyLastDiscoveryLookupResultToData(Dictionary data, bool isDiscoveryEnabled)
+ {
+ DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult;
+
+ if (lastResult != null && isDiscoveryEnabled)
+ {
+ data["spring:cloud:config:uri"] = lastResult.ConfigServerUri;
+
+ if (lastResult.Username != null)
+ {
+ data["spring:cloud:config:username"] = lastResult.Username;
+ }
+
+ if (lastResult.Password != null)
+ {
+ data["spring:cloud:config:password"] = lastResult.Password;
+ }
+ }
}
private static bool AreDictionariesEqual(IDictionary first, Dictionary second)
@@ -351,93 +507,92 @@ private static bool AreDictionariesEqual(IDictionary
second.ContainsKey(firstKey) && EqualityComparer.Default.Equals(first[firstKey], second[firstKey]));
}
- internal string[] GetLabels()
+ internal string[] GetLabels(ConfigServerClientOptions optionsSnapshot)
{
- if (string.IsNullOrWhiteSpace(ClientOptions.Label))
+ if (string.IsNullOrWhiteSpace(optionsSnapshot.Label))
{
return EmptyLabels;
}
- return ClientOptions.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ return optionsSnapshot.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
- private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, CancellationToken cancellationToken)
+ private async Task UpdateDiscoveryAsync(ConfigServerClientOptions optionsSnapshot, bool failFast, CancellationToken cancellationToken)
{
- IServiceInstance[] instances = (await configServerDiscoveryService.GetConfigServerInstancesAsync(cancellationToken)).ToArray();
-
- if (instances.Length == 0)
+ if (optionsSnapshot.Discovery.Enabled)
{
- if (ClientOptions.FailFast)
+ List instances = await _configServerDiscoveryService.GetConfigServerInstancesAsync(optionsSnapshot, cancellationToken);
+ SetLastDiscoveryLookupResult(instances);
+
+ if (instances.Count == 0 && failFast)
{
throw new ConfigServerException("Could not locate Config Server via discovery, are you missing a Discovery service assembly?");
}
-
- return;
}
-
- UpdateSettingsFromDiscovery(instances, ClientOptions);
+ else
+ {
+ SetLastDiscoveryLookupResult([]);
+ }
}
- internal void UpdateSettingsFromDiscovery(IEnumerable instances, ConfigServerClientOptions clientOptions)
+ internal void SetLastDiscoveryLookupResult(IEnumerable instances)
{
- var endpoints = new StringBuilder();
+ var endpointBuilder = new StringBuilder();
+ string? username = null;
+ string? password = null;
foreach (IServiceInstance instance in instances)
{
+ if (instance.Metadata.TryGetValue("password", out string? instancePassword))
+ {
+ instance.Metadata.TryGetValue("user", out string? instanceUsername);
+ username = instanceUsername ?? "user";
+ password = instancePassword;
+ }
+
string uri = instance.Uri.ToString();
- IReadOnlyDictionary metaData = instance.Metadata;
- if (metaData.Count > 0)
+ if (instance.Metadata.TryGetValue("configPath", out string? path) && path != null)
{
- if (metaData.TryGetValue("password", out string? password))
+ if (uri.EndsWith('/') && path.StartsWith('/'))
{
- metaData.TryGetValue("user", out string? username);
- username ??= "user";
- clientOptions.Username = username;
- clientOptions.Password = password;
+ uri = uri[..^1];
}
- if (metaData.TryGetValue("configPath", out string? path) && path != null)
- {
- if (uri.EndsWith('/') && path.StartsWith('/'))
- {
- uri = uri[..^1];
- }
-
- uri += path;
- }
+ uri += path;
}
- endpoints.Append(uri);
- endpoints.Append(',');
+ endpointBuilder.Append(uri);
+ endpointBuilder.Append(',');
}
- if (endpoints.Length > 0)
+ if (endpointBuilder.Length > 0)
{
- string uris = endpoints.ToString(0, endpoints.Length - 1);
- clientOptions.Uri = uris;
+ string uris = endpointBuilder.ToString(0, endpointBuilder.Length - 1);
+ _lastDiscoveryLookupResult = new DiscoveryLookupResult(uris, username, password);
+ }
+ else
+ {
+ _lastDiscoveryLookupResult = null;
}
}
internal async Task ProvideRuntimeReplacementsAsync(ICollection discoveryClientsFromServiceProvider, CancellationToken cancellationToken)
{
- if (_configServerDiscoveryService is not null)
- {
- await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken);
- }
+ await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken);
}
internal async Task ShutdownAsync(CancellationToken cancellationToken)
{
- if (_configServerDiscoveryService is not null)
- {
- await _configServerDiscoveryService.ShutdownAsync(cancellationToken);
- }
+ await _configServerDiscoveryService.ShutdownAsync(cancellationToken);
}
///
- /// Creates the that will be used in accessing the Spring Cloud Configuration server.
+ /// Creates the that will be used in accessing the Spring Cloud Config server.
///
+ ///
+ /// A snapshot of the client options to use for this request.
+ ///
///
/// The Uri used when accessing the server.
///
@@ -447,127 +602,86 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken)
///
/// The HttpRequestMessage built from the path.
///
- internal async Task GetRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken)
+ internal async Task GetConfigServerRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri,
+ CancellationToken cancellationToken)
{
var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped));
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uriWithoutUserInfo);
if (requestUri.TryGetUsernamePassword(out string? username, out string? password) && password.Length > 0)
{
- _logger.LogDebug("Adding credentials from '{RequestUri}' to Authorization header.", requestUri.ToMaskedString());
+ LogAddingCredentials(requestUri);
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")));
}
else
{
- if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri))
+ if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
{
- using HttpClient httpClient = CreateHttpClient(ClientOptions);
- var accessTokenUri = new Uri(ClientOptions.AccessTokenUri);
+ using HttpClient httpClient = CreateHttpClient(optionsSnapshot);
+ var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
string accessToken =
- await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken);
+ await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken);
- _logger.LogDebug("Fetched access token from '{AccessTokenUri}'.", accessTokenUri.ToMaskedString());
+ LogAccessTokenFetched(accessTokenUri);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
}
- if (!string.IsNullOrEmpty(ClientOptions.Token) && ClientOptions is { Uri: not null, IsMultiServerConfiguration: false })
+ if (!string.IsNullOrEmpty(optionsSnapshot.Token) && optionsSnapshot is { Uri: not null, IsMultiServerConfiguration: false })
{
- if (!ClientOptions.DisableTokenRenewal)
- {
- RenewToken();
- }
-
- requestMessage.Headers.Add(TokenHeader, ClientOptions.Token);
+ requestMessage.Headers.Add(TokenHeader, optionsSnapshot.Token);
}
return requestMessage;
}
- ///
- /// Adds the client settings for the Configuration Server to the data dictionary.
- ///
- internal void AddConfigServerClientOptions()
+ internal async Task RemoteLoadAsync(ConfigServerClientOptions optionsSnapshot, List requestUris, string? label,
+ CancellationToken cancellationToken)
{
- Dictionary data = Data.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase);
-
- AddConfigServerClientOptions(data);
-
- Data = data;
- }
-
- ///
- /// Adds the client settings for the Configuration Server to the data dictionary.
- ///
- ///
- /// The client settings to add.
- ///
- private void AddConfigServerClientOptions(Dictionary data)
- {
- data["spring:cloud:config:enabled"] = ClientOptions.Enabled.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:failFast"] = ClientOptions.FailFast.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:env"] = ClientOptions.Environment;
- data["spring:cloud:config:label"] = ClientOptions.Label;
- data["spring:cloud:config:name"] = ClientOptions.Name;
- data["spring:cloud:config:uri"] = ClientOptions.Uri;
- data["spring:cloud:config:username"] = ClientOptions.Username;
- data["spring:cloud:config:password"] = ClientOptions.Password;
- data["spring:cloud:config:token"] = ClientOptions.Token;
- data["spring:cloud:config:timeout"] = ClientOptions.Timeout.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:pollingInterval"] = ClientOptions.PollingInterval.ToString(null, CultureInfo.InvariantCulture);
- data["spring:cloud:config:validateCertificates"] = ClientOptions.ValidateCertificates.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:accessTokenUri"] = ClientOptions.AccessTokenUri;
- data["spring:cloud:config:clientSecret"] = ClientOptions.ClientSecret;
- data["spring:cloud:config:clientId"] = ClientOptions.ClientId;
- data["spring:cloud:config:tokenTtl"] = ClientOptions.TokenTtl.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:tokenRenewRate"] = ClientOptions.TokenRenewRate.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:disableTokenRenewal"] = ClientOptions.DisableTokenRenewal.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:retry:enabled"] = ClientOptions.Retry.Enabled.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:retry:initialInterval"] = ClientOptions.Retry.InitialInterval.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:retry:maxInterval"] = ClientOptions.Retry.MaxInterval.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:retry:multiplier"] = ClientOptions.Retry.Multiplier.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:retry:maxAttempts"] = ClientOptions.Retry.MaxAttempts.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:discovery:enabled"] = ClientOptions.Discovery.Enabled.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:discovery:serviceId"] = ClientOptions.Discovery.ServiceId;
- data["spring:cloud:config:health:enabled"] = ClientOptions.Health.Enabled.ToString(CultureInfo.InvariantCulture);
- data["spring:cloud:config:health:timeToLive"] = ClientOptions.Health.TimeToLive.ToString(CultureInfo.InvariantCulture);
-
- foreach ((string headerName, string headerValue) in ClientOptions.Headers)
- {
- data[$"spring:cloud:config:headers:{headerName}"] = headerValue;
- }
- }
-
- internal async Task RemoteLoadAsync(List requestUris, string? label, CancellationToken cancellationToken)
- {
- _logger.LogTrace("Entered {Method}", nameof(RemoteLoadAsync));
+ LogRemoteLoadEntered(nameof(RemoteLoadAsync));
// Get client if not already set
- using HttpClient httpClient = CreateHttpClient(ClientOptions);
+ using HttpClient httpClient = CreateHttpClient(optionsSnapshot);
Exception? error = null;
foreach (Uri requestUri in requestUris)
{
- // Make Config Server URI from settings
- Uri uri = BuildConfigServerUri(requestUri, label);
+ try
+ {
+ // Make Config Server URI from settings
+ Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label);
+
+ LogTryingToConnect(uri);
+ HttpRequestMessage request;
+
+ try
+ {
+ // Get the request message (potentially fetches access token)
+ LogBuildingHttpRequest();
+ request = await GetConfigServerRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
+ }
+ catch (Exception exception) when (!exception.IsCancellation())
+ {
+ if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
+ {
+ var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
+ LogFailedToFetchAccessToken(exception, accessTokenUri);
- _logger.LogDebug("Trying to connect to Config Server at {RequestUri}", uri.ToMaskedString());
+ continue;
+ }
- // Get the request message
- _logger.LogTrace("Building HTTP request message");
- HttpRequestMessage request = await GetRequestMessageAsync(uri, cancellationToken);
+ throw;
+ }
- // Invoke Config Server
- try
- {
- _logger.LogTrace("Sending HTTP request");
+ // Invoke Config Server
+ LogSendingHttpRequest();
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
- _logger.LogDebug("Config Server returned status: {StatusCode} invoking path: {RequestUri}", response.StatusCode, uri.ToMaskedString());
+ LogConfigServerReturnedStatus(uri, response.StatusCode);
if (response.StatusCode != HttpStatusCode.OK)
{
@@ -579,13 +693,20 @@ private void AddConfigServerClientOptions(Dictionary data)
// Throw if status >= 400
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
- throw new HttpRequestException($"Config Server returned status: {response.StatusCode} invoking path: {uri.ToMaskedString()}");
+ MaskedUri masked = uri;
+ throw new HttpRequestException($"Config Server returned status: {response.StatusCode} invoking path: {masked}");
+ }
+
+ if ((int)response.StatusCode >= 300)
+ {
+ MaskedUri masked = response.Headers.Location;
+ LogConfigServerRedirected((int)response.StatusCode, masked);
}
return null;
}
- _logger.LogTrace("Parsing JSON response");
+ LogParsingJsonResponse();
return await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken);
}
catch (Exception exception) when (!exception.IsCancellation())
@@ -594,7 +715,7 @@ private void AddConfigServerClientOptions(Dictionary data)
if (IsSocketError(exception))
{
- _logger.LogTrace(exception, "Socket error detected");
+ LogSocketError(exception);
continue;
}
@@ -604,7 +725,7 @@ private void AddConfigServerClientOptions(Dictionary data)
if (error != null)
{
- throw error;
+ ExceptionDispatchInfo.Capture(error).Throw();
}
return null;
@@ -613,6 +734,9 @@ private void AddConfigServerClientOptions(Dictionary data)
///
/// Creates the Uri that will be used in accessing the Configuration Server.
///
+ ///
+ /// A snapshot of the client options to use for URI construction.
+ ///
///
/// Base server uri to use.
///
@@ -622,23 +746,23 @@ private void AddConfigServerClientOptions(Dictionary data)
///
/// The request URI for the Configuration Server.
///
- internal Uri BuildConfigServerUri(Uri serverUri, string? label)
+ internal Uri BuildConfigServerUri(ConfigServerClientOptions optionsSnapshot, Uri serverUri, string? label)
{
ArgumentNullException.ThrowIfNull(serverUri);
var uriBuilder = new UriBuilder(serverUri);
- if (!string.IsNullOrEmpty(ClientOptions.Username))
+ if (!string.IsNullOrEmpty(optionsSnapshot.Username))
{
- uriBuilder.UserName = WebUtility.UrlEncode(ClientOptions.Username);
+ uriBuilder.UserName = WebUtility.UrlEncode(optionsSnapshot.Username);
}
- if (!string.IsNullOrEmpty(ClientOptions.Password))
+ if (!string.IsNullOrEmpty(optionsSnapshot.Password))
{
- uriBuilder.Password = WebUtility.UrlEncode(ClientOptions.Password);
+ uriBuilder.Password = WebUtility.UrlEncode(optionsSnapshot.Password);
}
- string pathSuffix = $"{WebUtility.UrlEncode(ClientOptions.Name)}/{WebUtility.UrlEncode(ClientOptions.Environment)}";
+ string pathSuffix = $"{WebUtility.UrlEncode(optionsSnapshot.Name)}/{WebUtility.UrlEncode(optionsSnapshot.Environment)}";
if (!string.IsNullOrWhiteSpace(label))
{
@@ -689,7 +813,7 @@ private void AddPropertySource(PropertySource? source, Dictionary RefreshVaultTokenAsync(CancellationToken.None).GetAwaiter().GetResult(), null,
- TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate), TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate));
-#pragma warning restore S4462 // Calls to "async" methods should not be blocking
- }
-
- // fire and forget
- internal async Task RefreshVaultTokenAsync(CancellationToken cancellationToken)
+ ///
+ /// Extends the lease of the current HashiCorp Vault token; it does not generate a new token. A new token is only picked up when the configuration
+ /// changes and reconfigures the timer.
+ ///
+ internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken)
{
- if (string.IsNullOrEmpty(ClientOptions.Token))
+ if (string.IsNullOrEmpty(optionsSnapshot.Token))
{
return;
}
- string obscuredToken = $"{ClientOptions.Token[..4]}[*]{ClientOptions.Token[^4..]}";
+ string obscuredToken = $"{optionsSnapshot.Token[..4]}[*]{optionsSnapshot.Token[^4..]}";
try
{
- using HttpClient httpClient = CreateHttpClient(ClientOptions);
+ using HttpClient httpClient = CreateHttpClient(optionsSnapshot);
- Uri uri = GetVaultRenewUri();
- HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(uri, cancellationToken);
-
- _logger.LogInformation("Renewing Vault token {Token} for {Ttl} milliseconds at Uri {Uri}", obscuredToken, ClientOptions.TokenTtl,
- uri.ToMaskedString());
+ Uri uri = BuildVaultRenewUri(optionsSnapshot);
+ HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
+ LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri);
using HttpResponseMessage response = await httpClient.SendAsync(message, cancellationToken);
if (response.StatusCode != HttpStatusCode.OK)
{
- _logger.LogWarning("Renewing Vault token {Token} returned status: {Status}", obscuredToken, response.StatusCode);
+ LogVaultTokenRenewalStatus(obscuredToken, response.StatusCode);
}
}
catch (Exception exception) when (!exception.IsCancellation())
{
- _logger.LogError(exception, "Unable to renew Vault token {Token}. Is the token invalid or expired?", obscuredToken);
+ LogUnableToRenewVaultToken(exception, obscuredToken);
}
}
- private Uri GetVaultRenewUri()
+ private static Uri BuildVaultRenewUri(ConfigServerClientOptions optionsSnapshot)
{
- string baseUri = ClientOptions.Uri!.Split(',')[0].Trim();
+ string baseUri = optionsSnapshot.Uri!.Split(',')[0].Trim();
if (!baseUri.EndsWith('/'))
{
@@ -753,42 +869,38 @@ private Uri GetVaultRenewUri()
return new Uri(baseUri + VaultRenewPath, UriKind.RelativeOrAbsolute);
}
- private async Task GetVaultRenewRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken)
+ private async Task GetVaultRenewRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri,
+ CancellationToken cancellationToken)
{
var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped));
var requestMessage = new HttpRequestMessage(HttpMethod.Post, uriWithoutUserInfo);
- if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri))
+ if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
{
- using HttpClient httpClient = CreateHttpClient(ClientOptions);
- var accessTokenUri = new Uri(ClientOptions.AccessTokenUri);
+ using HttpClient httpClient = CreateHttpClient(optionsSnapshot);
+ var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
- string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken);
+ string accessToken =
+ await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken);
- _logger.LogDebug("Fetched access token from '{AccessTokenUri}'.", accessTokenUri.ToMaskedString());
+ LogAccessTokenFetched(accessTokenUri);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
- if (!string.IsNullOrEmpty(ClientOptions.Token))
+ if (!string.IsNullOrEmpty(optionsSnapshot.Token))
{
- requestMessage.Headers.Add(VaultTokenHeader, ClientOptions.Token);
+ requestMessage.Headers.Add(VaultTokenHeader, optionsSnapshot.Token);
}
- int renewTtlInSeconds = ClientOptions.TokenTtl / 1000;
+ int renewTtlInSeconds = optionsSnapshot.TokenTtl / 1000;
string json = $"{{\"increment\":{renewTtlInSeconds}}}";
requestMessage.Content = new StringContent(json, Encoding.UTF8, "application/json");
return requestMessage;
}
- internal bool IsDiscoveryFirstEnabled()
- {
- IConfigurationSection clientConfigSection = _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix);
- return clientConfigSection.GetValue("discovery:enabled", ClientOptions.Discovery.Enabled);
- }
-
///
- /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Configuration Server.
+ /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Config Server.
///
///
/// The settings used to configure the HttpClient.
@@ -799,17 +911,11 @@ internal bool IsDiscoveryFirstEnabled()
internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions)
{
ArgumentNullException.ThrowIfNull(clientOptions);
- ObjectDisposedException.ThrowIf(_httpClientHandler == null, this);
- var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions.ClientCertificate));
- clientCertificateConfigurer.Configure("ConfigServer", _httpClientHandler);
+ HttpClientHandler handler = _createHttpClientHandler();
+ ConfigureHttpClientHandler(handler, clientOptions);
- var validateCertificatesHandler =
- new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions));
-
- validateCertificatesHandler.Configure(Options.DefaultName, _httpClientHandler);
-
- var httpClient = new HttpClient(_httpClientHandler, false);
+ var httpClient = new HttpClient(handler, _disposeHttpClientHandler);
httpClient.ConfigureForSteeltoe(clientOptions.HttpTimeout);
foreach ((string headerName, string headerValue) in clientOptions.Headers)
@@ -820,6 +926,27 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions)
return httpClient;
}
+ private static void ConfigureHttpClientHandler(HttpClientHandler httpClientHandler, ConfigServerClientOptions optionsSnapshot)
+ {
+ if (!string.IsNullOrEmpty(optionsSnapshot.Token))
+ {
+ // Disable AutoRedirect to prevent credential leaks. HttpClientHandler strips the Authorization header
+ // on redirects but does not strip custom headers (X-Vault-Token, X-Config-Token), which this handler
+ // uses for Vault token renewal and Config Server fetches.
+ httpClientHandler.AllowAutoRedirect = false;
+ }
+
+ httpClientHandler.ClientCertificates.Clear();
+
+ var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot.ClientCertificate));
+ clientCertificateConfigurer.Configure("ConfigServer", httpClientHandler);
+
+ var validateCertificatesHandler =
+ new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot));
+
+ validateCertificatesHandler.Configure(Options.DefaultName, httpClientHandler);
+ }
+
private static bool IsSocketError(Exception exception)
{
return exception is HttpRequestException && exception.InnerException is SocketException;
@@ -827,17 +954,157 @@ private static bool IsSocketError(Exception exception)
public void Dispose()
{
- _refreshTimer?.Dispose();
- _refreshTimer = null;
+ lock (_lifecycleLock)
+ {
+ if (_shutdownToken.IsCancellationRequested)
+ {
+ return;
+ }
- _timerTickLock?.Dispose();
- _timerTickLock = null;
+ LogDisposing();
+ _shutdownTokenSource.Cancel();
+ _changeTokenRegistration.Dispose();
+ ShutdownTimers();
+ _shutdownTokenSource.Dispose();
+ }
+ }
+
+ private void ShutdownTimers()
+ {
+ // This is fast because in-flight timer callbacks terminate quickly: outstanding HTTP requests are canceled via shutdown token.
+
+ using var reloadTimerStopped = new ManualResetEvent(false);
+ using var vaultTimerStopped = new ManualResetEvent(false);
- if (_ownsHttpClientHandler)
+ if (_configurationReloadTimer == null || !_configurationReloadTimer.Dispose(reloadTimerStopped))
{
- _httpClientHandler?.Dispose();
+ reloadTimerStopped.Set();
}
- _httpClientHandler = null;
+ if (_vaultRenewTimer == null || !_vaultRenewTimer.Dispose(vaultTimerStopped))
+ {
+ vaultTimerStopped.Set();
+ }
+
+ WaitHandle.WaitAll([
+ reloadTimerStopped,
+ vaultTimerStopped
+ ]);
+
+ _configurationReloadTimer = null;
+ _vaultRenewTimer = null;
}
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Rebinding options after outer configuration change.")]
+ private partial void LogEnteringOnSettingsChanged();
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to bind Config Server options from configuration.")]
+ private partial void LogBindSettingsFailed(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Entering remote configuration reload polling cycle.")]
+ private partial void LogEnteringConfigurationReloadCycle();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling lock obtained.")]
+ private partial void LogConfigurationReloadCycleLockObtained();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Previous remote configuration reload cycle is still running, or already disposed; skipping this cycle.")]
+ private partial void LogSkippingConfigurationReloadCycle();
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to reload remote configuration during polling.")]
+ private partial void LogConfigurationReloadCycleFailed(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling cycle completed, releasing lock.")]
+ private partial void LogConfigurationReloadCycleCompleted();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Entering Vault token renewal cycle.")]
+ private partial void LogEnteringVaultRenewCycle();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal lock obtained.")]
+ private partial void LogVaultRenewCycleLockObtained();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Previous Vault token renewal cycle is still running, or already disposed; skipping this cycle.")]
+ private partial void LogSkippingVaultRenewCycle();
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to renew Vault token.")]
+ private partial void LogVaultRenewCycleFailed(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal cycle completed, releasing lock.")]
+ private partial void LogVaultRenewCycleCompleted();
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching remote configuration.")]
+ private partial void LogConfigServerClientDisabled();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching remote configuration from server(s).")]
+ private partial void LogFetchingRemoteConfiguration();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Processing label '{Label}'.")]
+ private partial void LogProcessingLabel(string? label);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server uris listed.")]
+ private partial void LogMultipleConfigServerUris();
+
+ [LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true,
+ Message = "Located environment with name {Name}, profiles {Profiles}, label {Label}, version {Version} and state {State}.")]
+ private partial void LogEnvironmentLocated(string? name, string profiles, string? label, string? version, string? state);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has changed, raising configuration reload.")]
+ private partial void LogDataChanged();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has not changed.")]
+ private partial void LogDataNotChanged();
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching remote configuration from server(s).")]
+ private partial void LogFetchingRemoteConfigurationFailed(Exception error);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")]
+ private partial void LogAddingCredentials(MaskedUri requestUri);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from {AccessTokenUri}.")]
+ private partial void LogAccessTokenFetched(MaskedUri accessTokenUri);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to fetch access token from '{AccessTokenUri}'.")]
+ private partial void LogFailedToFetchAccessToken(Exception exception, MaskedUri accessTokenUri);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Entered {Method}.")]
+ private partial void LogRemoteLoadEntered(string method);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Trying to connect to Config Server at {RequestUri}.")]
+ private partial void LogTryingToConnect(MaskedUri requestUri);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Building HTTP request message.")]
+ private partial void LogBuildingHttpRequest();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Sending HTTP request.")]
+ private partial void LogSendingHttpRequest();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server returned status {StatusCode} for path {RequestUri}.")]
+ private partial void LogConfigServerReturnedStatus(MaskedUri requestUri, HttpStatusCode statusCode);
+
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message =
+ "Config Server returned a {StatusCode} redirect to '{Uri}'. Redirects are not followed to prevent credential leaks. Update 'spring:cloud:config:uri' to point directly to the target.")]
+ private partial void LogConfigServerRedirected(int statusCode, MaskedUri uri);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Parsing JSON response.")]
+ private partial void LogParsingJsonResponse();
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Socket error detected.")]
+ private partial void LogSocketError(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Config Server exception for property {Key} of type {Type}.")]
+ private partial void LogConfigServerPropertyException(Exception exception, string key, Type type);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Renewing Vault token {Token} for {Ttl} milliseconds at Uri {Uri}.")]
+ private partial void LogRenewingVaultToken(string token, int ttl, MaskedUri uri);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Renewing Vault token {Token} returned status {Status}.")]
+ private partial void LogVaultTokenRenewalStatus(string token, HttpStatusCode status);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Unable to renew Vault token {Token}. The token is likely invalid or has expired.")]
+ private partial void LogUnableToRenewVaultToken(Exception exception, string token);
+
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Disposing Config Server configuration provider.")]
+ private partial void LogDisposing();
+
+ private sealed record DiscoveryLookupResult(string ConfigServerUri, string? Username, string? Password);
}
diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs
index 02d2d15f0a..711a9c0796 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs
@@ -5,7 +5,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Steeltoe.Common.Certificates;
namespace Steeltoe.Configuration.ConfigServer;
@@ -13,74 +12,68 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource
{
private readonly ILoggerFactory _loggerFactory;
- internal List Sources { get; } = [];
- internal Dictionary Properties { get; } = [];
+ internal List Sources { get; }
+ internal Dictionary Properties { get; }
///
- /// Gets the default settings the Config Server client uses to contact the Config Server.
+ /// Gets the default options the client uses to contact Config Server.
///
- internal ConfigServerClientOptions DefaultOptions { get; }
+ public ConfigServerClientOptions DefaultOptions { get; }
///
- /// Gets the configuration the Config Server client uses to contact the Config Server. Values returned override the default values provided in
- /// .
+ /// Gets the configuration the client uses to contact Config Server. Entries override .
///
- internal IConfiguration? Configuration { get; private set; }
+ public IConfiguration? Configuration { get; private set; }
///
- /// Initializes a new instance of the class.
+ /// Gets an optional delegate that further configures options from code, after settings from have been applied.
///
- ///
- /// the default settings used by the Config Server client.
- ///
- ///
- /// configuration used by the Config Server client. Values will override those found in default settings.
- ///
- ///
- /// Used for internal logging. Pass to disable logging.
- ///
- public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, ILoggerFactory loggerFactory)
- {
- ArgumentNullException.ThrowIfNull(configuration);
- ArgumentNullException.ThrowIfNull(defaultOptions);
- ArgumentNullException.ThrowIfNull(loggerFactory);
+ public Action? Configure { get; }
- Configuration = configuration;
- DefaultOptions = defaultOptions;
- _loggerFactory = loggerFactory;
- }
+ ///
+ /// Gets an optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is
+ /// responsible for handler disposal.
+ ///
+ public Func? CreateHttpClientHandler { get; }
///
/// Initializes a new instance of the class.
///
///
- /// the default settings used by the Config Server client.
+ /// The default options the client uses to contact Config Server.
///
///
- /// configuration sources used by the Config Server client. The will be built from these sources and the values will
- /// override those found in .
+ /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries override
+ /// .
///
///
- /// properties to be used when sources are built.
+ /// Configuration properties the client uses to contact Config Server. The will be built from these, whose entries override
+ /// .
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from the built have been applied.
+ ///
+ ///
+ /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible
+ /// for handler disposal.
///
///
/// Used for internal logging. Pass to disable logging.
///
public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IList sources,
- IDictionary? properties, ILoggerFactory loggerFactory)
+ IDictionary? properties, Action? configure, Func? createHttpClientHandler,
+ ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(defaultOptions);
ArgumentNullException.ThrowIfNull(sources);
ArgumentNullException.ThrowIfNull(loggerFactory);
Sources = sources.ToList();
-
- if (properties != null)
- {
- Properties = new Dictionary(properties);
- }
+ Properties = properties != null ? new Dictionary(properties) : [];
DefaultOptions = defaultOptions;
+ Configure = configure;
+ CreateHttpClientHandler = createHttpClientHandler;
_loggerFactory = loggerFactory;
}
@@ -95,37 +88,19 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions,
///
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
- if (Configuration == null)
- {
- // Create our own builder to build sources
- var configurationBuilder = new ConfigurationBuilder();
-
- foreach (IConfigurationSource source in Sources)
- {
- configurationBuilder.Add(source);
- }
-
- // Use properties provided
- foreach (KeyValuePair pair in Properties)
- {
- configurationBuilder.Properties.Add(pair);
- }
+ var configurationBuilder = new ConfigurationBuilder();
- // Create configuration
- Configuration = configurationBuilder.Build();
+ foreach (IConfigurationSource source in Sources)
+ {
+ configurationBuilder.Add(source);
}
- string? clientCertificatePath = Configuration.GetValue($"{CertificateOptions.ConfigurationKeyPrefix}:ConfigServer:CertificateFilePath");
-
- if (!string.IsNullOrEmpty(clientCertificatePath) && DefaultOptions.ClientCertificate.Certificate == null)
+ foreach (KeyValuePair pair in Properties)
{
- var certificateConfigurer = new ConfigureCertificateOptions(Configuration);
-
- var options = new CertificateOptions();
- certificateConfigurer.Configure("ConfigServer", options);
- DefaultOptions.ClientCertificate.Certificate = options.Certificate;
+ configurationBuilder.Properties.Add(pair);
}
+ Configuration = configurationBuilder.Build();
return new ConfigServerConfigurationProvider(this, _loggerFactory);
}
}
diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs
index 5bc32ebaf1..595fa993cb 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs
@@ -12,36 +12,60 @@
using Steeltoe.Discovery.Configuration;
using Steeltoe.Discovery.Consul;
using Steeltoe.Discovery.Eureka;
+using LockPrimitive =
+#if NET10_0_OR_GREATER
+ System.Threading.Lock
+#else
+ object
+#endif
+ ;
namespace Steeltoe.Configuration.ConfigServer;
-internal sealed class ConfigServerDiscoveryService
+internal sealed partial class ConfigServerDiscoveryService
{
private static readonly AssemblyLoader AssemblyLoader = new();
private readonly IConfiguration _configuration;
- private readonly ConfigServerClientOptions _options;
+ private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
+ private readonly LockPrimitive _initLock = new();
private ServiceProvider? _temporaryServiceProviderForDiscoveryClients;
+ private volatile ICollection? _discoveryClients;
- internal ICollection DiscoveryClients { get; private set; }
+ internal ICollection? DiscoveryClients
+ {
+ get => _discoveryClients;
+ private set => _discoveryClients = value;
+ }
- public ConfigServerDiscoveryService(IConfiguration configuration, ConfigServerClientOptions options, ILoggerFactory loggerFactory)
+ public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(configuration);
- ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
_configuration = configuration;
- _options = options;
+ _loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger();
- DiscoveryClients = SetupDiscoveryClients(loggerFactory);
}
- // Create discovery clients to be used (hopefully only) during startup
- private IDiscoveryClient[] SetupDiscoveryClients(ILoggerFactory loggerFactory)
+ private void EnsureInitialized()
+ {
+ if (_discoveryClients == null)
+ {
+ lock (_initLock)
+ {
+ if (_discoveryClients == null)
+ {
+ SetupDiscoveryClients();
+ }
+ }
+ }
+ }
+
+ private void SetupDiscoveryClients()
{
var tempServices = new ServiceCollection();
- tempServices.AddSingleton(loggerFactory);
+ tempServices.AddSingleton(_loggerFactory);
tempServices.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
// force settings to make sure we don't register the app here
@@ -70,7 +94,7 @@ private IDiscoveryClient[] SetupDiscoveryClients(ILoggerFactory loggerFactory)
WireEurekaDiscoveryClient(tempServices);
}
- return GetDiscoveryClientsFromServiceCollection(tempServices);
+ _discoveryClients = GetDiscoveryClientsFromServiceCollection(tempServices);
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -99,50 +123,56 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle
foreach (IDiscoveryClient discoveryClient in discoveryClients)
{
- _logger.LogDebug("Found discovery client of type {DiscoveryClientType}", discoveryClient.GetType());
+ LogDiscoveryClientFound(discoveryClient.GetType());
}
return discoveryClients;
}
- internal async Task> GetConfigServerInstancesAsync(CancellationToken cancellationToken)
+ internal async Task> GetConfigServerInstancesAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken)
{
+ ArgumentNullException.ThrowIfNull(optionsSnapshot);
+
+ EnsureInitialized();
+
int attempts = 0;
- int backOff = _options.Retry.InitialInterval;
+ int backOff = optionsSnapshot.Retry.InitialInterval;
List instances = [];
do
{
- _logger.LogDebug("Locating ConfigServer {ServiceId} via discovery", _options.Discovery.ServiceId);
+ LogLocatingConfigServer(optionsSnapshot.Discovery.ServiceId);
- if (_options.Discovery.ServiceId != null)
+ if (optionsSnapshot.Discovery.ServiceId != null)
{
- foreach (IDiscoveryClient discoveryClient in DiscoveryClients)
+ foreach (IDiscoveryClient discoveryClient in _discoveryClients ?? [])
{
try
{
- IList serviceInstances = await discoveryClient.GetInstancesAsync(_options.Discovery.ServiceId, cancellationToken);
+ IList serviceInstances =
+ await discoveryClient.GetInstancesAsync(optionsSnapshot.Discovery.ServiceId, cancellationToken);
+
instances.AddRange(serviceInstances);
}
catch (Exception exception) when (!exception.IsCancellation())
{
- _logger.LogError(exception, "Failed to get instances during ConfigServer lookup from {DiscoveryClient}.", discoveryClient.GetType());
+ LogFailedToGetInstances(exception, discoveryClient.GetType());
}
}
}
- if (!_options.Retry.Enabled || instances.Count > 0)
+ if (!optionsSnapshot.Retry.Enabled || instances.Count > 0)
{
break;
}
attempts++;
- if (attempts <= _options.Retry.MaxAttempts)
+ if (attempts <= optionsSnapshot.Retry.MaxAttempts)
{
- Thread.CurrentThread.Join(backOff);
- int nextBackOff = (int)(backOff * _options.Retry.Multiplier);
- backOff = Math.Min(nextBackOff, _options.Retry.MaxInterval);
+ await Task.Delay(backOff, cancellationToken);
+ int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier);
+ backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval);
}
else
{
@@ -158,7 +188,7 @@ internal async Task ProvideRuntimeReplacementsAsync(ICollection _logger;
@@ -30,7 +30,7 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider
if (Provider == null)
{
- _logger.LogWarning("Unable to find ConfigServerConfigurationProvider, health check disabled");
+ LogHealthCheckDisabled();
}
}
@@ -40,22 +40,24 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider
if (Provider == null)
{
- _logger.LogDebug("No Config Server provider found");
+ LogNoProviderFound();
health.Status = HealthStatus.Unknown;
health.Details.Add("error", "No Config Server provider found");
return health;
}
- if (!IsEnabled())
+ ConfigServerClientOptions optionsSnapshot = Provider.ClientOptions;
+
+ if (!optionsSnapshot.Health.Enabled)
{
return null;
}
- IList? sources = await GetPropertySourcesAsync(Provider, cancellationToken);
+ IList? sources = await GetPropertySourcesAsync(Provider, optionsSnapshot, cancellationToken);
if (sources == null || sources.Count == 0)
{
- _logger.LogDebug("No property sources found");
+ LogNoPropertySourcesFound();
health.Status = HealthStatus.Unknown;
health.Details.Add("error", "No property sources found");
return health;
@@ -67,51 +69,82 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider
internal void UpdateHealth(HealthCheckResult health, IList sources)
{
- _logger.LogDebug("Config Server health check returning UP");
+ LogHealthCheckReturningUp();
health.Status = HealthStatus.Up;
List names = [];
foreach (PropertySource source in sources)
{
- _logger.LogDebug("Returning property source: {PropertySource}", source.Name);
names.Add(source.Name);
}
+ ExpensiveLogReturningPropertySources(names);
health.Details.Add("propertySources", names);
}
- internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, CancellationToken cancellationToken)
+ private void ExpensiveLogReturningPropertySources(List names)
+ {
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ string propertySources = string.Join(", ", names);
+ LogReturningPropertySources(propertySources);
+ }
+ }
+
+ internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, ConfigServerClientOptions optionsSnapshot,
+ CancellationToken cancellationToken)
{
long currentTime = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
- if (IsCacheStale(currentTime))
+ if (IsCacheStale(currentTime, optionsSnapshot))
{
LastAccess = currentTime;
- _logger.LogDebug("Cache stale, fetching config server health");
- Cached = await provider.LoadInternalAsync(false, cancellationToken);
+ LogCacheStale();
+
+ try
+ {
+ Cached = await provider.LoadInternalAsync(optionsSnapshot, false, cancellationToken);
+ }
+ catch (ConfigServerException exception)
+ {
+ LogFetchFailed(exception);
+ Cached = null;
+ return null;
+ }
}
return Cached?.PropertySources;
}
- internal bool IsCacheStale(long accessTime)
+ internal bool IsCacheStale(long accessTime, ConfigServerClientOptions optionsSnapshot)
{
if (Cached == null)
{
return true;
}
- return accessTime - LastAccess >= GetTimeToLive();
+ return accessTime - LastAccess >= optionsSnapshot.Health.TimeToLive;
}
- internal bool IsEnabled()
- {
- return Provider is { ClientOptions.Health.Enabled: true };
- }
+ [LoggerMessage(Level = LogLevel.Warning, Message = "No Config Server provider found, health check disabled.")]
+ private partial void LogHealthCheckDisabled();
- internal long GetTimeToLive()
- {
- return Provider != null ? Provider.ClientOptions.Health.TimeToLive : long.MaxValue;
- }
+ [LoggerMessage(Level = LogLevel.Debug, Message = "No Config Server provider found.")]
+ private partial void LogNoProviderFound();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Failed fetching remote configuration from server(s).")]
+ private partial void LogFetchFailed(Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "No property sources found.")]
+ private partial void LogNoPropertySourcesFound();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server health check returning UP.")]
+ private partial void LogHealthCheckReturningUp();
+
+ [LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true, Message = "Returning property sources: {PropertySources}.")]
+ private partial void LogReturningPropertySources(string propertySources);
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Cache stale, fetching config server health.")]
+ private partial void LogCacheStale();
}
diff --git a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs
index bf3e0d0337..ab3dde3d9a 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -23,7 +24,7 @@ public static class ConfigServerHostBuilderExtensions
///
public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder)
{
- return AddConfigServer(builder, NullLoggerFactory.Instance);
+ return AddConfigServer(builder, null, NullLoggerFactory.Instance);
}
///
@@ -39,12 +40,49 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder)
/// The incoming so that additional calls can be chained.
///
public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddConfigServer(builder, null, loggerFactory);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure)
+ {
+ return AddConfigServer(builder, configure, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(loggerFactory);
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
- wrapper.AddConfigServer(loggerFactory);
+ wrapper.AddConfigServer(configure, loggerFactory);
return builder;
}
@@ -60,7 +98,7 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILog
///
public static IHostBuilder AddConfigServer(this IHostBuilder builder)
{
- return AddConfigServer(builder, NullLoggerFactory.Instance);
+ return AddConfigServer(builder, null, NullLoggerFactory.Instance);
}
///
@@ -76,12 +114,49 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder)
/// The incoming so that additional calls can be chained.
///
public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddConfigServer(builder, null, loggerFactory);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure)
+ {
+ return AddConfigServer(builder, configure, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(loggerFactory);
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
- wrapper.AddConfigServer(loggerFactory);
+ wrapper.AddConfigServer(configure, loggerFactory);
return builder;
}
@@ -98,7 +173,7 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFac
///
public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder)
{
- return AddConfigServer(builder, NullLoggerFactory.Instance);
+ return AddConfigServer(builder, null, NullLoggerFactory.Instance);
}
///
@@ -115,12 +190,52 @@ public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuild
/// The incoming so that additional calls can be chained.
///
public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddConfigServer(builder, null, loggerFactory);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to
+ /// the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure)
+ {
+ return AddConfigServer(builder, configure, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to
+ /// the service container.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure,
+ ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(loggerFactory);
HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
- wrapper.AddConfigServer(loggerFactory);
+ wrapper.AddConfigServer(configure, loggerFactory);
return builder;
}
diff --git a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs
index 624aa54ee4..8c891be1cd 100644
--- a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs
+++ b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs
@@ -25,11 +25,40 @@ public static class ConfigServerServiceCollectionExtensions
/// The incoming so that additional calls can be chained.
///
public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services)
+ {
+ return ConfigureConfigServerClientOptions(services, null);
+ }
+
+ ///
+ /// Adds for use with the options pattern.
+ ///
+ ///
+ /// The to add services to.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services, Action? configure)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions();
- services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>());
+
+ services.AddSingleton(serviceProvider =>
+ {
+ var configuration = serviceProvider.GetRequiredService();
+ return new ConfigureConfigServerClientOptions(configuration, configure);
+ });
+
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>(serviceProvider =>
+ serviceProvider.GetRequiredService()));
+
+ services.TryAddEnumerable(ServiceDescriptor
+ .Singleton, ConfigurationChangeTokenSource>());
return services;
}
@@ -64,10 +93,28 @@ public static IServiceCollection AddConfigServerHealthContributor(this IServiceC
/// The incoming so that additional calls can be chained.
///
public static IServiceCollection AddConfigServerServices(this IServiceCollection services)
+ {
+ return AddConfigServerServices(services, null);
+ }
+
+ ///
+ /// Configures , hosted service and health contributor, and ensures is
+ /// available.
+ ///
+ ///
+ /// The to add services to.
+ ///
+ ///
+ /// An optional delegate that further configures options from code, after settings from have been applied.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IServiceCollection AddConfigServerServices(this IServiceCollection services, Action? configure)
{
ArgumentNullException.ThrowIfNull(services);
- services.ConfigureConfigServerClientOptions();
+ services.ConfigureConfigServerClientOptions(configure);
services.TryAddSingleton(serviceProvider => (IConfigurationRoot)serviceProvider.GetRequiredService());
services.AddHostedService();
services.AddConfigServerHealthContributor();
diff --git a/src/Configuration/src/ConfigServer/ConfigurationSchema.json b/src/Configuration/src/ConfigServer/ConfigurationSchema.json
index 8f9e677fc0..0bfa06a7a1 100644
--- a/src/Configuration/src/ConfigServer/ConfigurationSchema.json
+++ b/src/Configuration/src/ConfigServer/ConfigurationSchema.json
@@ -173,7 +173,7 @@
},
"TokenRenewRate": {
"type": "integer",
- "description": "Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)."
+ "description": "Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)."
},
"TokenTtl": {
"type": "integer",
diff --git a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs
index 4a7ae64ffe..d1855d5d15 100644
--- a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs
+++ b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs
@@ -5,23 +5,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Steeltoe.Common;
+using Steeltoe.Common.Certificates;
using Steeltoe.Configuration.CloudFoundry;
namespace Steeltoe.Configuration.ConfigServer;
internal sealed class ConfigureConfigServerClientOptions : IConfigureOptions
{
- private const string VcapServicesConfigServerCredentialsPrefix = "vcap:services:p-config-server:0:credentials";
- private const string VcapServicesConfigServer30CredentialsPrefix = "vcap:services:p.config-server:0:credentials";
+ private const string VcapServicesConfigServerVersion2CredentialsPrefix = "vcap:services:p-config-server:0:credentials";
+ private const string VcapServicesConfigServerVersion3CredentialsPrefix = "vcap:services:p.config-server:0:credentials";
private const string VcapServicesConfigServerCredentialsAltPrefix = "vcap:services:config-server:0:credentials";
private readonly IConfiguration _configuration;
+ private readonly Action? _configure;
- public ConfigureConfigServerClientOptions(IConfiguration configuration)
+ public ConfigureConfigServerClientOptions(IConfiguration configuration, Action? configure)
{
ArgumentNullException.ThrowIfNull(configuration);
_configuration = configuration;
+ _configure = configure;
}
public void Configure(ConfigServerClientOptions options)
@@ -29,17 +32,21 @@ public void Configure(ConfigServerClientOptions options)
ArgumentNullException.ThrowIfNull(options);
_configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix).Bind(options);
+ _configure?.Invoke(options);
+
OverrideFromVcapServicesCredentials(options);
+ ConfigureClientCertificate(options);
options.Name ??= GetApplicationName();
+ options.Environment ??= "Production";
}
private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions options)
{
VcapServicesConfigServerCredentialsOptions credentialsOptions = new();
_configuration.GetSection(VcapServicesConfigServerCredentialsAltPrefix).Bind(credentialsOptions);
- _configuration.GetSection(VcapServicesConfigServer30CredentialsPrefix).Bind(credentialsOptions);
- _configuration.GetSection(VcapServicesConfigServerCredentialsPrefix).Bind(credentialsOptions);
+ _configuration.GetSection(VcapServicesConfigServerVersion2CredentialsPrefix).Bind(credentialsOptions);
+ _configuration.GetSection(VcapServicesConfigServerVersion3CredentialsPrefix).Bind(credentialsOptions);
options.Uri = credentialsOptions.Uri ?? options.Uri;
options.ClientId = credentialsOptions.ClientId ?? options.ClientId;
@@ -47,6 +54,26 @@ private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions optio
options.AccessTokenUri = credentialsOptions.AccessTokenUri ?? options.AccessTokenUri;
}
+ private void ConfigureClientCertificate(ConfigServerClientOptions options)
+ {
+ if (options.ClientCertificate.Certificate != null)
+ {
+ return;
+ }
+
+ var certificateConfigurer = new ConfigureCertificateOptions(_configuration);
+
+ var certificateOptions = new CertificateOptions();
+ certificateConfigurer.Configure("ConfigServer", certificateOptions);
+
+ if (certificateOptions.Certificate == null)
+ {
+ certificateConfigurer.Configure(certificateOptions);
+ }
+
+ options.ClientCertificate = certificateOptions.Clone();
+ }
+
private string? GetApplicationName()
{
var vcapOptions = new CloudFoundryApplicationOptions();
diff --git a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs
index 4709af0345..7c1c699ae2 100644
--- a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs
+++ b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs
@@ -10,36 +10,37 @@ namespace Steeltoe.Configuration.ConfigServer;
internal static class HostBuilderWrapperExtensions
{
- public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, ILoggerFactory loggerFactory)
+ public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, Action? configure,
+ ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(wrapper);
ArgumentNullException.ThrowIfNull(loggerFactory);
wrapper.ConfigureAppConfiguration((context, builder) =>
{
- ConfigServerClientOptions options = CreateOptions(context.HostEnvironment);
- builder.AddConfigServer(options, loggerFactory);
+ Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment);
+ builder.AddConfigServer(configureOptions, loggerFactory);
});
- wrapper.ConfigureServices(services => services.AddConfigServerServices());
+ wrapper.ConfigureServices((context, services) =>
+ {
+ Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment);
+ services.AddConfigServerServices(configureOptions);
+ });
return wrapper;
}
- private static ConfigServerClientOptions CreateOptions(IHostEnvironment hostEnvironment)
+ private static Action CreateOptionsConfigurer(Action? configure, IHostEnvironment hostEnvironment)
{
- var options = new ConfigServerClientOptions();
-
- if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName) && hostEnvironment.EnvironmentName != "Production")
+ return options =>
{
- // Only take IHostEnvironment.EnvironmentName when it was explicitly set (it defaults to "Production").
- // In the default case, we want the various other ways of setting the environment name to kick in.
- options.Environment = hostEnvironment.EnvironmentName;
- }
-
- // Intentionally NOT taking hostEnvironment.ApplicationName here, because that would disable the various other ways of setting the application name.
- // Ultimately, its value ends up in configuration key "applicationName", whose value is used if nothing else is configured.
+ configure?.Invoke(options);
- return options;
+ if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName))
+ {
+ options.Environment ??= hostEnvironment.EnvironmentName;
+ }
+ };
}
}
diff --git a/src/Configuration/src/ConfigServer/PublicAPI.Shipped.txt b/src/Configuration/src/ConfigServer/PublicAPI.Shipped.txt
index ec107fda87..6fc15c2161 100644
--- a/src/Configuration/src/ConfigServer/PublicAPI.Shipped.txt
+++ b/src/Configuration/src/ConfigServer/PublicAPI.Shipped.txt
@@ -1,15 +1,26 @@
#nullable enable
static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.ConfigServer.ConfigServerClientOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.ConfigServer.ConfigServerClientOptions! options) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHostBuilder!
static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.AddConfigServerHealthContributor(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.AddConfigServerServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.AddConfigServerServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.ConfigureConfigServerClientOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.ConfigureConfigServerClientOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Steeltoe.Configuration.ConfigServer.ConfigEnvironment
Steeltoe.Configuration.ConfigServer.ConfigEnvironment.ConfigEnvironment() -> void
diff --git a/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj b/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj
index a1144d9d89..a4297db81d 100644
--- a/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj
+++ b/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for Spring Cloud Config Server.
configuration;ConfigurationProvider;spring-cloud;Spring;Cloud;Config;Server;Spring-Cloud-Config-Server;tanzu
true
diff --git a/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs b/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs
index a92bdcabbc..6eca548d7f 100644
--- a/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs
+++ b/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs
@@ -40,7 +40,7 @@ private IBufferedCipher CreateCipher(string algorithm)
return algorithm.ToUpperInvariant() switch
{
"DEFAULT" => CipherUtilities.GetCipher("RSA/NONE/PKCS1Padding"),
- "OAEP" => CipherUtilities.GetCipher("RSA/ECB/PKCS1"),
+ "OAEP" => CipherUtilities.GetCipher("RSA/NONE/OAEPWithSHA1AndMGF1Padding"),
_ => throw new ArgumentException("algorithm should be one of DEFAULT or OAEP")
};
}
diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs
index 5caa46c875..e836828333 100644
--- a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs
+++ b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs
@@ -13,10 +13,13 @@ internal sealed partial class DecryptionConfigurationProvider(
IList providers, ITextDecryptor? textDecryptor, ILoggerFactory loggerFactory)
: CompositeConfigurationProvider(providers, loggerFactory)
{
+ private const int RegexMatchTimeoutInMilliseconds = 1_000;
+
private readonly ILogger _logger = loggerFactory.CreateLogger();
private ITextDecryptor? _textDecryptor = textDecryptor;
- [GeneratedRegex("^{cipher}({key:(?.*)})?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)]
+ [GeneratedRegex("^{cipher}({key:(?.*)})?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
+ RegexMatchTimeoutInMilliseconds)]
private static partial Regex CipherRegex();
public override bool TryGet(string key, out string? value)
@@ -43,7 +46,7 @@ public override bool TryGet(string key, out string? value)
}
}
- return value != null;
+ return found;
}
private ITextDecryptor EnsureDecryptor(IConfigurationRoot configurationRoot)
@@ -53,5 +56,5 @@ private ITextDecryptor EnsureDecryptor(IConfigurationRoot configurationRoot)
}
[LoggerMessage(Level = LogLevel.Trace, Message = "Decrypted value '{CipherValue}' at key '{Key}' to '{PlainTextValue}'.")]
- private partial void LogDecrypt(string key, string? cipherValue, string? plainTextValue);
+ private partial void LogDecrypt(string key, string cipherValue, string plainTextValue);
}
diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs
index a55cbc8aee..d703d9e247 100644
--- a/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs
+++ b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs
@@ -35,5 +35,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
}
[LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")]
- private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount);
+ private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount);
}
diff --git a/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj b/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj
index ee14db7fcd..99ea4150a5 100644
--- a/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj
+++ b/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for decrypting encrypted configuration values.
configuration;ConfigurationProvider;cryptography;decryption;Spring;Boot
true
diff --git a/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationBuilderExtensions.cs b/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationBuilderExtensions.cs
index c946f40952..3b0ffa3cd8 100644
--- a/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationBuilderExtensions.cs
+++ b/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationBuilderExtensions.cs
@@ -68,7 +68,7 @@ public static IConfigurationBuilder AddKubernetesServiceBindings(this IConfigura
/// Whether the configuration should be reloaded if the files are changed, added or removed.
///
///
- /// A predicate which is called before adding a key to the configuration. If it returns false, the key will be ignored.
+ /// A predicate that is called before adding a key to the configuration. If it returns false, the key will be ignored.
///
///
/// The source to read Kubernetes secret files on disk from.
diff --git a/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationSchema.json b/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationSchema.json
new file mode 100644
index 0000000000..df9796cf2b
--- /dev/null
+++ b/src/Configuration/src/Kubernetes.ServiceBindings/ConfigurationSchema.json
@@ -0,0 +1,20 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.Kubernetes": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.Kubernetes.ServiceBindings": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/Kubernetes.ServiceBindings/KubernetesServiceBindingConfigurationProvider.cs b/src/Configuration/src/Kubernetes.ServiceBindings/KubernetesServiceBindingConfigurationProvider.cs
index 23df9fdbf3..952c9ccc1a 100644
--- a/src/Configuration/src/Kubernetes.ServiceBindings/KubernetesServiceBindingConfigurationProvider.cs
+++ b/src/Configuration/src/Kubernetes.ServiceBindings/KubernetesServiceBindingConfigurationProvider.cs
@@ -9,7 +9,7 @@
namespace Steeltoe.Configuration.Kubernetes.ServiceBindings;
-internal sealed class KubernetesServiceBindingConfigurationProvider : PostProcessorConfigurationProvider, IDisposable
+internal sealed class KubernetesServiceBindingConfigurationProvider : PostProcessorConfigurationProvider
{
public const string ProviderKey = "provider";
public const string TypeKey = "type";
@@ -91,9 +91,10 @@ private void Load(bool reload)
OnReload();
}
- public void Dispose()
+ public override void Dispose()
{
_changeToken?.Dispose();
+ base.Dispose();
}
private void AddBindingType(ServiceBinding binding, Dictionary data)
diff --git a/src/Configuration/src/Kubernetes.ServiceBindings/Properties/AssemblyInfo.cs b/src/Configuration/src/Kubernetes.ServiceBindings/Properties/AssemblyInfo.cs
index 81a5cd3ad3..2f1ffe7ab8 100644
--- a/src/Configuration/src/Kubernetes.ServiceBindings/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/Kubernetes.ServiceBindings/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.Kubernetes", "Steeltoe.Configuration.Kubernetes.ServiceBindings")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.Kubernetes.ServiceBindings.Test")]
diff --git a/src/Configuration/src/Kubernetes.ServiceBindings/Steeltoe.Configuration.Kubernetes.ServiceBindings.csproj b/src/Configuration/src/Kubernetes.ServiceBindings/Steeltoe.Configuration.Kubernetes.ServiceBindings.csproj
index 502fe1e4d4..7439614f6f 100644
--- a/src/Configuration/src/Kubernetes.ServiceBindings/Steeltoe.Configuration.Kubernetes.ServiceBindings.csproj
+++ b/src/Configuration/src/Kubernetes.ServiceBindings/Steeltoe.Configuration.Kubernetes.ServiceBindings.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for reading from Service Binding Specification for Kubernetes.
configuration;ConfigurationProvider;kubernetes;k8s;bindings;service-bindings;cloud-native-bindings;cnb;tanzu
true
diff --git a/src/Configuration/src/Placeholder/ConfigurationSchema.json b/src/Configuration/src/Placeholder/ConfigurationSchema.json
new file mode 100644
index 0000000000..fd9d115255
--- /dev/null
+++ b/src/Configuration/src/Placeholder/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.Placeholder": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs
index 1c4c341578..3f956affad 100644
--- a/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs
+++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs
@@ -36,7 +36,7 @@ public override bool TryGet(string key, out string? value)
}
}
- return value != null;
+ return found;
}
[LoggerMessage(Level = LogLevel.Trace, Message = "Replaced value '{OriginalValue}' at key '{Key}' with '{ReplacementValue}'.")]
diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs
index 0b6a35b2f9..29ecbb202a 100644
--- a/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs
+++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs
@@ -32,5 +32,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
}
[LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")]
- private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount);
+ private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount);
}
diff --git a/src/Configuration/src/Placeholder/Properties/AssemblyInfo.cs b/src/Configuration/src/Placeholder/Properties/AssemblyInfo.cs
index 1a575f9ac5..db6fab0b9f 100644
--- a/src/Configuration/src/Placeholder/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/Placeholder/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.Placeholder")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer")]
diff --git a/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs
index 4cca8c32f1..c861fdcc6b 100644
--- a/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs
+++ b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs
@@ -22,7 +22,7 @@ namespace Steeltoe.Configuration.Placeholder;
/// .
///
///
-internal sealed class PropertyPlaceholderHelper
+internal sealed partial class PropertyPlaceholderHelper
{
private const string Prefix = "${";
private const string Suffix = "}";
@@ -110,7 +110,7 @@ public PropertyPlaceholderHelper(ILogger logger)
propertyValue = ParseStringValue(propertyValue, configuration, visitedPlaceholders);
Replace(result, startIndex, endIndex + Suffix.Length, propertyValue);
- _logger.LogDebug("Resolved placeholder '{Placeholder}' to '{Value}'", innerPlaceholder, propertyValue);
+ LogPlaceholderResolved(innerPlaceholder, propertyValue);
startIndex = IndexOf(result, Prefix, startIndex + propertyValue.Length);
}
else
@@ -202,6 +202,9 @@ private static string Substring(StringBuilder builder, int start, int end)
return builder.ToString()[start..end];
}
+ [LoggerMessage(Level = LogLevel.Trace, Message = "Resolved placeholder '{Placeholder}' to '{Value}'.")]
+ private partial void LogPlaceholderResolved(string placeholder, string value);
+
private readonly struct PlaceholderExpression(string key, string? defaultValue) : IEquatable
{
public string Key { get; } = key;
diff --git a/src/Configuration/src/Placeholder/Steeltoe.Configuration.Placeholder.csproj b/src/Configuration/src/Placeholder/Steeltoe.Configuration.Placeholder.csproj
index 18dbfa28b7..3d5ee8bfee 100644
--- a/src/Configuration/src/Placeholder/Steeltoe.Configuration.Placeholder.csproj
+++ b/src/Configuration/src/Placeholder/Steeltoe.Configuration.Placeholder.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for resolving property placeholders in configuration values.
configuration;ConfigurationProvider;placeholders;Spring;Boot
true
diff --git a/src/Configuration/src/RandomValue/ConfigurationSchema.json b/src/Configuration/src/RandomValue/ConfigurationSchema.json
new file mode 100644
index 0000000000..049c278508
--- /dev/null
+++ b/src/Configuration/src/RandomValue/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.RandomValue": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/RandomValue/Properties/AssemblyInfo.cs b/src/Configuration/src/RandomValue/Properties/AssemblyInfo.cs
index 11ef1de91a..e2e0837a76 100644
--- a/src/Configuration/src/RandomValue/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/RandomValue/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.RandomValue")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.RandomValue.Test")]
diff --git a/src/Configuration/src/RandomValue/RandomValueProvider.cs b/src/Configuration/src/RandomValue/RandomValueProvider.cs
index 8eed55e1c4..dbbebe0195 100644
--- a/src/Configuration/src/RandomValue/RandomValueProvider.cs
+++ b/src/Configuration/src/RandomValue/RandomValueProvider.cs
@@ -12,7 +12,7 @@ namespace Steeltoe.Configuration.RandomValue;
///
/// Configuration provider that provides random values. Note: This code was inspired by the Spring Boot equivalent class.
///
-internal sealed class RandomValueProvider : ConfigurationProvider
+internal sealed partial class RandomValueProvider : ConfigurationProvider
{
private readonly ILogger _logger;
private readonly string _prefix;
@@ -60,7 +60,7 @@ public override bool TryGet(string key, out string? value)
}
value = GetRandomValue(key[_prefix.Length..]);
- _logger.LogDebug("Generated random value {Value} for '{Key}'", value, key);
+ LogRandomValueGenerated(value, key);
return true;
}
@@ -199,4 +199,7 @@ private string GetRandomBytes()
Random.Shared.NextBytes(bytes);
return Convert.ToHexString(bytes);
}
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Generated random value '{Value}' for '{Key}'.")]
+ private partial void LogRandomValueGenerated(string? value, string key);
}
diff --git a/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj b/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj
index 842b7e774d..1873d57c88 100644
--- a/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj
+++ b/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for generating random values.
configuration;ConfigurationProvider;random
true
diff --git a/src/Configuration/src/SpringBoot/ConfigurationSchema.json b/src/Configuration/src/SpringBoot/ConfigurationSchema.json
new file mode 100644
index 0000000000..5082a91d02
--- /dev/null
+++ b/src/Configuration/src/SpringBoot/ConfigurationSchema.json
@@ -0,0 +1,17 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Configuration.SpringBoot": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration/src/SpringBoot/Properties/AssemblyInfo.cs b/src/Configuration/src/SpringBoot/Properties/AssemblyInfo.cs
index 6f92dcdbb9..ac320c12db 100644
--- a/src/Configuration/src/SpringBoot/Properties/AssemblyInfo.cs
+++ b/src/Configuration/src/SpringBoot/Properties/AssemblyInfo.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
+using Aspire;
+
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.SpringBoot")]
[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Configuration.SpringBoot.Test")]
diff --git a/src/Configuration/src/SpringBoot/SpringBootEnvironmentVariableProvider.cs b/src/Configuration/src/SpringBoot/SpringBootEnvironmentVariableProvider.cs
index 825cab291d..ed2685a5f7 100644
--- a/src/Configuration/src/SpringBoot/SpringBootEnvironmentVariableProvider.cs
+++ b/src/Configuration/src/SpringBoot/SpringBootEnvironmentVariableProvider.cs
@@ -61,11 +61,8 @@ public override void Load()
{
string? value = Data[key];
- if (value != null)
- {
- string newKey = key.Contains('.') ? key.Replace('.', ':') : key;
- data[newKey] = value;
- }
+ string newKey = key.Contains('.') ? key.Replace('.', ':') : key;
+ data[newKey] = value;
}
}
diff --git a/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj b/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj
index a8d87f95fb..5606d85532 100644
--- a/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj
+++ b/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0;net8.0
Configuration provider for reading Spring Boot-style configuration.
configuration;ConfigurationProvider;Spring;Boot
true
diff --git a/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs b/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs
index fdf8871894..c4527bcb23 100644
--- a/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs
@@ -63,4 +63,18 @@ public void HostApplicationAddCloudFoundryConfiguration_Adds()
configurationRoot.EnumerateProviders().Should().ContainSingle();
}
+
+ [Fact]
+ public void Does_not_register_multiple_times()
+ {
+ WebApplicationBuilder hostBuilder = TestWebApplicationBuilderFactory.Create();
+ hostBuilder.AddCloudFoundryConfiguration();
+ int beforeSourceCount = hostBuilder.Configuration.EnumerateSources().Count();
+ int beforeServiceCount = hostBuilder.Services.Count;
+
+ hostBuilder.AddCloudFoundryConfiguration();
+
+ hostBuilder.Configuration.EnumerateSources().Count().Should().Be(beforeSourceCount);
+ hostBuilder.Services.Count.Should().Be(beforeServiceCount);
+ }
}
diff --git a/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs b/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs
index 05bbbfd55f..d461d659a7 100644
--- a/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/CloudfoundryConfigurationProviderTest.cs
@@ -12,7 +12,13 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Steeltoe.Common.TestResources;
-using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
+using IPNetworkAlias =
+#if NET10_0_OR_GREATER
+ System.Net.IPNetwork
+#else
+ Microsoft.AspNetCore.HttpOverrides.IPNetwork
+#endif
+ ;
namespace Steeltoe.Configuration.CloudFoundry.Test;
@@ -192,8 +198,8 @@ public void Load_VCAP_APPLICATION_Allows_Reload_Without_Throwing_Exception()
{
const string environment = """
{
- "name": "my-app",
- "version": "fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca"
+ "application_version": "fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca",
+ "space_name": "test-space"
}
""";
@@ -204,7 +210,7 @@ public void Load_VCAP_APPLICATION_Allows_Reload_Without_Throwing_Exception()
IConfigurationRoot configurationRoot = configurationBuilder.Build();
- VcapApp? options = null;
+ CloudFoundryApplicationOptions? options = null;
using var tokenSource = new CancellationTokenSource(250.Milliseconds());
@@ -219,12 +225,12 @@ public void Load_VCAP_APPLICATION_Allows_Reload_Without_Throwing_Exception()
while (!tokenSource.IsCancellationRequested)
{
- options = configurationRoot.GetSection("vcap:application").Get();
+ options = configurationRoot.GetSection("vcap:application").Get();
}
options.Should().NotBeNull();
- options.Name.Should().Be("my-app");
- options.Version.Should().Be("fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca");
+ options.ApplicationVersion.Should().Be("fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca");
+ options.SpaceName.Should().Be("test-space");
}
[Theory]
@@ -244,14 +250,22 @@ public async Task ForwardedHeadersOptions_unrestricted_when_running_on_CloudFoun
{
options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedFor);
options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedProto);
+#if NET10_0_OR_GREATER
+ options.KnownIPNetworks.Should().BeEmpty();
+#else
options.KnownNetworks.Should().BeEmpty();
+#endif
options.KnownProxies.Should().BeEmpty();
}
else
{
options.ForwardedHeaders.Should().NotHaveFlag(ForwardedHeaders.XForwardedFor);
options.ForwardedHeaders.Should().NotHaveFlag(ForwardedHeaders.XForwardedProto);
- options.KnownNetworks.Should().ContainSingle().Which.Should().BeEquivalentTo(IPNetwork.Parse("127.0.0.1/8"));
+#if NET10_0_OR_GREATER
+ options.KnownIPNetworks.Should().ContainSingle().Which.Should().BeEquivalentTo(IPNetworkAlias.Parse("127.0.0.1/8"));
+#else
+ options.KnownNetworks.Should().ContainSingle().Which.Should().BeEquivalentTo(IPNetworkAlias.Parse("127.0.0.1/8"));
+#endif
options.KnownProxies.Should().ContainSingle().Which.Should().Be(IPAddress.Parse("::1"));
}
}
@@ -467,14 +481,4 @@ public void Loads_VCAP_SERVICES_from_stream()
provider.TryGet("p-mysql:1:credentials:uri", out value).Should().BeTrue();
value.Should().Be("mysql://gxXQb2pMbzFsZQW8:lvMkGf6oJQvKSOwn@192.168.0.97:3306/cf_b2d83697_5fa1_4a51_991b_975c9d7e5515?reconnect=true");
}
-
- private sealed class VcapApp
- {
-#pragma warning disable S3459 // Unassigned members should be removed
-#pragma warning disable S1144 // Unused private types or members should be removed
- public string? Name { get; set; }
- public string? Version { get; set; }
-#pragma warning restore S1144 // Unused private types or members should be removed
-#pragma warning restore S3459 // Unassigned members should be removed
- }
}
diff --git a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/BasePostProcessorsTest.cs b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/BasePostProcessorsTest.cs
index 92486324f0..5677383aef 100644
--- a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/BasePostProcessorsTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/BasePostProcessorsTest.cs
@@ -57,16 +57,6 @@ internal PostProcessorConfigurationProvider GetConfigurationProvider(IConfigurat
return new TestPostProcessorConfigurationProvider(source);
}
- protected string? GetFileContentAtKey(Dictionary configurationData, string key)
- {
- if (configurationData.TryGetValue(key, out string? value) && value != null)
- {
- return File.ReadAllText(value);
- }
-
- return null;
- }
-
private sealed class TestPostProcessorConfigurationProvider(PostProcessorConfigurationSource source)
: PostProcessorConfigurationProvider(source);
diff --git a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/CloudFoundryServiceBindingConfigurationProviderTest.cs b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/CloudFoundryServiceBindingConfigurationProviderTest.cs
index 91aa24f7fd..ce973ec42f 100644
--- a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/CloudFoundryServiceBindingConfigurationProviderTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/CloudFoundryServiceBindingConfigurationProviderTest.cs
@@ -118,7 +118,7 @@ public void PostProcessors_OnByDefault()
var postProcessor = new TestPostProcessor();
var reader = new StringServiceBindingsReader(VcapServicesJson);
- var source = new CloudFoundryServiceBindingConfigurationSource(reader);
+ var source = new CloudFoundryServiceBindingConfigurationSource(reader, CloudFoundryServiceBrokerTypes.All);
source.RegisterPostProcessor(postProcessor);
var builder = new ConfigurationBuilder();
@@ -137,7 +137,7 @@ public void Build_CapturesParentConfiguration()
};
var reader = new StringServiceBindingsReader(string.Empty);
- var source = new CloudFoundryServiceBindingConfigurationSource(reader);
+ var source = new CloudFoundryServiceBindingConfigurationSource(reader, CloudFoundryServiceBrokerTypes.All);
var builder = new ConfigurationBuilder();
builder.Add(source);
@@ -153,7 +153,7 @@ public void Build_CapturesParentConfiguration()
public void Build_LoadsServiceBindings()
{
var reader = new StringServiceBindingsReader(VcapServicesJson);
- var source = new CloudFoundryServiceBindingConfigurationSource(reader);
+ var source = new CloudFoundryServiceBindingConfigurationSource(reader, CloudFoundryServiceBrokerTypes.All);
var builder = new ConfigurationBuilder();
builder.Add(source);
diff --git a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/ConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/ConfigurationBuilderExtensionsTest.cs
index 0511f2e41b..672b54db16 100644
--- a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/ConfigurationBuilderExtensionsTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/ConfigurationBuilderExtensionsTest.cs
@@ -57,6 +57,35 @@ public void AddCloudFoundryServiceBindings_RegistersProcessors()
source.PostProcessors.Should().NotBeEmpty();
}
+ [Fact]
+ public void AddCloudFoundryServiceBindings_RegistersSubsetOfProcessors()
+ {
+ var builder = new ConfigurationBuilder();
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.PostgreSql | CloudFoundryServiceBrokerTypes.MySql);
+
+ builder.Sources.Should().ContainSingle();
+ CloudFoundryServiceBindingConfigurationSource source = builder.Sources[0].Should().BeOfType().Subject;
+ source.PostProcessors.Should().HaveCount(2);
+ }
+
+ [Fact]
+ public void AddCloudFoundryServiceBindings_DoesNotAddMultipleSourcesForSamePostProcessor()
+ {
+ var builder = new ConfigurationBuilder();
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.None);
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.PostgreSql | CloudFoundryServiceBrokerTypes.MySql);
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.PostgreSql | CloudFoundryServiceBrokerTypes.SqlServer);
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.MySql | CloudFoundryServiceBrokerTypes.RabbitMQ);
+ builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.SqlServer | CloudFoundryServiceBrokerTypes.RabbitMQ);
+
+ CloudFoundryServiceBindingConfigurationSource[] sources = [.. builder.Sources.OfType()];
+
+ sources.Should().HaveCount(3);
+ sources[0].BrokerTypes.Should().Be(CloudFoundryServiceBrokerTypes.PostgreSql | CloudFoundryServiceBrokerTypes.MySql);
+ sources[1].BrokerTypes.Should().Be(CloudFoundryServiceBrokerTypes.SqlServer);
+ sources[2].BrokerTypes.Should().Be(CloudFoundryServiceBrokerTypes.RabbitMQ);
+ }
+
[Fact]
public void AddCloudFoundryServiceBindings_EnvironmentVariableSet_LoadsServiceBindings()
{
diff --git a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/PostProcessorsTest.cs b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/PostProcessorsTest.cs
index 7ae4df1f94..c0efd7e94d 100644
--- a/src/Configuration/test/CloudFoundry.Test/ServiceBindings/PostProcessorsTest.cs
+++ b/src/Configuration/test/CloudFoundry.Test/ServiceBindings/PostProcessorsTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging.Abstractions;
using Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors;
@@ -12,65 +13,111 @@ public sealed class PostProcessorsTest : BasePostProcessorsTest
[Fact]
public void Processes_MySql_configuration()
{
- var postProcessor = new MySqlCloudFoundryPostProcessor();
+ List tempPaths = [];
- Tuple[] secrets =
- [
- Tuple.Create("credentials:hostname", "test-host"),
- Tuple.Create("credentials:port", "test-port"),
- Tuple.Create("credentials:name", "test-database"),
- Tuple.Create("credentials:username", "test-username"),
- Tuple.Create("credentials:password", "test-password")
- ];
-
- Dictionary configurationData =
- GetConfigurationData(TestProviderName, TestBindingName, [MySqlCloudFoundryPostProcessor.BindingType], null, secrets);
-
- PostProcessorConfigurationProvider provider = GetConfigurationProvider(postProcessor);
-
- postProcessor.PostProcessConfiguration(provider, configurationData);
+ using (var postProcessor = new MySqlCloudFoundryPostProcessor())
+ {
+ Tuple[] secrets =
+ [
+ Tuple.Create("credentials:hostname", "test-host"),
+ Tuple.Create("credentials:port", "test-port"),
+ Tuple.Create("credentials:name", "test-database"),
+ Tuple.Create("credentials:username", "test-username"),
+ Tuple.Create("credentials:password", "test-password"),
+ Tuple.Create("credentials:sslCert", "test-ssl-cert"),
+ Tuple.Create("credentials:sslKey", "test-ssl-key"),
+ Tuple.Create("credentials:sslrootcert", "test-ssl-root-cert") // tests case-insensitivity
+ ];
+
+ Dictionary configurationData =
+ GetConfigurationData(TestProviderName, TestBindingName, [MySqlCloudFoundryPostProcessor.BindingType], null, secrets);
+
+ PostProcessorConfigurationProvider provider = GetConfigurationProvider(postProcessor);
+
+ postProcessor.PostProcessConfiguration(provider, configurationData);
+
+ string keyPrefix = GetOutputKeyPrefix(TestBindingName, MySqlCloudFoundryPostProcessor.BindingType);
+ configurationData.Should().ContainKey($"{keyPrefix}:host").WhoseValue.Should().Be("test-host");
+ configurationData.Should().ContainKey($"{keyPrefix}:port").WhoseValue.Should().Be("test-port");
+ configurationData.Should().ContainKey($"{keyPrefix}:database").WhoseValue.Should().Be("test-database");
+ configurationData.Should().ContainKey($"{keyPrefix}:username").WhoseValue.Should().Be("test-username");
+ configurationData.Should().ContainKey($"{keyPrefix}:password").WhoseValue.Should().Be("test-password");
+
+ foreach ((string key, string expectedValue) in new Dictionary
+ {
+ ["ssl-cert"] = "test-ssl-cert",
+ ["ssl-key"] = "test-ssl-key",
+ ["ssl-ca"] = "test-ssl-root-cert"
+ })
+ {
+ string? tempPath = configurationData.Should().ContainKey($"{keyPrefix}:{key}").WhoseValue;
+
+ AssertFileHasContent(tempPath, expectedValue);
+ AssertUnixFileModeIsUserOnly(tempPath);
+
+ tempPaths.Add(tempPath);
+ }
+ }
- string keyPrefix = GetOutputKeyPrefix(TestBindingName, MySqlCloudFoundryPostProcessor.BindingType);
- configurationData.Should().ContainKey($"{keyPrefix}:host").WhoseValue.Should().Be("test-host");
- configurationData.Should().ContainKey($"{keyPrefix}:port").WhoseValue.Should().Be("test-port");
- configurationData.Should().ContainKey($"{keyPrefix}:database").WhoseValue.Should().Be("test-database");
- configurationData.Should().ContainKey($"{keyPrefix}:username").WhoseValue.Should().Be("test-username");
- configurationData.Should().ContainKey($"{keyPrefix}:password").WhoseValue.Should().Be("test-password");
+ foreach (string tempPath in tempPaths)
+ {
+ File.Exists(tempPath).Should().BeFalse();
+ }
}
[Fact]
public void Processes_PostgreSql_configuration()
{
- var postProcessor = new PostgreSqlCloudFoundryPostProcessor();
-
- Tuple[] secrets =
- [
- Tuple.Create("credentials:hostname", "test-host"),
- Tuple.Create("credentials:port", "test-port"),
- Tuple.Create("credentials:name", "test-database"),
- Tuple.Create("credentials:username", "test-username"),
- Tuple.Create("credentials:password", "test-password"),
- Tuple.Create("credentials:sslCert", "test-ssl-cert"),
- Tuple.Create("credentials:sslKey", "test-ssl-key"),
- Tuple.Create("credentials:sslrootcert", "test-ssl-root-cert") // tests case-insensitivity
- ];
-
- Dictionary configurationData =
- GetConfigurationData(TestProviderName, TestBindingName, [PostgreSqlCloudFoundryPostProcessor.BindingType], null, secrets);
-
- PostProcessorConfigurationProvider provider = GetConfigurationProvider(postProcessor);
+ List tempPaths = [];
- postProcessor.PostProcessConfiguration(provider, configurationData);
+ using (var postProcessor = new PostgreSqlCloudFoundryPostProcessor())
+ {
+ Tuple[] secrets =
+ [
+ Tuple.Create("credentials:hostname", "test-host"),
+ Tuple.Create("credentials:port", "test-port"),
+ Tuple.Create("credentials:name", "test-database"),
+ Tuple.Create("credentials:username", "test-username"),
+ Tuple.Create("credentials:password", "test-password"),
+ Tuple.Create("credentials:sslCert", "test-ssl-cert"),
+ Tuple.Create("credentials:sslKey", "test-ssl-key"),
+ Tuple.Create("credentials:sslrootcert", "test-ssl-root-cert") // tests case-insensitivity
+ ];
+
+ Dictionary configurationData =
+ GetConfigurationData(TestProviderName, TestBindingName, [PostgreSqlCloudFoundryPostProcessor.BindingType], null, secrets);
+
+ PostProcessorConfigurationProvider provider = GetConfigurationProvider(postProcessor);
+
+ postProcessor.PostProcessConfiguration(provider, configurationData);
+
+ string keyPrefix = GetOutputKeyPrefix(TestBindingName, PostgreSqlCloudFoundryPostProcessor.BindingType);
+ configurationData.Should().ContainKey($"{keyPrefix}:host").WhoseValue.Should().Be("test-host");
+ configurationData.Should().ContainKey($"{keyPrefix}:port").WhoseValue.Should().Be("test-port");
+ configurationData.Should().ContainKey($"{keyPrefix}:database").WhoseValue.Should().Be("test-database");
+ configurationData.Should().ContainKey($"{keyPrefix}:username").WhoseValue.Should().Be("test-username");
+ configurationData.Should().ContainKey($"{keyPrefix}:password").WhoseValue.Should().Be("test-password");
+
+ foreach ((string key, string expectedValue) in new Dictionary
+ {
+ ["SSL Certificate"] = "test-ssl-cert",
+ ["SSL Key"] = "test-ssl-key",
+ ["Root Certificate"] = "test-ssl-root-cert"
+ })
+ {
+ string? tempPath = configurationData.Should().ContainKey($"{keyPrefix}:{key}").WhoseValue;
+
+ AssertFileHasContent(tempPath, expectedValue);
+ AssertUnixFileModeIsUserOnly(tempPath);
+
+ tempPaths.Add(tempPath);
+ }
+ }
- string keyPrefix = GetOutputKeyPrefix(TestBindingName, PostgreSqlCloudFoundryPostProcessor.BindingType);
- configurationData.Should().ContainKey($"{keyPrefix}:host").WhoseValue.Should().Be("test-host");
- configurationData.Should().ContainKey($"{keyPrefix}:port").WhoseValue.Should().Be("test-port");
- configurationData.Should().ContainKey($"{keyPrefix}:database").WhoseValue.Should().Be("test-database");
- configurationData.Should().ContainKey($"{keyPrefix}:username").WhoseValue.Should().Be("test-username");
- configurationData.Should().ContainKey($"{keyPrefix}:password").WhoseValue.Should().Be("test-password");
- GetFileContentAtKey(configurationData, $"{keyPrefix}:SSL Certificate").Should().Be("test-ssl-cert");
- GetFileContentAtKey(configurationData, $"{keyPrefix}:SSL Key").Should().Be("test-ssl-key");
- GetFileContentAtKey(configurationData, $"{keyPrefix}:Root Certificate").Should().Be("test-ssl-root-cert");
+ foreach (string tempPath in tempPaths)
+ {
+ File.Exists(tempPath).Should().BeFalse();
+ }
}
[Fact]
@@ -288,4 +335,22 @@ public void Processes_Identity_configuration()
configurationData.Should().ContainKey($"{keyPrefix}:ClientSecret").WhoseValue.Should().Be("test-secret");
}
}
+
+ private static void AssertFileHasContent([NotNull] string? path, string expectedValue)
+ {
+ path.Should().NotBeNull();
+ File.Exists(path).Should().BeTrue();
+
+ string fileContent = File.ReadAllText(path);
+ fileContent.Should().Be(expectedValue);
+ }
+
+ private static void AssertUnixFileModeIsUserOnly(string path)
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ UnixFileMode fileMode = File.GetUnixFileMode(path);
+ fileMode.Should().Be(UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+ }
}
diff --git a/src/Configuration/test/CloudFoundry.Test/Steeltoe.Configuration.CloudFoundry.Test.csproj b/src/Configuration/test/CloudFoundry.Test/Steeltoe.Configuration.CloudFoundry.Test.csproj
index 5e3cb6423f..095019fc55 100644
--- a/src/Configuration/test/CloudFoundry.Test/Steeltoe.Configuration.CloudFoundry.Test.csproj
+++ b/src/Configuration/test/CloudFoundry.Test/Steeltoe.Configuration.CloudFoundry.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs
new file mode 100644
index 0000000000..5ca2eab3c0
--- /dev/null
+++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs
@@ -0,0 +1,430 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using RichardSzalay.MockHttp;
+using Steeltoe.Common.TestResources;
+
+namespace Steeltoe.Configuration.ConfigServer.Discovery.Test;
+
+public sealed class ConfigServerClientOptionsTest
+{
+ [Fact]
+ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in_IConfiguration()
+ {
+ const string configServerResponseJson = """
+ {
+ "name": "example-app-name",
+ "profiles": [
+ "example-profile"
+ ],
+ "label": "example-label",
+ "version": "1",
+ "propertySources": [
+ {
+ "name": "example-source",
+ "source": {
+ "example-server-key": "example-server-value"
+ }
+ }
+ ]
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "discovery": {
+ "enabled": true
+ },
+ "uri": "http://overridden-by-discovery",
+ "name": "example-app-name",
+ "env": "example-profile",
+ "timeout": 30000,
+ "label": "example-label"
+ }
+ }
+ },
+ "discovery": {
+ "services": [
+ {
+ "serviceId": "configserver",
+ "host": "discovered-server.com",
+ "port": 9999,
+ "isSecure": true,
+ "metadata": {
+ "user": "example-user",
+ "password": "example-password",
+ "configPath": "internal"
+ }
+ }
+ ]
+ },
+ "eureka": {
+ "client": {
+ "enabled": false
+ }
+ },
+ "consul": {
+ "discovery": {
+ "enabled": false
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label")
+ .Respond("application/json", configServerResponseJson);
+
+ Action configureOptions = options => options.ValidateCertificates = false;
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ // ReSharper disable once AccessToDisposedClosure
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ handler.Mock.VerifyNoOutstandingExpectation();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ IServiceCollection services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.ConfigureConfigServerClientOptions(configureOptions);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+
+ provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery");
+ provider.ClientOptions.Username.Should().BeNull();
+ provider.ClientOptions.Password.Should().BeNull();
+ provider.ClientOptions.Name.Should().Be("example-app-name");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Timeout.Should().Be(30_000);
+ provider.ClientOptions.Label.Should().Be("example-label");
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal");
+ optionsMonitor.CurrentValue.Username.Should().Be("example-user");
+ optionsMonitor.CurrentValue.Password.Should().Be("example-password");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "discovery": {
+ "enabled": true
+ },
+ "uri": "http://overridden-by-discovery",
+ "name": "alternate-name-1",
+ "env": "example-profile",
+ "timeout": 15000,
+ "label": "example-label"
+ }
+ }
+ },
+ "discovery": {
+ "services": [
+ {
+ "serviceId": "configserver",
+ "host": "ignored-other-discovered-server.com",
+ "port": 3333,
+ "isSecure": true,
+ "metadata": {
+ "user": "ignored-other-example-user",
+ "password": "ignored-other-example-password",
+ "configPath": "ignored-other-internal"
+ }
+ }
+ ]
+ },
+ "eureka": {
+ "client": {
+ "enabled": false
+ }
+ },
+ "consul": {
+ "discovery": {
+ "enabled": false
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery");
+ provider.ClientOptions.Username.Should().BeNull();
+ provider.ClientOptions.Password.Should().BeNull();
+ provider.ClientOptions.Name.Should().Be("alternate-name-1");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Timeout.Should().Be(15_000);
+ provider.ClientOptions.Label.Should().Be("example-label");
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ // Discovery changes don't propagate until the provider reloads.
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal");
+ optionsMonitor.CurrentValue.Username.Should().Be("example-user");
+ optionsMonitor.CurrentValue.Password.Should().Be("example-password");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "discovery": {
+ "enabled": false
+ },
+ "uri": "https://explicit-server:7777",
+ "name": "alternate-name-2",
+ "env": "example-profile",
+ "timeout": 10000
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ provider.ClientOptions.Uri.Should().Be("https://explicit-server:7777");
+ provider.ClientOptions.Name.Should().Be("alternate-name-2");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Timeout.Should().Be(10_000);
+ provider.ClientOptions.Label.Should().BeNull();
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ // Discovery changes don't propagate until the provider reloads.
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+ }
+
+ [Fact]
+ public void Updates_discovered_Config_Server_URI_on_provider_reload()
+ {
+ const string configServerResponseJson = """
+ {
+ "name": "example-app-name",
+ "profiles": [
+ "example-profile"
+ ],
+ "label": "example-label",
+ "version": "1",
+ "propertySources": [
+ {
+ "name": "example-source",
+ "source": {
+ "example-server-key": "example-server-value"
+ }
+ }
+ ]
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "discovery": {
+ "enabled": true
+ },
+ "uri": "http://overridden-by-discovery",
+ "name": "example-app-name",
+ "env": "example-profile",
+ "label": "example-label"
+ }
+ }
+ },
+ "discovery": {
+ "services": [
+ {
+ "serviceId": "configserver",
+ "host": "discovered-server.com",
+ "port": 9999,
+ "isSecure": true,
+ "metadata": {
+ "user": "example-user",
+ "password": "example-password",
+ "configPath": "internal"
+ }
+ }
+ ]
+ },
+ "eureka": {
+ "client": {
+ "enabled": false
+ }
+ },
+ "consul": {
+ "discovery": {
+ "enabled": false
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label")
+ .Respond("application/json", configServerResponseJson);
+
+ Action configureOptions = options => options.ValidateCertificates = false;
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ // ReSharper disable once AccessToDisposedClosure
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ handler.Mock.VerifyNoOutstandingExpectation();
+ handler.Mock.Clear();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ IServiceCollection services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.ConfigureConfigServerClientOptions(configureOptions);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+
+ provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery");
+ provider.ClientOptions.Username.Should().BeNull();
+ provider.ClientOptions.Password.Should().BeNull();
+ provider.ClientOptions.Name.Should().Be("example-app-name");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Label.Should().Be("example-label");
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal");
+ optionsMonitor.CurrentValue.Username.Should().Be("example-user");
+ optionsMonitor.CurrentValue.Password.Should().Be("example-password");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "discovery": {
+ "enabled": true
+ },
+ "uri": "http://overridden-again-by-discovery",
+ "name": "alternate-name",
+ "env": "alternate-profile",
+ "label": "alternate-label"
+ }
+ }
+ },
+ "discovery": {
+ "services": [
+ {
+ "serviceId": "configserver",
+ "host": "alternate-discovered-server.com",
+ "port": 7777,
+ "isSecure": true,
+ "metadata": {
+ "user": "alternate-user",
+ "password": "alternate-password",
+ "configPath": "internal"
+ }
+ }
+ ]
+ },
+ "eureka": {
+ "client": {
+ "enabled": false
+ }
+ },
+ "consul": {
+ "discovery": {
+ "enabled": false
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery");
+ provider.ClientOptions.Username.Should().BeNull();
+ provider.ClientOptions.Password.Should().BeNull();
+ provider.ClientOptions.Name.Should().Be("alternate-name");
+ provider.ClientOptions.Environment.Should().Be("alternate-profile");
+ provider.ClientOptions.Label.Should().Be("alternate-label");
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal");
+ optionsMonitor.CurrentValue.Username.Should().Be("example-user");
+ optionsMonitor.CurrentValue.Password.Should().Be("example-password");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+
+ handler.Mock.Expect(HttpMethod.Get, "https://alternate-discovered-server.com:7777/internal/alternate-name/alternate-profile/alternate-label")
+ .Respond("application/json", configServerResponseJson);
+
+ provider.Load();
+ handler.Mock.VerifyNoOutstandingExpectation();
+
+ provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery");
+ provider.ClientOptions.Username.Should().BeNull();
+ provider.ClientOptions.Password.Should().BeNull();
+ provider.ClientOptions.Name.Should().Be("alternate-name");
+ provider.ClientOptions.Environment.Should().Be("alternate-profile");
+ provider.ClientOptions.Label.Should().Be("alternate-label");
+ provider.ClientOptions.ValidateCertificates.Should().BeFalse();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be("https://alternate-discovered-server.com:7777/internal");
+ optionsMonitor.CurrentValue.Username.Should().Be("alternate-user");
+ optionsMonitor.CurrentValue.Password.Should().Be("alternate-password");
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+ }
+}
diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs
index 73770ae0a3..556dfc475e 100644
--- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs
+++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs
@@ -15,12 +15,12 @@ namespace Steeltoe.Configuration.ConfigServer.Discovery.Test;
public sealed class ConfigServerDiscoveryServiceTest
{
[Fact]
- public void ConfigServerDiscoveryService_FindsDiscoveryClients()
+ public async Task ConfigServerDiscoveryService_FindsDiscoveryClients()
{
IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build();
- var options = new ConfigServerClientOptions();
- var service = new ConfigServerDiscoveryService(configuration, options, NullLoggerFactory.Instance);
+ var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance);
+ await service.GetConfigServerInstancesAsync(new ConfigServerClientOptions(), TestContext.Current.CancellationToken);
service.DiscoveryClients.Should().HaveCount(3);
service.DiscoveryClients.OfType().Should().ContainSingle();
@@ -41,8 +41,8 @@ public async Task InvokeGetInstances_ReturnsExpected()
IConfigurationRoot configurationRoot = builder.Build();
var options = new ConfigServerClientOptions();
- var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance);
- IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken);
+ var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance);
+ IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken);
result.Should().BeEmpty();
}
@@ -66,8 +66,8 @@ public async Task InvokeGetInstances_RetryEnabled_ReturnsExpected()
Timeout = 10
};
- var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance);
- IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken);
+ var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance);
+ IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken);
result.Should().BeEmpty();
}
@@ -92,8 +92,8 @@ public async Task GetConfigServerInstances_ReturnsExpected()
Timeout = 10
};
- var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance);
- IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken);
+ var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance);
+ IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken);
result.Should().BeEmpty();
}
@@ -103,7 +103,7 @@ public async Task RuntimeReplacementsCanBeProvided()
IConfigurationRoot configurationRoot = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build();
var testDiscoveryClient = new TestDiscoveryClient();
- var service = new ConfigServerDiscoveryService(configurationRoot, new ConfigServerClientOptions(), NullLoggerFactory.Instance);
+ var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance);
await service.ProvideRuntimeReplacementsAsync([testDiscoveryClient], TestContext.Current.CancellationToken);
@@ -114,6 +114,10 @@ private sealed class TestDiscoveryClient : IDiscoveryClient
{
public string Description => throw new NotImplementedException();
+#pragma warning disable CS0067 // The event is never used
+ public event EventHandler? InstancesFetched;
+#pragma warning restore CS0067 // The event is never used
+
public Task> GetServiceIdsAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/Steeltoe.Configuration.ConfigServer.Discovery.Test.csproj b/src/Configuration/test/ConfigServer.Discovery.Test/Steeltoe.Configuration.ConfigServer.Discovery.Test.csproj
index 466f805b9b..3d62140336 100644
--- a/src/Configuration/test/ConfigServer.Discovery.Test/Steeltoe.Configuration.ConfigServer.Discovery.Test.csproj
+++ b/src/Configuration/test/ConfigServer.Discovery.Test/Steeltoe.Configuration.ConfigServer.Discovery.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs
index f06a592b64..fee665ea3d 100644
--- a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs
+++ b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs
@@ -6,18 +6,14 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Steeltoe.Common.TestResources;
-using Steeltoe.Common.TestResources.IO;
namespace Steeltoe.Configuration.ConfigServer.Integration.Test;
-// NOTE: Some of the tests assume a running Spring Cloud Config Server is started
+// NOTE: These tests assume Spring Cloud Config Server (and sometimes Eureka Server) is running
// with repository data for application: foo, profile: development
//
-// The easiest way to get that to happen is clone the spring-cloud-config
-// repo and run the config-server.
-// e.g. git clone https://github.com/spring-cloud/spring-cloud-config.git
-// cd spring-cloud-config\spring-cloud-config-server
-// mvn spring-boot:run
+// The easiest way to run the servers is with docker compose
+// (see docker-compose.yml at the repo root)
public sealed class ConfigServerConfigurationExtensionsIntegrationTest
{
[Fact]
@@ -41,15 +37,11 @@ public void SpringCloudConfigServer_ReturnsExpectedDefaultData()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
- var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
-
- configurationBuilder.AddJsonFile(fileName);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot root = configurationBuilder.Build();
@@ -85,21 +77,13 @@ public async Task SpringCloudConfigServer_ReturnsExpectedDefaultData_AsInjectedO
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
WebHostBuilder builder = TestWebHostBuilderFactory.Create();
builder.UseEnvironment("development");
builder.UseStartup();
-
- builder.ConfigureAppConfiguration(configurationBuilder =>
- {
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
- });
-
+ builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider));
builder.AddConfigServer();
using IWebHost host = builder.Build();
@@ -176,21 +160,13 @@ public async Task SpringCloudConfigServer_ConfiguredViaCloudfoundryEnv_ReturnsEx
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
WebHostBuilder builder = TestWebHostBuilderFactory.Create();
builder.UseEnvironment("development");
builder.UseStartup();
-
- builder.ConfigureAppConfiguration(configurationBuilder =>
- {
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
- });
-
+ builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider));
builder.AddConfigServer();
using IWebHost host = builder.Build();
@@ -231,15 +207,12 @@ public void SpringCloudConfigServer_DiscoveryFirst_ReturnsExpectedDefaultData()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.Add(FastTestConfigurations.Discovery);
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot root = configurationBuilder.Build();
@@ -275,20 +248,12 @@ public async Task SpringCloudConfigServer_WithHealthEnabled_ReturnsHealth()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
WebHostBuilder builder = TestWebHostBuilderFactory.Create();
builder.UseStartup();
-
- builder.ConfigureAppConfiguration(configurationBuilder =>
- {
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
- });
-
+ builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider));
builder.AddConfigServer();
using IWebHost host = builder.Build();
diff --git a/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs b/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs
index 35047a8abd..7d58f613a7 100644
--- a/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs
+++ b/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs
@@ -26,7 +26,7 @@ public void PlaceholderInsideDecryptionProvider_ReturnsDecryptedValuesInPlacehol
["encrypt:rsa:algorithm"] = "OAEP",
["encrypt:rsa:salt"] = "deadbeef",
["encrypted"] =
- "{cipher}AQATBPXCmri0MCEoCam0noXJgKGlFfE/chVN7XhH1V23MqJ8sI3lI61PyvsryJP3LlfNn38gUuulMeslAs/gUCoPFPV/zD7M8x527wQUbmWD6bR0ZMJ4hu3DisK6Diw2YAOxXSsm3Zh46cPFQcowfOG1x2OXj+5uL4T+VBGdt3Nr6dHCOumkTJ1KAtaJMfASf3J8G4M27v6m4Y2EdBqP1zWwDhAZ3R0u9uTP9xYUqQiKsUeOixrhOaCvtb1Q+Zg6A41CxM4cjL3Ty6miNYLx3QkxRvfkdo0iqo7jTrWWAT1aeRV6t5U5iMlWnD4eXzad60E3ZSINhvDiB03xPPPuHKC6qUTRJEEbQFegmn/KIPMMn9WaH/JLLZNvQYMuaFszZ84AE3aQcH0be+sNFDSjHNHL",
+ "{cipher}AQBoKgZNlxY+EWcGG0CXhyfV4q/u7lJjCBS+9liSKpu/w4gNmJhTYvjDJ3XIExVSVit41po5n91LVI3h777QlY7b0D2zOI0f4YR/9MtAdsq/cgRGZ4uzcv69bmVnQ0yt5ilxV021TH0EsVEmwmgyY+n1mKcD7aXWQwS2lAvJycgVgrDfbj2qz2c7aPn+8mXvG8EAbNmEhCbATCdPDlmBUPjLvuSweDlzlefQJ+jVSxLHfOcQ+g17arhIH1j0nZEAGywoNBGS1xg6DQ+8sW0GiYennTrnKslzMFjPTQ8QJSONzYysdRLGbV2Bi73ifUd+4AnMuSKcIRNiRACRtt+i7ZhrgTWRV1F+F8vfIiqf3SfzHdyclHkoCVkPhNBc9ySq0XRubPtg7UnW2KPZufZ0D7xx",
["placeholder"] = "${encrypted}"
};
@@ -59,7 +59,7 @@ public void DecryptionInsidePlaceholderProvider_ReturnsDecryptedValuesInPlacehol
["encrypt:rsa:algorithm"] = "OAEP",
["encrypt:rsa:salt"] = "deadbeef",
["encrypted"] =
- "{cipher}AQATBPXCmri0MCEoCam0noXJgKGlFfE/chVN7XhH1V23MqJ8sI3lI61PyvsryJP3LlfNn38gUuulMeslAs/gUCoPFPV/zD7M8x527wQUbmWD6bR0ZMJ4hu3DisK6Diw2YAOxXSsm3Zh46cPFQcowfOG1x2OXj+5uL4T+VBGdt3Nr6dHCOumkTJ1KAtaJMfASf3J8G4M27v6m4Y2EdBqP1zWwDhAZ3R0u9uTP9xYUqQiKsUeOixrhOaCvtb1Q+Zg6A41CxM4cjL3Ty6miNYLx3QkxRvfkdo0iqo7jTrWWAT1aeRV6t5U5iMlWnD4eXzad60E3ZSINhvDiB03xPPPuHKC6qUTRJEEbQFegmn/KIPMMn9WaH/JLLZNvQYMuaFszZ84AE3aQcH0be+sNFDSjHNHL",
+ "{cipher}AQBoKgZNlxY+EWcGG0CXhyfV4q/u7lJjCBS+9liSKpu/w4gNmJhTYvjDJ3XIExVSVit41po5n91LVI3h777QlY7b0D2zOI0f4YR/9MtAdsq/cgRGZ4uzcv69bmVnQ0yt5ilxV021TH0EsVEmwmgyY+n1mKcD7aXWQwS2lAvJycgVgrDfbj2qz2c7aPn+8mXvG8EAbNmEhCbATCdPDlmBUPjLvuSweDlzlefQJ+jVSxLHfOcQ+g17arhIH1j0nZEAGywoNBGS1xg6DQ+8sW0GiYennTrnKslzMFjPTQ8QJSONzYysdRLGbV2Bi73ifUd+4AnMuSKcIRNiRACRtt+i7ZhrgTWRV1F+F8vfIiqf3SfzHdyclHkoCVkPhNBc9ySq0XRubPtg7UnW2KPZufZ0D7xx",
["placeholder"] = "${encrypted}"
};
diff --git a/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj b/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj
index 300d587d5c..017de0c119 100644
--- a/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj
+++ b/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj
@@ -1,6 +1,6 @@
- net9.0;net8.0
+ net10.0;net9.0;net8.0
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs
index 828b6dd46f..2c648dc0b3 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs
@@ -3,11 +3,14 @@
// See the LICENSE file in the project root for more information.
using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using RichardSzalay.MockHttp;
using Steeltoe.Common.TestResources;
-using Steeltoe.Common.TestResources.IO;
+using Steeltoe.Configuration.Placeholder;
namespace Steeltoe.Configuration.ConfigServer.Test;
@@ -18,7 +21,7 @@ public void DefaultConstructor_InitializedWithDefaults()
{
var options = new ConfigServerClientOptions();
- TestHelper.VerifyDefaults(options, null);
+ TestHelper.VerifyDefaults(options, null, null);
}
[Fact]
@@ -36,7 +39,7 @@ public async Task ConfigureConfigServerClientOptions_WithDefaults()
var optionsMonitor = serviceProvider.GetRequiredService>();
string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name;
- TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName);
+ TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName, "Production");
}
[Fact]
@@ -69,17 +72,15 @@ public async Task ConfigureConfigServerClientOptions_WithValues()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
+
var builder = new ConfigurationBuilder();
- builder.SetBasePath(directory);
- builder.AddJsonFile(fileName);
+ builder.AddInMemoryAppSettingsJsonFile(fileProvider);
IConfiguration configuration = builder.Build();
services.AddSingleton(configuration);
- services.ConfigureConfigServerClientOptions();
+ services.ConfigureConfigServerClientOptions(options => options.Environment = "staging");
await using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
var service = serviceProvider.GetRequiredService>();
@@ -88,7 +89,7 @@ public async Task ConfigureConfigServerClientOptions_WithValues()
options.Enabled.Should().BeTrue();
options.FailFast.Should().BeTrue();
options.Uri.Should().Be("http://localhost:8888");
- options.Environment.Should().Be("development");
+ options.Environment.Should().Be("staging");
options.AccessTokenUri.Should().BeNull();
options.ClientId.Should().BeNull();
options.ClientSecret.Should().BeNull();
@@ -114,4 +115,339 @@ public async Task ConfigureConfigServerClientOptions_WithValues()
options.Headers.Should().ContainKey("bar").WhoseValue.Should().Be("foo");
options.Headers.Should().ContainKey("foo").WhoseValue.Should().Be("bar");
}
+
+ [Fact]
+ public void Clone_preserves_all_properties_and_produces_independent_nested_objects()
+ {
+ using var certificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key");
+ using var issuerCertificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key");
+
+ var original = new ConfigServerClientOptions
+ {
+ ClientCertificate =
+ {
+ Certificate = certificate,
+ IssuerChain =
+ {
+ issuerCertificate
+ }
+ },
+ Enabled = false,
+ FailFast = true,
+ Environment = "staging",
+ Label = "feature/x",
+ Name = "my-app",
+ Uri = "https://config.example.com:9999",
+ Username = "user",
+ Password = "pass",
+ Token = "vault-token-123",
+ Timeout = 42_000,
+ PollingInterval = TimeSpan.FromSeconds(15),
+ ValidateCertificates = false,
+ Retry =
+ {
+ Enabled = true,
+ InitialInterval = 500,
+ MaxInterval = 5000,
+ Multiplier = 2.0,
+ MaxAttempts = 10
+ },
+ Discovery =
+ {
+ Enabled = true,
+ ServiceId = "my-config-server"
+ },
+ Health =
+ {
+ Enabled = false,
+ TimeToLive = 999
+ },
+ AccessTokenUri = "https://uaa.example.com/oauth/token",
+ ClientSecret = "secret",
+ ClientId = "client-id",
+ TokenTtl = 600_000,
+ TokenRenewRate = 120_000,
+ DisableTokenRenewal = true,
+ Headers =
+ {
+ ["X-Custom"] = "value"
+ }
+ };
+
+ ConfigServerClientOptions clone = original.Clone();
+
+ clone.ClientCertificate.Should().NotBeSameAs(original.ClientCertificate);
+ clone.ClientCertificate.Certificate.Should().BeSameAs(original.ClientCertificate.Certificate);
+ clone.ClientCertificate.IssuerChain.Should().NotBeSameAs(original.ClientCertificate.IssuerChain);
+ clone.ClientCertificate.IssuerChain.Should().ContainSingle().Which.Should().BeSameAs(issuerCertificate);
+
+ original.ClientCertificate.IssuerChain.Clear();
+ clone.ClientCertificate.IssuerChain.Should().ContainSingle();
+
+ clone.Enabled.Should().Be(original.Enabled);
+ clone.FailFast.Should().Be(original.FailFast);
+ clone.Environment.Should().Be(original.Environment);
+ clone.Label.Should().Be(original.Label);
+ clone.Name.Should().Be(original.Name);
+ clone.Uri.Should().Be(original.Uri);
+ clone.Username.Should().Be(original.Username);
+ clone.Password.Should().Be(original.Password);
+ clone.Token.Should().Be(original.Token);
+ clone.Timeout.Should().Be(original.Timeout);
+ clone.PollingInterval.Should().Be(original.PollingInterval);
+ clone.ValidateCertificates.Should().Be(original.ValidateCertificates);
+
+ clone.Retry.Should().NotBeSameAs(original.Retry);
+ clone.Retry.Enabled.Should().Be(original.Retry.Enabled);
+ clone.Retry.InitialInterval.Should().Be(original.Retry.InitialInterval);
+ clone.Retry.MaxInterval.Should().Be(original.Retry.MaxInterval);
+ clone.Retry.Multiplier.Should().Be(original.Retry.Multiplier);
+ clone.Retry.MaxAttempts.Should().Be(original.Retry.MaxAttempts);
+
+ clone.Discovery.Should().NotBeSameAs(original.Discovery);
+ clone.Discovery.Enabled.Should().Be(original.Discovery.Enabled);
+ clone.Discovery.ServiceId.Should().Be(original.Discovery.ServiceId);
+
+ clone.Health.Should().NotBeSameAs(original.Health);
+ clone.Health.Enabled.Should().Be(original.Health.Enabled);
+ clone.Health.TimeToLive.Should().Be(original.Health.TimeToLive);
+
+ clone.AccessTokenUri.Should().Be(original.AccessTokenUri);
+ clone.ClientSecret.Should().Be(original.ClientSecret);
+ clone.ClientId.Should().Be(original.ClientId);
+ clone.TokenTtl.Should().Be(original.TokenTtl);
+ clone.TokenRenewRate.Should().Be(original.TokenRenewRate);
+ clone.DisableTokenRenewal.Should().Be(original.DisableTokenRenewal);
+
+ clone.Headers.Should().NotBeSameAs(original.Headers);
+ clone.Headers.Should().BeEquivalentTo(original.Headers);
+ }
+
+ [Fact]
+ public void Certificate_configuration_survives_options_reload()
+ {
+ const string configServerResponseJson = """
+ {
+ "name": "myName",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": []
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "timeout": 30000
+ }
+ }
+ },
+ "Certificates": {
+ "ConfigServer": {
+ "CertificateFilePath": "instance.crt",
+ "PrivateKeyFilePath": "instance.key"
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.Expect(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson);
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ // ReSharper disable once AccessToDisposedClosure
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ handler.Mock.VerifyNoOutstandingExpectation();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ IServiceCollection services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.ConfigureConfigServerClientOptions();
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+
+ provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull();
+ optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull();
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "timeout": 15000
+ }
+ }
+ },
+ "Certificates": {
+ "ConfigServer": {
+ "CertificateFilePath": "instance.crt",
+ "PrivateKeyFilePath": "instance.key"
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ provider.ClientOptions.Timeout.Should().Be(15_000);
+ provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull();
+ optionsMonitor.CurrentValue.Timeout.Should().Be(15_000);
+ optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void Changes_in_IConfiguration_update_provider_options_and_injected_options()
+ {
+ const string configServerResponseJson = """
+ {
+ "name": "example-app-name",
+ "profiles": [
+ "example-profile"
+ ],
+ "label": "example-label",
+ "version": "1",
+ "propertySources": [
+ {
+ "name": "example-source",
+ "source": {
+ "example-server-key": "example-server-value"
+ }
+ }
+ ]
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "custom": {
+ "profileName": "example-profile"
+ },
+ "spring": {
+ "cloud": {
+ "config": {
+ "uri": "https://config.server.com:9999",
+ "name": "example-app-name",
+ "env": "${custom:profileName}",
+ "timeout": 30000
+ }
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.Expect(HttpMethod.Get, "https://config.server.com:9999/example-app-name/example-profile/example-label")
+ .Respond("application/json", configServerResponseJson);
+
+ var defaultOptions = new ConfigServerClientOptions
+ {
+ Name = "ignored-because-overridden-from-appsettings",
+ Label = "example-label" // used, but missing in IConfiguration and injected options
+ };
+
+ Action configureOptions = options => options.FailFast = true;
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddPlaceholderResolver();
+ // ReSharper disable once AccessToDisposedClosure
+ configurationBuilder.AddConfigServer(defaultOptions, configureOptions, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ handler.Mock.VerifyNoOutstandingExpectation();
+ handler.Mock.Clear();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ IServiceCollection services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.ConfigureConfigServerClientOptions(configureOptions);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+
+ provider.ClientOptions.Uri.Should().Be("https://config.server.com:9999");
+ provider.ClientOptions.Name.Should().Be("example-app-name");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Timeout.Should().Be(30_000);
+ provider.ClientOptions.Label.Should().Be("example-label");
+ provider.ClientOptions.FailFast.Should().BeTrue();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri);
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout);
+ optionsMonitor.CurrentValue.Label.Should().BeNull();
+ optionsMonitor.CurrentValue.FailFast.Should().BeTrue();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "custom": {
+ "profileName": "example-profile"
+ },
+ "spring": {
+ "cloud": {
+ "config": {
+ "uri": "https://alternate-config.server.com:7777",
+ "name": "alternate-name",
+ "env": "${custom:profileName}",
+ "timeout": 15000,
+ "label": "alternate-label"
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ AssertFinal();
+
+ handler.Mock.Expect(HttpMethod.Get, "https://alternate-config.server.com:7777/alternate-name/example-profile/alternate-label")
+ .Respond("application/json", configServerResponseJson);
+
+ provider.Load();
+ handler.Mock.VerifyNoOutstandingExpectation();
+
+ AssertFinal();
+
+ void AssertFinal()
+ {
+ provider.ClientOptions.Uri.Should().Be("https://alternate-config.server.com:7777");
+ provider.ClientOptions.Name.Should().Be("alternate-name");
+ provider.ClientOptions.Environment.Should().Be("example-profile");
+ provider.ClientOptions.Timeout.Should().Be(15_000);
+ provider.ClientOptions.Label.Should().Be("alternate-label");
+ provider.ClientOptions.FailFast.Should().BeTrue();
+
+ optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri);
+ optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name);
+ optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment);
+ optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout);
+ optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label);
+ optionsMonitor.CurrentValue.FailFast.Should().BeTrue();
+
+ configuration["example-server-key"].Should().Be("example-server-value");
+ }
+ }
}
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs
index b78ec586fa..f299b94b3a 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs
@@ -5,7 +5,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Steeltoe.Common.TestResources;
-using Steeltoe.Common.TestResources.IO;
using Steeltoe.Configuration.Placeholder;
namespace Steeltoe.Configuration.ConfigServer.Test;
@@ -43,7 +42,8 @@ public void AddConfigServer_WithLoggerFactorySucceeds()
IList logMessages = loggerProvider.GetAll();
- logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s).");
+ logMessages.Should().Contain(
+ "DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching remote configuration from server(s).");
}
[Fact]
@@ -81,14 +81,11 @@ public void AddConfigServer_JsonAppSettingsConfiguresClient()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -135,14 +132,11 @@ public void AddConfigServer_ValidateCertificates_DisablesCertValidation()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -169,14 +163,11 @@ public void AddConfigServer_Validate_Certificates_DisablesCertValidation()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -208,14 +199,11 @@ public void AddConfigServer_XmlAppSettingsConfiguresClient()
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile("appsettings.xml", appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsXmlFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddXmlFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsXmlFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -250,14 +238,11 @@ public void AddConfigServer_IniAppSettingsConfiguresClient()
password=myPassword
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile("appsettings.ini", appSettings);
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsIniFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddIniFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsIniFile(fileProvider);
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -347,15 +332,11 @@ public void AddConfigServer_SubstitutesPlaceholders()
}
""";
- using var sandbox = new Sandbox();
- string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
-
- string directory = Path.GetDirectoryName(path)!;
- string fileName = Path.GetFileName(path);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(directory);
- configurationBuilder.AddJsonFile(fileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
configurationBuilder.AddPlaceholderResolver();
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -442,21 +423,15 @@ public void AddConfigServer_WithCloudfoundryEnvironment_ConfiguresClientCorrectl
}
""";
- using var sandbox = new Sandbox();
- string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string appSettingsFileName = Path.GetFileName(appSettingsPath);
-
- string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication);
- string vcapAppFileName = Path.GetFileName(vcapAppPath);
-
- string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices);
- string vcapServicesFileName = Path.GetFileName(vcapServicesPath);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
+ fileProvider.IncludeFile("vcapapp.json", vcapApplication);
+ fileProvider.IncludeFile("vcapservices.json", vcapServices);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(sandbox.FullPath);
- configurationBuilder.AddJsonFile(appSettingsFileName);
- configurationBuilder.AddJsonFile(vcapAppFileName);
- configurationBuilder.AddJsonFile(vcapServicesFileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json");
+ configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json");
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
@@ -550,21 +525,15 @@ public void AddConfigServer_WithCloudfoundryEnvironmentSCS3_ConfiguresClientCorr
}
""";
- using var sandbox = new Sandbox();
- string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings);
- string appSettingsFileName = Path.GetFileName(appSettingsPath);
-
- string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication);
- string vcapAppFileName = Path.GetFileName(vcapAppPath);
-
- string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices);
- string vcapServicesFileName = Path.GetFileName(vcapServicesPath);
+ var fileProvider = new MemoryFileProvider();
+ fileProvider.IncludeAppSettingsJsonFile(appSettings);
+ fileProvider.IncludeFile("vcapapp.json", vcapApplication);
+ fileProvider.IncludeFile("vcapservices.json", vcapServices);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.SetBasePath(sandbox.FullPath);
- configurationBuilder.AddJsonFile(appSettingsFileName);
- configurationBuilder.AddJsonFile(vcapAppFileName);
- configurationBuilder.AddJsonFile(vcapServicesFileName);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json");
+ configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json");
configurationBuilder.AddConfigServer();
IConfigurationRoot configurationRoot = configurationBuilder.Build();
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs
index 57ed14e112..cdfeb6bbb4 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Steeltoe.Common.TestResources;
using Steeltoe.Configuration.CloudFoundry;
@@ -130,12 +129,11 @@ public void AddConfigServer_WithConfigServerCertificate_AddsConfigServerSourceWi
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(appSettings);
- configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance);
- _ = configurationBuilder.Build();
+ configurationBuilder.AddConfigServer(options);
+ IConfigurationRoot configurationRoot = configurationBuilder.Build();
- ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault();
- source.Should().NotBeNull();
- source.DefaultOptions.ClientCertificate.Should().NotBeNull();
+ ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single();
+ provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull();
}
[Fact]
@@ -154,12 +152,11 @@ public void AddConfigServer_WithGlobalCertificate_AddsConfigServerSourceWithCert
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(appSettings);
- configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance);
- _ = configurationBuilder.Build();
+ configurationBuilder.AddConfigServer(options);
+ IConfigurationRoot configurationRoot = configurationBuilder.Build();
- ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault();
- source.Should().NotBeNull();
- source.DefaultOptions.ClientCertificate.Should().NotBeNull();
+ ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single();
+ provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull();
}
[Fact]
@@ -172,21 +169,6 @@ public void AddConfigServer_AddsConfigServerSourceToList()
source.Should().NotBeNull();
}
- [Fact]
- public void AddConfigServer_WithLoggerFactorySucceeds()
- {
- CapturingLoggerProvider loggerProvider = new();
- using var loggerFactory = new LoggerFactory([loggerProvider]);
-
- var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.AddConfigServer(loggerFactory);
- _ = configurationBuilder.Build();
-
- IList logMessages = loggerProvider.GetAll();
-
- logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s).");
- }
-
[Theory]
[InlineData(VcapServicesV2)]
[InlineData(VcapServicesV3)]
@@ -217,37 +199,94 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices)
provider.Should().BeOfType();
provider.ClientOptions.Uri.Should().NotBe("https://uri-from-settings");
provider.ClientOptions.Uri.Should().Be("https://uri-from-vcap-services");
+ provider.ClientOptions.ClientId.Should().Be("some-client-id");
+ provider.ClientOptions.ClientSecret.Should().Be("some-secret");
+ provider.ClientOptions.AccessTokenUri.Should().Be("https://uaa-uri-from-vcap-services/oauth/token");
}
[Fact]
- public void AddConfigServer_PaysAttentionToSettings()
+ public void AddConfigServer_CallbackOverridesConfigurationAndDefaultOptions()
{
var options = new ConfigServerClientOptions
{
- Name = "testConfigName",
- Label = "testConfigLabel",
- Environment = "testEnv",
- Username = "testUser",
- Password = "testPassword",
+ Name = "nameInOptions",
+ Label = "labelInOptions",
+ Environment = "environmentInOptions",
+ Username = "usernameInOptions",
+ Password = "passwordInOptions",
Timeout = 10,
Retry =
{
- Enabled = false
+ InitialInterval = 5,
+ MaxInterval = 15,
+ MaxAttempts = 12
}
};
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "Spring": {
+ "Cloud": {
+ "Config": {
+ "Name": "nameInAppSettings",
+ "Label": "labelInAppSettings",
+ "Timeout": 50,
+ "Retry": {
+ "MaxInterval": 100,
+ "MaxAttempts": 9
+ }
+ }
+ }
+ }
+ }
+ """);
+
+ Action configureOptions = clientOptions => clientOptions.Retry.MaxAttempts = 2;
+
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance);
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddConfigServer(options, configureOptions, null, NullLoggerFactory.Instance);
IConfigurationRoot configurationRoot = configurationBuilder.Build();
ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault();
provider.Should().NotBeNull();
- provider.ClientOptions.Label.Should().Be("testConfigLabel");
- provider.ClientOptions.Name.Should().Be("testConfigName");
- provider.ClientOptions.Environment.Should().Be("testEnv");
- provider.ClientOptions.Username.Should().Be("testUser");
- provider.ClientOptions.Password.Should().Be("testPassword");
+ provider.ClientOptions.Name.Should().Be("nameInAppSettings");
+ provider.ClientOptions.Label.Should().Be("labelInAppSettings");
+ provider.ClientOptions.Environment.Should().Be("environmentInOptions");
+ provider.ClientOptions.Username.Should().Be("usernameInOptions");
+ provider.ClientOptions.Password.Should().Be("passwordInOptions");
+ provider.ClientOptions.Timeout.Should().Be(50);
+ provider.ClientOptions.Retry.InitialInterval.Should().Be(5);
+ provider.ClientOptions.Retry.MaxInterval.Should().Be(100);
+ provider.ClientOptions.Retry.MaxAttempts.Should().Be(2);
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "Spring": {
+ "Cloud": {
+ "Config": {
+ "Name": "alternateNameInAppSettings",
+ "Username": "alternateUsernameInAppSettings"
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ provider.ClientOptions.Name.Should().Be("alternateNameInAppSettings");
+ provider.ClientOptions.Label.Should().Be("labelInOptions");
+ provider.ClientOptions.Environment.Should().Be("environmentInOptions");
+ provider.ClientOptions.Username.Should().Be("alternateUsernameInAppSettings");
+ provider.ClientOptions.Password.Should().Be("passwordInOptions");
+ provider.ClientOptions.Timeout.Should().Be(10);
+ provider.ClientOptions.Retry.InitialInterval.Should().Be(5);
+ provider.ClientOptions.Retry.MaxInterval.Should().Be(15);
+ provider.ClientOptions.Retry.MaxAttempts.Should().Be(2);
}
[Fact]
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs
index e5f9f669ac..9fe210d9f4 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs
@@ -2,13 +2,19 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
+using System.Net;
+using System.Net.Sockets;
+using System.Reflection;
+using System.Text;
using FluentAssertions.Extensions;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using RichardSzalay.MockHttp;
using Steeltoe.Common.TestResources;
+// ReSharper disable AccessToDisposedClosure
+
namespace Steeltoe.Configuration.ConfigServer.Test;
public sealed partial class ConfigServerConfigurationProviderTest
@@ -22,11 +28,12 @@ public async Task RemoteLoadAsync_HostTimesOut()
};
var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance);
+ provider.Load();
+
List requestUris = [new("http://localhost:9999/app/profile")];
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.RemoteLoadAsync(requestUris, null, TestContext.Current.CancellationToken);
+ Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken);
(await action.Should().ThrowExactlyAsync()).WithInnerExceptionExactly();
}
@@ -34,59 +41,42 @@ public async Task RemoteLoadAsync_HostTimesOut()
[Fact]
public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [500];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
+ ConfigServerClientOptions options = GetCommonOptions();
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError);
- ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken);
+ Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken);
await action.Should().ThrowExactlyAsync();
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
+ handler.Mock.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [204];
+ ConfigServerClientOptions options = GetCommonOptions();
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NoContent);
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken);
- ConfigEnvironment? result = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken);
-
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
+ handler.Mock.VerifyNoOutstandingExpectation();
result.Should().BeNull();
}
[Fact]
- public async Task Create_WithPollingTimer()
+ public async Task Create_WithConfigurationReloadTimer()
{
await TestFailureTracer.CaptureAsync(async tracer =>
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -98,18 +88,6 @@ await TestFailureTracer.CaptureAsync(async tracer =>
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
- startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)];
- startup.Label = "test-label";
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
var options = new ConfigServerClientOptions
{
Name = "myName",
@@ -117,28 +95,45 @@ await TestFailureTracer.CaptureAsync(async tracer =>
Label = "label,test-label"
};
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.NotFound);
+
+ using var firstRequestCountdownEvent = new CountdownEvent(1);
+
+ MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ =>
+ {
+ if (!firstRequestCountdownEvent.IsSet)
+ {
+ firstRequestCountdownEvent.Signal();
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(responseJson, Encoding.UTF8, "application/json")
+ };
+ });
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory);
+ provider.Load();
- bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds());
+ bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken);
firstRequestCompleted.Should().BeTrue();
- startup.RequestCount.Should().BeGreaterThanOrEqualTo(1);
- startup.LastRequest.Should().NotBeNull();
+ handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().BeGreaterThanOrEqualTo(2);
+ handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2);
provider.GetReloadToken().HasChanged.Should().BeFalse();
});
}
[Fact]
- public async Task Create_FailFastEnabledAndExceptionThrownDuringPolling_DoesNotCrash()
+ public async Task Create_FailFastEnabledAndExceptionThrownDuringPolledConfigurationReload_DoesNotCrash()
{
await TestFailureTracer.CaptureAsync(async tracer =>
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -150,20 +145,6 @@ await TestFailureTracer.CaptureAsync(async tracer =>
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
-
- // Initial requests succeed, but later requests return 400 status code so that an exception is thrown during polling
- startup.ReturnStatus = [.. Enumerable.Repeat(200, 2).Concat(Enumerable.Repeat(400, 100))];
- startup.Label = "test-label";
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
var options = new ConfigServerClientOptions
{
Name = "myName",
@@ -172,28 +153,74 @@ await TestFailureTracer.CaptureAsync(async tracer =>
Label = "test-label"
};
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory);
+ using var handler = new DelegateToMockHttpClientHandler();
+ using var firstRequestCountdownEvent = new CountdownEvent(1);
+ int requestCount = 0;
+
+ MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ =>
+ {
+ int currentCount = Interlocked.Increment(ref requestCount);
+
+ if (!firstRequestCountdownEvent.IsSet)
+ {
+ firstRequestCountdownEvent.Signal();
+ }
- bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds());
+ if (currentCount <= 2)
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(responseJson, Encoding.UTF8, "application/json")
+ };
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.BadRequest);
+ });
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory);
+ provider.Load();
+
+ bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken);
firstRequestCompleted.Should().BeTrue();
- startup.RequestCount.Should().BeGreaterThanOrEqualTo(1);
- startup.LastRequest.Should().NotBeNull();
+ handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().BeGreaterThanOrEqualTo(2);
+ handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2);
provider.GetReloadToken().HasChanged.Should().BeFalse();
});
}
[Fact]
- public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisabled()
+ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConfigurationReloadDisabled()
{
- const string environment = """
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ Enabled = false,
+ PollingInterval = 300.Milliseconds(),
+ Label = "label,test-label"
+ };
+
+ using var handler = new DelegateToMockHttpClientHandler();
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.OK);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
+
+ await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
+ handler.Mock.GetMatchCount(request).Should().Be(0);
+ }
+
+ [Theory]
+ [InlineData(false, "00:00:01")]
+ [InlineData(true, "00:00:00")]
+ public void OnSettingsChanged_stops_reload_timer_when_polling_no_longer_enabled(bool enabled, string pollingInterval)
+ {
+ const string responseJson = """
{
- "name": "test-name",
+ "name": "myName",
"profiles": [
"Production"
],
@@ -203,37 +230,261 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisa
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
- startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)];
- startup.Label = "test-label";
+ var fileProvider = new MemoryFileProvider();
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "enabled": true,
+ "pollingInterval": "00:00:01"
+ }
+ }
+ }
+ }
+ """);
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson);
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ FieldInfo reloadTimerField =
+ typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ reloadTimerField.GetValue(provider).Should().NotBeNull();
+
+ fileProvider.ReplaceAppSettingsJsonFile($$"""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "enabled": {{(enabled ? "true" : "false")}},
+ "pollingInterval": "{{pollingInterval}}"
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ reloadTimerField.GetValue(provider).Should().BeNull();
+ }
+
+ [Fact]
+ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_changes()
+ {
+ const string responseJson = """
+ {
+ "name": "myName",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": []
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "enabled": true,
+ "pollingInterval": "00:00:05"
+ }
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson);
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+
+ FieldInfo reloadTimerField =
+ typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ reloadTimerField.GetValue(provider).Should().NotBeNull();
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "enabled": true,
+ "pollingInterval": "00:00:10"
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ reloadTimerField.GetValue(provider).Should().NotBeNull("timer should be rescheduled, not stopped");
+ }
+
+ [Fact]
+ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disabled()
+ {
+ const string responseJson = """
+ {
+ "name": "myName",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": []
+ }
+ """;
+
+ var fileProvider = new MemoryFileProvider();
+
+ fileProvider.IncludeAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "token": "MyVaultToken"
+ }
+ }
+ }
+ }
+ """);
+
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson);
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider);
+ configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single();
+ FieldInfo vaultTimerField = typeof(ConfigServerConfigurationProvider).GetField("_vaultRenewTimer", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ vaultTimerField.GetValue(provider).Should().NotBeNull();
+
+ fileProvider.ReplaceAppSettingsJsonFile("""
+ {
+ "spring": {
+ "cloud": {
+ "config": {
+ "name": "myName",
+ "token": "MyVaultToken",
+ "disableTokenRenewal": true
+ }
+ }
+ }
+ }
+ """);
+
+ fileProvider.NotifyChanged();
+
+ vaultTimerField.GetValue(provider).Should().BeNull();
+ }
+
+ [Fact]
+ public void Load_MultipleConfigServers_SocketError_FallsBackToNextServer()
+ {
+ const string responseJson = """
+ {
+ "name": "test-name",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": [
+ {
+ "name": "source",
+ "source": {
+ "key1": "value1"
+ }
+ }
+ ]
+ }
+ """;
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.When(HttpMethod.Get, "http://server1:8888/myName/Production")
+ .Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused)));
+
+ handler.Mock.When(HttpMethod.Get, "http://server2:8888/myName/Production").Respond("application/json", responseJson);
var options = new ConfigServerClientOptions
{
Name = "myName",
- Enabled = false,
- PollingInterval = 300.Milliseconds(),
- Label = "label,test-label"
+ Uri = "http://server1:8888, http://server2:8888"
};
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ provider.TryGet("key1", out string? value).Should().BeTrue();
+ value.Should().Be("value1");
+ }
+
+ [Fact]
+ public void Load_MultipleConfigServers_SocketErrorFromAccessTokenUri_LogsWarnings()
+ {
+ using var loggerProvider = new CapturingLoggerProvider((_, level) => level == LogLevel.Warning);
+ using var loggerFactory = new LoggerFactory([loggerProvider]);
- startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse();
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.When(HttpMethod.Get, "http://auth-server.com")
+ .Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused)));
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ AccessTokenUri = "http://auth-server.com",
+ Uri = "http://config-server1:8888,http://config-server2:8888"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, loggerFactory);
+ provider.Load();
+
+ IList logMessages = loggerProvider.GetAll();
+
+ logMessages.Should().BeEquivalentTo([
+ $"WARN {typeof(ConfigServerConfigurationProvider)}: Failed to fetch access token from 'http://auth-server.com/'.",
+ $"WARN {typeof(ConfigServerConfigurationProvider)}: Failed to fetch access token from 'http://auth-server.com/'.",
+ $"WARN {typeof(ConfigServerConfigurationProvider)}: Failed fetching remote configuration from server(s)."
+ ], assertionOptions => assertionOptions.WithStrictOrdering());
+
+ provider.InnerData.Should().BeEmpty();
}
[Fact]
- public async Task DoLoad_MultipleLabels_ChecksAllLabels()
+ public void Load_IdenticalData_DoesNotTriggerReload()
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -245,49 +496,77 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels()
{
"name": "source",
"source": {
- "key1": "value1",
- "key2": 10
+ "key1": "value1"
}
}
]
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson);
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
- startup.ReturnStatus =
- [
- 404,
- 200
- ];
+ provider.TryGet("key1", out string? value).Should().BeTrue();
+ value.Should().Be("value1");
- startup.Label = "test-label";
+ bool reloadFired = false;
+ provider.GetReloadToken().RegisterChangeCallback(_ => reloadFired = true, null);
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
+ provider.Load();
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
+ reloadFired.Should().BeFalse("identical data should not trigger OnReload");
+ provider.TryGet("key1", out value).Should().BeTrue();
+ value.Should().Be("value1");
+ }
+
+ [Fact]
+ public void Load_MultipleLabels_ChecksAllLabels()
+ {
+ const string responseJson = """
+ {
+ "name": "test-name",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": [
+ {
+ "name": "source",
+ "source": {
+ "key1": "value1",
+ "key2": 10
+ }
+ }
+ ]
+ }
+ """;
ConfigServerClientOptions options = GetCommonOptions();
options.Label = "label,test-label";
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/label").Respond(HttpStatusCode.NotFound);
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/test-label").Respond("application/json", responseJson);
- await provider.DoLoadAsync(true, TestContext.Current.CancellationToken);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
- startup.LastRequest.Should().NotBeNull();
- startup.RequestCount.Should().Be(2);
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}/test-label");
+ handler.Mock.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task RemoteLoadAsync_ConfigServerReturnsGood()
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -307,24 +586,16 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood()
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
+ ConfigServerClientOptions options = GetCommonOptions();
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson);
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken);
- ConfigEnvironment? env = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken);
-
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
+ handler.Mock.VerifyNoOutstandingExpectation();
env.Should().NotBeNull();
env.Name.Should().Be("test-name");
@@ -342,245 +613,156 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood()
[Fact]
public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_StopsChecking()
{
- using var startup = new TestConfigServerStartup();
-
- startup.ReturnStatus =
- [
- 500,
- 200
- ];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.Uri = "http://localhost:8888, http://localhost:8888";
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
- await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}")
+ .Respond(HttpStatusCode.InternalServerError);
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
- startup.RequestCount.Should().Be(1);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
+
+ handler.Mock.GetMatchCount(request).Should().Be(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().Be(1);
+ handler.Mock.GetMatchCount(request).Should().Be(1);
}
[Fact]
public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContinueChecking()
{
- using var startup = new TestConfigServerStartup();
-
- startup.ReturnStatus =
- [
- 404,
- 200
- ];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.Uri = "http://localhost:8888, http://localhost:8888";
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}")
+ .Respond(HttpStatusCode.NotFound);
- await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
- startup.RequestCount.Should().Be(1);
+ handler.Mock.GetMatchCount(request).Should().Be(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().Be(1);
+ handler.Mock.GetMatchCount(request).Should().Be(1);
}
[Fact]
- public async Task Load_ConfigServerReturnsNotFoundStatus()
+ public void Load_ConfigServerReturnsNotFoundStatus()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [404];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
- await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
- provider.Properties.Should().HaveCount(27);
+ handler.Mock.VerifyNoOutstandingExpectation();
+ provider.InnerData.Should().BeEmpty();
}
[Fact]
- public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled()
+ public void Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [404];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.FailFast = true;
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ Action action = provider.Load;
- await action.Should().ThrowExactlyAsync();
+ action.Should().ThrowExactly();
+ handler.Mock.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotContinueChecking_FailFastEnabled()
{
- using var startup = new TestConfigServerStartup();
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.FailFast = true;
options.Uri = "http://localhost:8888,http://localhost:8888";
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
- startup.Reset();
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}")
+ .Respond(HttpStatusCode.NotFound);
- startup.ReturnStatus =
- [
- 404,
- 200
- ];
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ Action action = provider.Load;
- await action.Should().ThrowExactlyAsync();
- startup.RequestCount.Should().Be(1);
+ action.Should().ThrowExactly();
+ handler.Mock.GetMatchCount(request).Should().Be(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().Be(1);
+ handler.Mock.GetMatchCount(request).Should().Be(1);
}
[Fact]
- public async Task Load_UriInvalid_FailFastEnabled()
+ public void Load_UriInvalid_FailFastEnabled()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [500];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.Uri = "http://username:p@ssword@localhost:8888";
options.FailFast = true;
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ Action action = provider.Load;
- await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid.");
+ action.Should().ThrowExactly().WithMessage("One or more Config Server URIs in configuration are invalid.");
+ handler.Mock.VerifyNoOutstandingExpectation();
}
[Fact]
- public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled()
+ public void Load_ConfigServerReturnsBadStatus_FailFastEnabled()
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [500];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.FailFast = true;
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- await action.Should().ThrowExactlyAsync();
+ Action action = provider.Load;
+
+ action.Should().ThrowExactly();
+ handler.Mock.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_FailFastEnabled()
{
- using var startup = new TestConfigServerStartup();
-
- startup.ReturnStatus =
- [
- 500,
- 500,
- 500
- ];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
options.FailFast = true;
options.Uri = "http://localhost:8888, http://localhost:8888, http://localhost:8888";
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}")
+ .Respond(HttpStatusCode.InternalServerError);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ Action action = provider.Load;
- await action.Should().ThrowExactlyAsync();
- startup.RequestCount.Should().Be(1);
+ action.Should().ThrowExactly();
+ handler.Mock.GetMatchCount(request).Should().Be(1);
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().Be(1);
+ handler.Mock.GetMatchCount(request).Should().Be(1);
}
[Fact]
@@ -588,16 +770,6 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled_RetryEnabled
{
await TestFailureTracer.CaptureAsync(async tracer =>
{
- using var startup = new TestConfigServerStartup();
- startup.ReturnStatus = [.. Enumerable.Repeat(500, 100)];
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
var options = new ConfigServerClientOptions
{
Name = "myName",
@@ -610,24 +782,25 @@ await TestFailureTracer.CaptureAsync(async tracer =>
Timeout = 1000
};
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory);
+ using var handler = new DelegateToMockHttpClientHandler();
+ MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond(HttpStatusCode.InternalServerError);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory);
- // ReSharper disable once AccessToDisposedClosure
- Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ Action action = provider.Load;
- await action.Should().ThrowExactlyAsync();
+ action.Should().ThrowExactly();
await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken);
- startup.RequestCount.Should().BeGreaterThan(3);
+ handler.Mock.GetMatchCount(request).Should().BeGreaterThan(3);
});
}
[Fact]
- public async Task Load_ChangesDataDictionary()
+ public void Load_ChangesDataDictionary()
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -635,6 +808,7 @@ public async Task Load_ChangesDataDictionary()
],
"label": "test-label",
"version": "test-version",
+ "state": "test-state",
"propertySources": [
{
"name": "source",
@@ -647,35 +821,30 @@ public async Task Load_ChangesDataDictionary()
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
- await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson);
- startup.LastRequest.Should().NotBeNull();
- startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}");
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
+ handler.Mock.VerifyNoOutstandingExpectation();
provider.TryGet("key1", out string? value).Should().BeTrue();
value.Should().Be("value1");
provider.TryGet("key2", out value).Should().BeTrue();
value.Should().Be("10");
+
+ provider.TryGet("spring:cloud:config:client:version", out value).Should().BeTrue();
+ value.Should().Be("test-version");
+ provider.TryGet("spring:cloud:config:client:state", out value).Should().BeTrue();
+ value.Should().Be("test-state");
}
[Fact]
- public async Task ReLoad_DataDictionary_With_New_Configurations()
+ public void ReLoad_DataDictionary_With_New_Configurations()
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -697,23 +866,15 @@ public async Task ReLoad_DataDictionary_With_New_Configurations()
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri("http://localhost:8888");
-
ConfigServerClientOptions options = GetCommonOptions();
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson);
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
provider.Load();
+ handler.Mock.VerifyNoOutstandingExpectation();
- startup.LastRequest.Should().NotBeNull();
provider.TryGet("featureToggles:ShowModule:0", out string? value).Should().BeTrue();
value.Should().Be("FT1");
provider.TryGet("featureToggles:ShowModule:1", out value).Should().BeTrue();
@@ -723,28 +884,30 @@ public async Task ReLoad_DataDictionary_With_New_Configurations()
provider.TryGet("enableSettings", out value).Should().BeTrue();
value.Should().Be("true");
- startup.Reset();
+ handler.Mock.Clear();
- startup.Response = """
- {
- "name": "test-name",
- "profiles": [
- "Production"
- ],
- "label": "test-label",
- "version": "test-version",
- "propertySources": [
+ const string newResponseJson = """
{
- "name": "source",
- "source": {
- "featureToggles.ShowModule[0]": "none"
- }
+ "name": "test-name",
+ "profiles": [
+ "Production"
+ ],
+ "label": "test-label",
+ "version": "test-version",
+ "propertySources": [
+ {
+ "name": "source",
+ "source": {
+ "featureToggles.ShowModule[0]": "none"
+ }
+ }
+ ]
}
- ]
- }
- """;
+ """;
+ handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", newResponseJson);
provider.Load();
+ handler.Mock.VerifyNoOutstandingExpectation();
provider.TryGet("featureToggles:ShowModule:0", out value).Should().BeTrue();
value.Should().Be("none");
@@ -754,35 +917,29 @@ public async Task ReLoad_DataDictionary_With_New_Configurations()
}
[Fact]
- public void AddConfigServerClientSettings_ChangesDataDictionary()
+ public void DataDictionary_DoesNotContainRedundantClientSettings()
{
var options = new ConfigServerClientOptions
{
Enabled = false,
FailFast = true,
Environment = "environment",
- Label = "label",
+ Label = "main",
Name = "name",
Uri = "https://foo.bar/",
- Username = "username",
- Password = "password",
- Token = "vaultToken",
+ Username = "user",
+ Password = "pass",
+ Token = "vault-token",
Timeout = 75_000,
- PollingInterval = 35.5.Seconds(),
+ PollingInterval = TimeSpan.FromSeconds(30),
ValidateCertificates = false,
- AccessTokenUri = "https://token.server.com/",
- ClientSecret = "client_secret",
- ClientId = "client_id",
- TokenTtl = 2,
- TokenRenewRate = 1,
- DisableTokenRenewal = true,
Retry =
{
Enabled = true,
- InitialInterval = 8,
- MaxInterval = 16,
- Multiplier = 1.1,
- MaxAttempts = 7
+ InitialInterval = 500,
+ MaxInterval = 5000,
+ Multiplier = 2.0,
+ MaxAttempts = 10
},
Discovery =
{
@@ -792,59 +949,63 @@ public void AddConfigServerClientSettings_ChangesDataDictionary()
Health =
{
Enabled = false,
- TimeToLive = 9
+ TimeToLive = 999
},
+ AccessTokenUri = "https://uaa.example.com/oauth/token",
+ ClientSecret = "secret",
+ ClientId = "client-id",
+ TokenTtl = 600_000,
+ TokenRenewRate = 120_000,
+ DisableTokenRenewal = true,
Headers =
{
- ["headerName1"] = "headerValue1",
- ["headerName2"] = "headerValue2"
+ ["X-Custom"] = "value"
}
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- provider.AddConfigServerClientOptions();
-
- AssertDataValue("spring:cloud:config:enabled", "False");
- AssertDataValue("spring:cloud:config:failFast", "True");
- AssertDataValue("spring:cloud:config:env", "environment");
- AssertDataValue("spring:cloud:config:label", "label");
- AssertDataValue("spring:cloud:config:name", "name");
- AssertDataValue("spring:cloud:config:uri", "https://foo.bar/");
- AssertDataValue("spring:cloud:config:username", "username");
- AssertDataValue("spring:cloud:config:password", "password");
- AssertDataValue("spring:cloud:config:token", "vaultToken");
- AssertDataValue("spring:cloud:config:timeout", "75000");
- AssertDataValue("spring:cloud:config:pollingInterval", "00:00:35.5000000");
- AssertDataValue("spring:cloud:config:validateCertificates", "False");
- AssertDataValue("spring:cloud:config:accessTokenUri", "https://token.server.com/");
- AssertDataValue("spring:cloud:config:clientSecret", "client_secret");
- AssertDataValue("spring:cloud:config:clientId", "client_id");
- AssertDataValue("spring:cloud:config:tokenTtl", "2");
- AssertDataValue("spring:cloud:config:tokenRenewRate", "1");
- AssertDataValue("spring:cloud:config:disableTokenRenewal", "True");
- AssertDataValue("spring:cloud:config:retry:enabled", "True");
- AssertDataValue("spring:cloud:config:retry:initialInterval", "8");
- AssertDataValue("spring:cloud:config:retry:maxInterval", "16");
- AssertDataValue("spring:cloud:config:retry:multiplier", "1.1");
- AssertDataValue("spring:cloud:config:retry:maxAttempts", "7");
- AssertDataValue("spring:cloud:config:discovery:enabled", "True");
- AssertDataValue("spring:cloud:config:discovery:serviceId", "my-config-server");
- AssertDataValue("spring:cloud:config:health:enabled", "False");
- AssertDataValue("spring:cloud:config:health:timeToLive", "9");
- AssertDataValue("spring:cloud:config:headers:headerName1", "headerValue1");
- AssertDataValue("spring:cloud:config:headers:headerName2", "headerValue2");
-
- void AssertDataValue(string key, string expected)
- {
- provider.TryGet(key, out string? value).Should().BeTrue();
- value.Should().Be(expected);
- }
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ provider.TryGet("spring:cloud:config:enabled", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:failFast", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:env", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:label", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:name", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:uri", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:username", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:password", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:token", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:timeout", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:pollingInterval", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:validateCertificates", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:validate_Certificates", out _).Should().BeFalse();
+
+ provider.TryGet("spring:cloud:config:retry:enabled", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:retry:initialInterval", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:retry:maxInterval", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:retry:multiplier", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:retry:maxAttempts", out _).Should().BeFalse();
+
+ provider.TryGet("spring:cloud:config:discovery:enabled", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:discovery:serviceId", out _).Should().BeFalse();
+
+ provider.TryGet("spring:cloud:config:health:enabled", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:health:timeToLive", out _).Should().BeFalse();
+
+ provider.TryGet("spring:cloud:config:accessTokenUri", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:clientSecret", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:clientId", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:tokenTtl", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:tokenRenewRate", out _).Should().BeFalse();
+ provider.TryGet("spring:cloud:config:disableTokenRenewal", out _).Should().BeFalse();
+
+ provider.TryGet("spring:cloud:config:headers:X-Custom", out _).Should().BeFalse();
}
[Fact]
- public async Task Reload_And_Bind_Without_Throwing_Exception()
+ public void Reload_And_Bind_Without_Throwing_Exception()
{
- const string environment = """
+ const string responseJson = """
{
"name": "test-name",
"profiles": [
@@ -864,30 +1025,22 @@ public async Task Reload_And_Bind_Without_Throwing_Exception()
}
""";
- using var startup = new TestConfigServerStartup();
- startup.Response = environment;
-
- await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
- startup.Configure(app);
- await app.StartAsync(TestContext.Current.CancellationToken);
-
ConfigServerClientOptions clientOptions = GetCommonOptions();
- using TestServer server = app.GetTestServer();
- server.BaseAddress = new Uri(clientOptions.Uri!);
- using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
- using var provider = new ConfigServerConfigurationProvider(clientOptions, null, httpClientHandler, NullLoggerFactory.Instance);
+ using var handler = new DelegateToMockHttpClientHandler();
+ handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{clientOptions.Name}/{clientOptions.Environment}").Respond("application/json", responseJson);
var configurationBuilder = new ConfigurationBuilder();
- configurationBuilder.Add(new TestConfigServerConfigurationSource(provider));
+ configurationBuilder.AddConfigServer(clientOptions, null, () => handler, NullLoggerFactory.Instance);
IConfigurationRoot configurationRoot = configurationBuilder.Build();
+ using ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single();
+
TestOptions? testOptions = null;
using var tokenSource = new CancellationTokenSource(250.Milliseconds());
_ = Task.Run(() =>
{
- // ReSharper disable once AccessToDisposedClosure
while (!tokenSource.IsCancellationRequested)
{
configurationRoot.Reload();
@@ -908,7 +1061,8 @@ private static ConfigServerClientOptions GetCommonOptions()
{
return new ConfigServerClientOptions
{
- Name = "myName"
+ Name = "myName",
+ Environment = "Staging"
};
}
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Redirect.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Redirect.cs
new file mode 100644
index 0000000000..71d85ad08d
--- /dev/null
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Redirect.cs
@@ -0,0 +1,197 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Steeltoe.Common.TestResources;
+
+namespace Steeltoe.Configuration.ConfigServer.Test;
+
+public sealed partial class ConfigServerConfigurationProviderTest
+{
+ [Fact]
+ public async Task RemoteLoadAsync_DoesNotFollowRedirect_WhenConfigServerEndpointRedirects()
+ {
+ using var loggerProvider = new CapturingLoggerProvider((_, level) => level == LogLevel.Warning);
+ using var loggerFactory = new LoggerFactory([loggerProvider]);
+ bool redirectRouteAccessed = false;
+
+ WebApplicationBuilder serverBuilder = WebApplication.CreateBuilder();
+ serverBuilder.Logging.ClearProviders();
+ await using WebApplication server = serverBuilder.Build();
+ server.Urls.Add("http://127.0.0.1:0");
+
+ server.MapGet("/myName/Staging",
+ (HttpContext httpContext) => httpContext.Response.Redirect($"http://127.0.0.1:{httpContext.Connection.LocalPort}/redirect-target", true));
+
+ server.MapGet("/redirect-target", () =>
+ {
+ redirectRouteAccessed = true;
+
+ return Results.Json(new ConfigEnvironment
+ {
+ Name = "redirected"
+ });
+ });
+
+ await server.StartAsync(TestContext.Current.CancellationToken);
+ int port = server.Services.GetRequiredService().Features.Get()!.Addresses.Select(a => new Uri(a).Port).First();
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ Environment = "Staging",
+ Uri = $"http://127.0.0.1:{port}",
+ Token = "vault-secret"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, loggerFactory);
+
+ ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken);
+
+ result.Should().BeNull();
+ redirectRouteAccessed.Should().BeFalse();
+
+ IList logMessages = loggerProvider.GetAll();
+ logMessages.Should().ContainSingle().Which.Should().Contain("Redirects are not followed to prevent credential leaks.");
+ }
+
+ [Fact]
+ public async Task RemoteLoadAsync_DoesNotFollowRedirect_WhenAccessTokenEndpointRedirects()
+ {
+ using var loggerProvider = new CapturingLoggerProvider((_, level) => level == LogLevel.Warning);
+ using var loggerFactory = new LoggerFactory([loggerProvider]);
+
+ bool redirectRouteAccessed = false;
+
+ WebApplicationBuilder serverBuilder = WebApplication.CreateBuilder();
+ serverBuilder.Logging.ClearProviders();
+ await using WebApplication server = serverBuilder.Build();
+ server.Urls.Add("http://127.0.0.1:0");
+
+ server.MapPost("/token",
+ (HttpContext httpContext) => httpContext.Response.Redirect($"http://127.0.0.1:{httpContext.Connection.LocalPort}/token-redirect", true));
+
+ server.MapGet("/token-redirect", () =>
+ {
+ redirectRouteAccessed = true;
+ return Results.Ok();
+ });
+
+ await server.StartAsync(TestContext.Current.CancellationToken);
+ int port = server.Services.GetRequiredService().Features.Get()!.Addresses.Select(a => new Uri(a).Port).First();
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ Environment = "Staging",
+ Token = "vault-secret",
+ AccessTokenUri = $"http://127.0.0.1:{port}/token",
+ ClientId = "some-client",
+ ClientSecret = "some-secret"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, loggerFactory);
+
+ ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken);
+
+ result.Should().BeNull();
+ redirectRouteAccessed.Should().BeFalse();
+
+ IList logMessages = loggerProvider.GetAll();
+ logMessages.Should().ContainSingle().Which.Should().Contain("Failed to fetch access token from");
+ }
+
+ [Fact]
+ public async Task RefreshVaultTokenAsync_DoesNotFollowRedirect_WhenVaultRenewEndpointRedirects()
+ {
+ using var loggerProvider = new CapturingLoggerProvider((_, level) => level == LogLevel.Warning);
+ using var loggerFactory = new LoggerFactory([loggerProvider]);
+
+ bool redirectRouteAccessed = false;
+
+ WebApplicationBuilder serverBuilder = WebApplication.CreateBuilder();
+ serverBuilder.Logging.ClearProviders();
+ await using WebApplication server = serverBuilder.Build();
+ server.Urls.Add("http://127.0.0.1:0");
+
+ server.MapPost("/vault/v1/auth/token/renew-self",
+ (HttpContext httpContext) => httpContext.Response.Redirect($"http://127.0.0.1:{httpContext.Connection.LocalPort}/vault-redirect", true));
+
+ server.MapGet("/vault-redirect", () =>
+ {
+ redirectRouteAccessed = true;
+ return Results.Ok();
+ });
+
+ await server.StartAsync(TestContext.Current.CancellationToken);
+ int port = server.Services.GetRequiredService().Features.Get()!.Addresses.Select(a => new Uri(a).Port).First();
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ Environment = "Staging",
+ Uri = $"http://127.0.0.1:{port}",
+ Token = "MyVaultToken"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, loggerFactory);
+
+ await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken);
+
+ redirectRouteAccessed.Should().BeFalse();
+
+ IList logMessages = loggerProvider.GetAll();
+ logMessages.Should().ContainSingle().Which.Should().Contain("returned status");
+ }
+
+ [Fact]
+ public async Task RefreshVaultTokenAsync_DoesNotFollowRedirect_WhenAccessTokenEndpointRedirects()
+ {
+ using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Warning);
+ using var loggerFactory = new LoggerFactory([loggerProvider]);
+
+ bool redirectRouteAccessed = false;
+
+ WebApplicationBuilder serverBuilder = WebApplication.CreateBuilder();
+ serverBuilder.Logging.ClearProviders();
+ await using WebApplication server = serverBuilder.Build();
+ server.Urls.Add("http://127.0.0.1:0");
+
+ server.MapPost("/token",
+ (HttpContext httpContext) => httpContext.Response.Redirect($"http://127.0.0.1:{httpContext.Connection.LocalPort}/token-redirect", true));
+
+ server.MapGet("/token-redirect", () =>
+ {
+ redirectRouteAccessed = true;
+ return Results.Ok();
+ });
+
+ await server.StartAsync(TestContext.Current.CancellationToken);
+ int port = server.Services.GetRequiredService().Features.Get()!.Addresses.Select(a => new Uri(a).Port).First();
+
+ var options = new ConfigServerClientOptions
+ {
+ Name = "myName",
+ Environment = "Staging",
+ Token = "MyVaultToken",
+ AccessTokenUri = $"http://127.0.0.1:{port}/token",
+ ClientId = "some-client",
+ ClientSecret = "some-secret"
+ };
+
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, loggerFactory);
+
+ await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken);
+
+ redirectRouteAccessed.Should().BeFalse();
+
+ IList logMessages = loggerProvider.GetAll();
+ logMessages.Should().ContainSingle().Which.Should().Contain("Unable to renew Vault token");
+ }
+}
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs
index 8380debc6b..1cd6f664f6 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs
@@ -15,22 +15,23 @@ public sealed partial class ConfigServerConfigurationProviderTest
public void DefaultConstructor_InitializedWithDefaultSettings()
{
var options = new ConfigServerClientOptions();
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name;
- TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName);
+ TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production");
}
[Fact]
public void SourceConstructor_WithDefaults_InitializesWithDefaultSettings()
{
- IConfiguration configuration = new ConfigurationBuilder().Build();
var options = new ConfigServerClientOptions();
- var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance);
+ var source = new ConfigServerConfigurationSource(options, [], null, null, null, NullLoggerFactory.Instance);
using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance);
+ provider.Load();
string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name;
- TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName);
+ TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production");
}
[Fact]
@@ -41,12 +42,13 @@ public void SourceConstructor_WithTimeoutConfigured_InitializesHttpClientWithCon
["spring:cloud:config:timeout"] = "30000"
};
- IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build();
+ var builder = new ConfigurationBuilder();
+ builder.AddInMemoryCollection(appSettings);
+ builder.AddConfigServer();
+ IConfigurationRoot configuration = builder.Build();
- var options = new ConfigServerClientOptions();
- var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance);
- using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance);
- using HttpClient httpClient = provider.CreateHttpClient(options);
+ ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single();
+ using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions);
httpClient.Should().NotBeNull();
httpClient.Timeout.Should().Be(30.Seconds());
@@ -61,10 +63,11 @@ public void GetConfigServerUri_NoBaseUri_Throws()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
- // ReSharper disable once AccessToDisposedClosure
- Action action = () => provider.BuildConfigServerUri(null!, null);
+ // ReSharper disable AccessToDisposedClosure
+ Action action = () => provider.BuildConfigServerUri(provider.ClientOptions, null!, null);
+ // ReSharper restore AccessToDisposedClosure
action.Should().ThrowExactly();
}
@@ -78,8 +81,10 @@ public void GetConfigServerUri_NoLabel()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), null).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null).ToString();
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}");
}
@@ -94,8 +99,10 @@ public void GetConfigServerUri_WithLabel()
Label = "myLabel"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString();
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}");
}
@@ -110,8 +117,10 @@ public void GetConfigServerUri_WithLabelContainingSlash()
Label = "myLabel/version"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString();
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version");
}
@@ -126,8 +135,10 @@ public void GetConfigServerUri_WithExtraPathInfo()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString();
uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
}
@@ -142,8 +153,10 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString();
uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
}
@@ -158,8 +171,10 @@ public void GetConfigServerUri_NoEndingSlash()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString();
uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
}
@@ -174,8 +189,10 @@ public void GetConfigServerUri_WithEndingSlash()
Environment = "Production"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString();
uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
}
@@ -190,8 +207,10 @@ public void GetConfigServerUri_MultipleEnvironments_EncodesComma()
Label = "demo"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString();
uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo");
}
@@ -208,8 +227,10 @@ public void GetConfigServerUri_EncodesSpecialCharacters()
Password = "#&:$<>'/so,\"me"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString();
uri.Should().Be(
"http://a%22%3A%3F%27*%2Cb%2F%5C%26:%23%26%3A%24%3C%3E%27%2Fso%2C%22me@localhost:8888/my%24<>%3A%2C\"%27Name/%40%2F%26%3F%3A\"%27/^%26%24%40%23*<>%2B");
diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs
index 6638ca8c82..7b8c950a39 100644
--- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs
+++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs
@@ -65,9 +65,10 @@ public async Task Deserialize_GoodJson()
public void GetLabels_Null()
{
var options = new ConfigServerClientOptions();
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- string[] result = provider.GetLabels();
+ string[] result = provider.GetLabels(provider.ClientOptions);
result.Should().ContainSingle().Which.Should().BeEmpty();
}
@@ -79,9 +80,10 @@ public void GetLabels_Empty()
Label = string.Empty
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- string[] result = provider.GetLabels();
+ string[] result = provider.GetLabels(provider.ClientOptions);
result.Should().ContainSingle().Which.Should().BeEmpty();
}
@@ -93,9 +95,10 @@ public void GetLabels_SingleString()
Label = "foobar"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- string[] result = provider.GetLabels();
+ string[] result = provider.GetLabels(provider.ClientOptions);
result.Should().ContainSingle().Which.Should().Be("foobar");
}
@@ -107,9 +110,10 @@ public void GetLabels_MultiString()
Label = "1,2,3,"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- string[] result = provider.GetLabels();
+ string[] result = provider.GetLabels(provider.ClientOptions);
result.Should().HaveCount(3);
result.Should().HaveElementAt(0, "1");
result.Should().HaveElementAt(1, "2");
@@ -124,9 +128,10 @@ public void GetLabels_MultiStringHoles()
Label = "1,,2,3,"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- string[] result = provider.GetLabels();
+ string[] result = provider.GetLabels(provider.ClientOptions);
result.Should().HaveCount(3);
result.Should().HaveElementAt(0, "1");
result.Should().HaveElementAt(1, "2");
@@ -143,10 +148,13 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL()
Environment = "development"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null);
- HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken);
+ Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
+
+ HttpRequestMessage request =
+ await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
request.Method.Should().Be(HttpMethod.Get);
request.RequestUri.Should().Be(requestUri);
@@ -167,10 +175,13 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings
Password = "password"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
- Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null);
- HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken);
+ HttpRequestMessage request =
+ await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
request.Method.Should().Be(HttpMethod.Get);
request.RequestUri.Should().Be(requestUri);
@@ -191,10 +202,13 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw
Password = "password"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
- Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null);
- HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken);
+ HttpRequestMessage request =
+ await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
request.Method.Should().Be(HttpMethod.Get);
request.RequestUri.Should().Be(requestUri);
@@ -213,10 +227,13 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded()
Token = "MyVaultToken"
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
- Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri!), null);
- HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken);
+ Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null);
+
+ HttpRequestMessage request =
+ await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
request.Method.Should().Be(HttpMethod.Get);
request.RequestUri.Should().Be(requestUri);
@@ -225,6 +242,43 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded()
headerValues.Should().Contain("MyVaultToken");
}
+ [Fact]
+ public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet()
+ {
+ var options = new ConfigServerClientOptions
+ {
+ Uri = "http://localhost:8888/",
+ Name = "foo",
+ Environment = "development",
+ AccessTokenUri = "https://auth.server.com",
+ ClientId = "test-client-id",
+ ClientSecret = "test-client-secret"
+ };
+
+ using var handler = new DelegateToMockHttpClientHandler();
+
+ handler.Mock.Expect(HttpMethod.Post, "https://auth.server.com/").WithHeaders("Authorization", "Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0")
+ .WithFormData("grant_type=client_credentials").Respond("application/json", """
+ {
+ "access_token": "my-bearer-token"
+ }
+ """);
+
+ // ReSharper disable once AccessToDisposedClosure
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+
+ Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
+
+ HttpRequestMessage request =
+ await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
+
+ handler.Mock.VerifyNoOutstandingExpectation();
+
+ request.Headers.Authorization.Should().NotBeNull();
+ request.Headers.Authorization.Scheme.Should().Be("Bearer");
+ request.Headers.Authorization.Parameter.Should().Be("my-bearer-token");
+ }
+
[Fact]
public async Task RefreshVaultToken_Succeeds()
{
@@ -240,8 +294,11 @@ public async Task RefreshVaultToken_Succeeds()
handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken")
.WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent);
- using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance);
- await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken);
+ // ReSharper disable once AccessToDisposedClosure
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+ provider.Load();
+
+ await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken);
handler.Mock.VerifyNoOutstandingExpectation();
}
@@ -267,8 +324,10 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds()
handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken")
.WithHeaders("Authorization", "Bearer secret").WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent);
- using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance);
- await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken);
+ // ReSharper disable once AccessToDisposedClosure
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
+
+ await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken);
handler.Mock.VerifyNoOutstandingExpectation();
}
@@ -287,8 +346,10 @@ public void GetHttpClient_AddsHeaders_IfConfigured()
}
};
- using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
- using HttpClient httpClient = provider.CreateHttpClient(options);
+ using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance);
+ provider.Load();
+
+ using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions);
httpClient.Should().NotBeNull();
httpClient.DefaultRequestHeaders.GetValues("foo").SingleOrDefault().Should().Be("bar");
@@ -308,42 +369,42 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected()
}
};
- using (var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance))
+ using (var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance))
{
- provider.IsDiscoveryFirstEnabled().Should().BeTrue();
+ provider.Load();
+ provider.ClientOptions.Discovery.Enabled.Should().BeTrue();
}
- var values = new Dictionary
+ var appSettings = new Dictionary
{
["spring:cloud:config:discovery:enabled"] = "True"
};
- IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
-
options = new ConfigServerClientOptions
{
Name = "foo",
Environment = "development"
};
- var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance);
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryCollection(appSettings);
+ configurationBuilder.AddConfigServer(options);
+ IConfigurationRoot configuration = configurationBuilder.Build();
- using (var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance))
+ using (ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single())
{
- provider.IsDiscoveryFirstEnabled().Should().BeTrue();
+ provider.ClientOptions.Discovery.Enabled.Should().BeTrue();
}
}
[Fact]
public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly()
{
- var values = new Dictionary
+ var appSettings = new Dictionary
{
["spring:cloud:config:discovery:enabled"] = "True"
};
- IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
-
var options = new ConfigServerClientOptions
{
Uri = "http://localhost:8888/",
@@ -351,13 +412,19 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly()
Environment = "development"
};
- var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance);
- using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance);
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryCollection(appSettings);
+ configurationBuilder.AddConfigServer(options);
+ IConfigurationRoot configuration = configurationBuilder.Build();
+
+ using ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single();
- provider.UpdateSettingsFromDiscovery(new List(), options);
- options.Username.Should().BeNull();
- options.Password.Should().BeNull();
- options.Uri.Should().Be("http://localhost:8888/");
+ ConfigServerClientOptions optionsSnapshot = provider.ClientOptions;
+ provider.SetLastDiscoveryLookupResult(new List());
+ provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot);
+ optionsSnapshot.Username.Should().BeNull();
+ optionsSnapshot.Password.Should().BeNull();
+ optionsSnapshot.Uri.Should().Be("http://localhost:8888/");
var metadata1 = new Dictionary
{
@@ -373,28 +440,28 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly()
List instances =
[
- new TestServiceInstance("i1", new Uri("https://foo.bar:8888/"), metadata1),
- new TestServiceInstance("i2", new Uri("https://foo.bar.baz:9999/"), metadata2)
+ new TestServiceInstance("s", "i1", new Uri("https://foo.bar:8888/"), metadata1),
+ new TestServiceInstance("s", "i2", new Uri("https://foo.bar.baz:9999/"), metadata2)
];
- provider.UpdateSettingsFromDiscovery(instances, options);
- options.Username.Should().Be("secondUser");
- options.Password.Should().Be("secondPassword");
- options.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath");
+ optionsSnapshot = provider.ClientOptions;
+ provider.SetLastDiscoveryLookupResult(instances);
+ provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot);
+ optionsSnapshot.Username.Should().Be("secondUser");
+ optionsSnapshot.Password.Should().Be("secondPassword");
+ optionsSnapshot.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath");
}
[Fact]
- public async Task DiscoverServerInstances_FailsFast()
+ public void DiscoverServerInstances_FailsFast()
{
- var values = new Dictionary
+ var appSettings = new Dictionary
{
["spring:cloud:config:discovery:enabled"] = "True",
["spring:cloud:config:failFast"] = "True",
["eureka:client:eurekaServer:retryCount"] = "0"
};
- IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
-
var options = new ConfigServerClientOptions
{
Name = "foo",
@@ -402,27 +469,17 @@ public async Task DiscoverServerInstances_FailsFast()
Timeout = 10
};
- var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance);
- using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance);
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryCollection(appSettings);
+ configurationBuilder.AddConfigServer(options);
- // ReSharper disable once AccessToDisposedClosure
- Func