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 -[![Build Status](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml/badge.svg?branch=main)](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml?query=branch%3Amain) +[![Build Status](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml/badge.svg?branch=4.x)](https://github.com/SteeltoeOSS/Steeltoe/actions/workflows/Steeltoe.All.yml?query=branch%3A4.x)  [![SonarCloud](https://sonarcloud.io/api/project_badges/measure?project=SteeltoeOSS_steeltoe&branch=main&metric=alert_status)](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main)  [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=SteeltoeOSS_steeltoe&branch=main&metric=coverage)](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main&metric=coverage&view=list)  [![NuGet Version](https://img.shields.io/nuget/v/Steeltoe.Common.svg?style=flat)](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 action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = () => _ = configurationBuilder.Build(); - await action.Should().ThrowExactlyAsync().WithMessage("Could not locate Config Server via discovery*"); + action.Should().ThrowExactly().WithMessage("Could not locate Config Server via discovery*"); } private static string GetEncodedUserPassword(string user, string password) { return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{password}")); } - - private sealed class TestServiceInstance(string serviceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance - { - public string ServiceId { get; } = serviceId; - public string Host { get; } = uri.Host; - public int Port { get; } = uri.Port; - public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; - public Uri Uri { get; } = uri; - public IReadOnlyDictionary Metadata { get; } = metadata; - } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index 1be77f49a0..a66b415c5c 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -21,7 +21,7 @@ public void Constructors_InitializesProperties() var source = new ConfigServerConfigurationSource(options, sources, new Dictionary { ["foo"] = "bar" - }, NullLoggerFactory.Instance); + }, null, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); source.Configuration.Should().BeNull(); @@ -29,14 +29,6 @@ public void Constructors_InitializesProperties() source.Sources.Should().ContainSingle().Which.Should().Be(memSource); source.Properties.Should().ContainSingle(); source.Properties.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); - - IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, NullLoggerFactory.Instance); - source.DefaultOptions.Should().Be(options); - - ConfigurationRoot? root = source.Configuration.Should().BeOfType().Subject; - - root.Should().BeSameAs(configurationRoot); } [Fact] @@ -46,7 +38,7 @@ public void Build_ReturnsProvider() var memSource = new MemoryConfigurationSource(); List sources = [memSource]; - var source = new ConfigServerConfigurationSource(options, sources, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, sources, null, null, null, NullLoggerFactory.Instance); IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().BeOfType(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs index 55d238660e..647f657180 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs @@ -11,12 +11,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerDiscoveryServiceTest { [Fact] - public void ConfigServerDiscoveryService_FindsNoDiscoveryClients() + public async Task ConfigServerDiscoveryService_FindsNoDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer).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().BeEmpty(); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs index f0a408d663..8d6f046141 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs @@ -27,7 +27,6 @@ public void Constructor_FindsConfigServerProviderInsidePlaceholderProvider() builder.AddInMemoryCollection(values); builder.AddConfigServer(); builder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); @@ -48,57 +47,12 @@ public void FindProvider_FindsProvider() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); contributor.Provider.Should().NotBeNull(); } - [Fact] - public void GetTimeToLive_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:timeToLive"] = "100000", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.GetTimeToLive().Should().Be(100_000); - } - - [Fact] - public void IsEnabled_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:enabled"] = "true", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsEnabled().Should().BeTrue(); - } - [Fact] public void IsCacheStale_ReturnsExpected() { @@ -115,15 +69,16 @@ public void IsCacheStale_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsCacheStale(0).Should().BeTrue(); // No cache established yet + ConfigServerClientOptions optionsSnapshot = contributor.Provider!.ClientOptions; + + contributor.IsCacheStale(0, optionsSnapshot).Should().BeTrue(); // No cache established yet contributor.Cached = new ConfigEnvironment(); contributor.LastAccess = 9; - contributor.IsCacheStale(10).Should().BeTrue(); - contributor.IsCacheStale(8).Should().BeFalse(); + contributor.IsCacheStale(10, optionsSnapshot).Should().BeTrue(); + contributor.IsCacheStale(8, optionsSnapshot).Should().BeFalse(); } [Fact] @@ -143,7 +98,6 @@ public async Task GetPropertySources_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance) @@ -152,7 +106,9 @@ public async Task GetPropertySources_ReturnsExpected() }; long lastAccess = contributor.LastAccess = DateTimeOffset.Now.ToUnixTimeMilliseconds() - 100; - IList? sources = await contributor.GetPropertySourcesAsync(contributor.Provider!, TestContext.Current.CancellationToken); + + IList? sources = + await contributor.GetPropertySourcesAsync(contributor.Provider!, contributor.Provider!.ClientOptions, TestContext.Current.CancellationToken); contributor.LastAccess.Should().NotBe(lastAccess); sources.Should().BeNull(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs index 776dcc8631..ad06440291 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs @@ -102,8 +102,8 @@ public void AddConfigServer_HostBuilder_DisposesTimer() provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -114,20 +114,20 @@ public void AddConfigServer_WebHostBuilder_DisposesTimer() ["spring:cloud:config:pollingInterval"] = 1.Seconds().ToString() }; - WebHostBuilder builder = TestWebHostBuilderFactory.Create(); - builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); - builder.AddConfigServer(); + WebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); + hostBuilder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); + hostBuilder.AddConfigServer(); ConfigServerConfigurationProvider provider; - using (IWebHost webHost = builder.Build()) + using (IWebHost webHost = hostBuilder.Build()) { var configurationRoot = (IConfigurationRoot)webHost.Services.GetRequiredService(); provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -150,8 +150,8 @@ public async Task AddConfigServer_WebApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -174,8 +174,8 @@ public void AddConfigServer_HostApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs index ac140148b3..11de4911de 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs @@ -18,7 +18,9 @@ public async Task ServiceConstructsAndOperatesWithConfigurationRoot() var provider = new ConfigServerConfigurationProvider(new ConfigServerClientOptions { Enabled = false - }, null, null, NullLoggerFactory.Instance); + }, null, null, null, NullLoggerFactory.Instance); + + provider.Load(); var configurationRoot = new ConfigurationRoot([provider]); var service = new ConfigServerHostedService(configurationRoot, []); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs index 05fe12d3a4..9bf40b414a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs @@ -2,10 +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.Reflection; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Placeholder; @@ -13,24 +10,6 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerServiceCollectionExtensionsTest { - [Fact] - public async Task ConfigureConfigServerClientOptions_ConfiguresConfigServerClientOptions_WithDefaults() - { - var builder = new ConfigurationBuilder(); - builder.AddConfigServer(); - IConfiguration configuration = builder.Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); - - await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); - var optionsMonitor = serviceProvider.GetRequiredService>(); - - string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); - } - [Fact] public void DoesNotAddConfigServerMultipleTimes() { diff --git a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs b/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs deleted file mode 100644 index 27100c24a7..0000000000 --- a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.ConfigServer.Test; - -internal sealed class ForwardingHttpClientHandler : HttpClientHandler -{ - private readonly HttpMessageInvoker _invoker; - - public ForwardingHttpClientHandler(HttpMessageHandler innerHandler) - { - ArgumentNullException.ThrowIfNull(innerHandler); - - _invoker = new HttpMessageInvoker(innerHandler, false); - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return _invoker.SendAsync(request, cancellationToken); - } -} diff --git a/src/Configuration/test/ConfigServer.Test/HttpRequestInfo.cs b/src/Configuration/test/ConfigServer.Test/HttpRequestInfo.cs deleted file mode 100644 index 7137ab3bdf..0000000000 --- a/src/Configuration/test/ConfigServer.Test/HttpRequestInfo.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Http; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -public sealed class HttpRequestInfo -{ - public string Method { get; } - public HostString Host { get; } - public PathString Path { get; } - public QueryString QueryString { get; } - public Stream Body { get; } = new MemoryStream(); - - private HttpRequestInfo(HttpRequest request) - { - Method = request.Method; - Host = request.Host; - Path = request.Path; - QueryString = request.QueryString; - } - - public static async Task CopyFromAsync(HttpContext httpContext) - { - var info = new HttpRequestInfo(httpContext.Request); - - await httpContext.Request.Body.CopyToAsync(info.Body, httpContext.RequestAborted); - info.Body.Seek(0, SeekOrigin.Begin); - - return info; - } -} diff --git a/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj b/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj index 972ea07add..be6dd615fb 100644 --- a/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj +++ b/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs deleted file mode 100644 index 23c99470bf..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class TestConfigServerConfigurationSource(IConfigurationProvider provider) : IConfigurationSource -{ - private readonly IConfigurationProvider _provider = provider; - - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return _provider; - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs deleted file mode 100644 index e3947d75d1..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs +++ /dev/null @@ -1,129 +0,0 @@ -// 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.Http; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class TestConfigServerStartup : IDisposable -{ - private volatile CountdownEvent _firstRequestCountdownEvent = new(1); - private volatile string? _response; - private volatile int[] _returnStatus = [200]; - private volatile HttpRequestInfo? _lastRequest; - private volatile int _requestCount; - private volatile string _label = string.Empty; - private volatile string _appName = string.Empty; - private volatile string _env = string.Empty; - - public string? Response - { - get => _response; - set => _response = value; - } - - public int[] ReturnStatus - { - get => _returnStatus; - set => _returnStatus = value; - } - - public HttpRequestInfo? LastRequest - { - get => _lastRequest; - set => _lastRequest = value; - } - - public int RequestCount - { - get => _requestCount; - set => _requestCount = value; - } - - public string Label - { - get => _label; - set => _label = value; - } - - public string AppName - { - get => _appName; - set => _appName = value; - } - - public string Env - { - get => _env; - set => _env = value; - } - - public void Reset() - { - Response = null; - ReturnStatus = [200]; - LastRequest = null; - RequestCount = 0; - Label = AppName = Env = string.Empty; - - _firstRequestCountdownEvent.Dispose(); - _firstRequestCountdownEvent = new CountdownEvent(1); - } - - public void Configure(IApplicationBuilder app) - { - app.Run(async context => - { - LastRequest = await HttpRequestInfo.CopyFromAsync(context); - context.Response.StatusCode = GetStatusCode(context.Request.Path); - RequestCount++; - - if (context.Response.StatusCode == 200) - { - context.Response.Headers.Append("content-type", "application/json"); - - if (Response != null) - { - await context.Response.WriteAsync(Response, context.RequestAborted); - } - } - - if (RequestCount == 1) - { - _firstRequestCountdownEvent.Signal(); - } - }); - } - - private int GetStatusCode(string path) - { - if (!string.IsNullOrEmpty(Label) && !path.Contains(Label, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(Env) && !path.Contains(Env, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(AppName) && !path.Contains(AppName, StringComparison.Ordinal)) - { - return 404; - } - - return ReturnStatus[RequestCount]; - } - - public bool WaitForFirstRequest(TimeSpan timeout) - { - return _firstRequestCountdownEvent.Wait(timeout, TestContext.Current.CancellationToken); - } - - public void Dispose() - { - _firstRequestCountdownEvent.Dispose(); - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestHelper.cs b/src/Configuration/test/ConfigServer.Test/TestHelper.cs index 57db7c8e6f..6fd0ff93ac 100644 --- a/src/Configuration/test/ConfigServer.Test/TestHelper.cs +++ b/src/Configuration/test/ConfigServer.Test/TestHelper.cs @@ -6,12 +6,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; internal static class TestHelper { - public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName) + public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName, string? expectedEnvironment) { options.Enabled.Should().BeTrue(); options.FailFast.Should().BeFalse(); options.Uri.Should().Be("http://localhost:8888"); - options.Environment.Should().Be("Production"); + options.Environment.Should().Be(expectedEnvironment); options.AccessTokenUri.Should().BeNull(); options.ClientId.Should().BeNull(); options.ClientSecret.Should().BeNull(); diff --git a/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs new file mode 100644 index 0000000000..91167ccba5 --- /dev/null +++ b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs @@ -0,0 +1,20 @@ +// 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.Discovery; + +namespace Steeltoe.Configuration.ConfigServer.Test; + +internal sealed class TestServiceInstance(string serviceId, string instanceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance +{ + public string ServiceId { get; } = serviceId; + public string InstanceId { get; } = instanceId; + public string Host { get; } = uri.Host; + public int Port { get; } = uri.Port; + public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; + public Uri Uri { get; } = uri; + public Uri? NonSecureUri => IsSecure ? null : Uri; + public Uri? SecureUri => IsSecure ? Uri : null; + public IReadOnlyDictionary Metadata { get; } = metadata; +} diff --git a/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs index 87cd0e1dc0..dde1be7826 100644 --- a/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs +++ b/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs @@ -23,11 +23,13 @@ public static TheoryData GetTestVector() List<(string Salt, string Key, string Cipher, string PlainText)> data = [ ("deadbeef", "12345678901234567890", "23f97efeed4ab62294432e8ef6b2905e336c245ecb1d5122b2c288c4deeae1b737952312e97e2cf013dd31a28fc60704", - "encrypt the world"), // from Spring Cloud Config documentation + "encrypt the world"), ("deadbeef", "foo", "682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda", "mysecret"), ("deadbeef", "12345678901234567890", "e31b13ab248f96f3cc22be5942d9ebec19a6b50318b2f5d30ea515064971bdebff6974890197626f0dcd5b648950e96f", "encrypt the world"), ("deadbeef", "12345678901234567890", "e401ca0578839c9e5207f52d0ae4dc836f8c6530cdc90f14b544180f6fdb9265b80d6ace9fbbab700c7af32141171358", + "encrypt the world"), + ("nohexsaltvalue", "12345678901234567890", "000102030405060708090a0b0c0d0e0f31fb8ea24a48e4ffed43352dfacfc9b89cd5ff630715fb4ffeb536c02111dd53", "encrypt the world") ]; diff --git a/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs index 17e562fa85..248fb57cc2 100644 --- a/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs +++ b/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.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.Text; using Steeltoe.Configuration.Encryption.Cryptography; namespace Steeltoe.Configuration.Encryption.Test.Cryptography; @@ -42,8 +43,8 @@ public void Constructor_WithUnsupportedAlgorithmThrows() } [Theory] - [MemberData(nameof(GetTestVector))] - public void Decode_TestForSpringConfigCipher_WithDefaultKey(string salt, string strong, string algorithm, string cipher, string plainText) + [MemberData(nameof(GetSpringConfigServerTestVectors))] + public void Decrypt_WithSpringCipherText_UsingDefaultKeyAlias(string salt, string strong, string algorithm, string cipher, string plainText) { var decryptor = new RsaKeyStoreDecryptor(_keyProvider, "mytestkey", salt, bool.Parse(strong), algorithm); string decrypted = decryptor.Decrypt(cipher); @@ -52,8 +53,8 @@ public void Decode_TestForSpringConfigCipher_WithDefaultKey(string salt, string } [Theory] - [MemberData(nameof(GetTestVector))] - public void Decode_TestForSpringConfigCipher_WithSpecifiedKey(string salt, string strong, string algorithm, string cipher, string plainText) + [MemberData(nameof(GetSpringConfigServerTestVectors))] + public void Decrypt_WithSpringCipherText_UsingExplicitKeyAlias(string salt, string strong, string algorithm, string cipher, string plainText) { var decryptor = new RsaKeyStoreDecryptor(_keyProvider, "someKey", salt, bool.Parse(strong), algorithm); string decrypted = decryptor.Decrypt(cipher, "mytestkey"); @@ -61,24 +62,42 @@ public void Decode_TestForSpringConfigCipher_WithSpecifiedKey(string salt, strin decrypted.Should().Be(plainText); } - public static TheoryData GetTestVector() + // Requires Config Server to be running with OAEP encryption configured (see docker-compose.yml at the repo root) + [Fact] + [Trait("Category", "Integration")] + public async Task Decrypt_WithOaepAlgorithm_CanDecryptSpringConfigServerCipherText() + { + // ReSharper disable once ShortLivedHttpClient + using var httpClient = new HttpClient(); + + HttpResponseMessage response = await httpClient.PostAsync(new Uri("http://localhost:8888/encrypt"), + new StringContent("encrypt the world", Encoding.UTF8, "text/plain"), TestContext.Current.CancellationToken); + + response.EnsureSuccessStatusCode(); + string springCipherText = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + var decryptor = new RsaKeyStoreDecryptor(_keyProvider, "mytestkey", "deadbeef", false, "OAEP"); + string decrypted = decryptor.Decrypt(springCipherText); + + decrypted.Should().Be("encrypt the world"); + } + + // Pre-generated ciphertext is from Spring Cloud Config Server (steeltoe.azurecr.io/config-server:4.3.1) + public static TheoryData GetSpringConfigServerTestVectors() { List<(string Salt, string Strong, string Algorithm, string Cipher, string PlainText)> data = [ ("deadbeef", "false", "OAEP", - "AQATBPXCmri0MCEoCam0noXJgKGlFfE/chVN7XhH1V23MqJ8sI3lI61PyvsryJP3LlfNn38gUuulMeslAs/gUCoPFPV/zD7M8x527wQUbmWD6bR0ZMJ4hu3DisK6Diw2YAOxXSsm3Zh46cPFQcowfOG1x2OXj+5uL4T+VBGdt3Nr6dHCOumkTJ1KAtaJMfASf3J8G4M27v6m4Y2EdBqP1zWwDhAZ3R0u9uTP9xYUqQiKsUeOixrhOaCvtb1Q+Zg6A41CxM4cjL3Ty6miNYLx3QkxRvfkdo0iqo7jTrWWAT1aeRV6t5U5iMlWnD4eXzad60E3ZSINhvDiB03xPPPuHKC6qUTRJEEbQFegmn/KIPMMn9WaH/JLLZNvQYMuaFszZ84AE3aQcH0be+sNFDSjHNHL", - "encrypt the world"), - ("deadbeef", "false", "OAEP", - "AQBoZM07gyw+GN0SXCkARLiSDjhN0flk07QP9+BsNnPEQD+alfH6A5FJwwuEf7d/kNJozppaZuHcPpDnRZbzmsRcqOcO0BiJFjsbX5K9o8jcAsGhDmLAf0jy/Ry1de6bELjZ4MPArbVN9numHTre4plXBXun2AVeNNBYG3yHed0A68o6FCc6UR/Pfdo/H+oTburn2qVKaZL+DAqIKHntcZjTLg/ZRa7MKUMCKiFEtV88U3lg+1YUqgz+XUmg2zyUsHgHNzYlTOtJWkFW51wNz/M2C92Zsu4R6bF1ewb2RM0N8VmjQAw6GpfLNX+CB3gGlDPsfGjc9qiF3zNsJSk88dm1+NruXeon5Nth691NQJ6DpgMXhhFzv7L/eyZKL/kZpGIVZK6dW3iePzsBtuFdrjiZ", + "AQA5BZk2Pg7/nbcuTrJ/i4MDOIc831GfHLUg1GQlBtOvRJm2iXngfbPKcnTjbtZZ9X+qPnbdkUcTVbgYcsszY3uoqWIN5Yybwg+dHsqZTv1/XSvwR/kwflDg+I6C+dxg3GepoCAZSPi+J5/MsfCYJAp6KI3WW34tbqkqNlJq1TmF4b/AQHmP7Pth6cIsFE7svQ0xDQRhY61lJESLvLZ1Em4XpA4cfiye1YQhNud7/AKTtyENf7oPT/7siWBN82gyCB63/HEMRNtSLobOKO1XzgWNc96ms4pIhACOA3cZarTDUqaKY+B84ATV5QKgfkQ/ihI6r2oeYB24ApKwjNyE4F4b0bFH1cchdsbooreJlflgn0U9gK0oo8t7cVGnih3lccJs3t0uAFVm+SrJGMG/8rgp", "encrypt the world"), ("beefdead", "true", "OAEP", - "AQAbWqohCeQ+TTqyJ3ZlNvAtx5cC2I3PmJetuSR82yRRyX+wWd7mTkUXuN/wANJ+nr1ySdzPudjml1lHaxZn42I9szkIKSkNT+6Yg+zNaREMetcE5SXA1awtSbEaFY2NcualSzPVWs8ulsUkKlYyyh6XP9gT/kODbmX0mS6DCtxalJgjei7WujLaJaPjc3jk+EhV9M1TovexqI7XoLlsgrGf6/1gQE+SSOamTFJopWpYEeSpSEwY2dXZfct5KCFWGJVA7eDPRJk0dT6EWIvqd6J4YoMWonxgVy4nG/Gq0NTisXv9XpJHAPYBg0c8B0WrWi2PG/Q00wvFRqGmYQ1hQIVmbJm8z+f0WoCxKwnCZvvdLlgrx2qeK1S21dPdgtmLXlj5bRUrektFrNhlevlENW7wgg==", + "AQAwoGhHV/Z82UWIrmqmTT92L510iKkwiF+EhlroV/No3dLwamUovEB9n/4IF+j6wfv8q1Baqekn5y6folcQmiMJd86JHW2n+WNeKUlbjf3Rk5uwgSTL2ST1JZ8w6sZ0PZVE2tqaQoc9mHRmjT7hqRm1lQVsHsic5tCxdTmhYVdGp5J6UGTRPQNfyJBR34w+LFjtgyaOrF//o8Z5ZF9XUx3MGaoe4HnURIYRq5HHcd4yVFINaBpW19ndgPV+nWRANxnmltLgPUbLWBJSvJ8czHOfZvZnTSJrWDBp1GIHN0OFkObJAIl7hmOdCh3vFPkxOL9gH4690VlMOCWYI8elsvuFsOdPG++FtJVSbuGgYC3AnuFo955yBfy8tgdegQ5Zzb2sOS2mwqsWr4mly4Pis+bgpw==", + "encrypt the world"), + ("deadbeef", "false", "DEFAULT", + "AQBSAVVzUP21aZXVAnuTMBDQ+/HGatD/+6H2YT/EbVofx5pWNJIjOlq1ioDpLHRB/JS86nI5oC9scEVBajc/gcWiYJAOtG4+g0Sw2ixzmi3jmho/CYxhtbxGFrkrTOC0r/0I6gcGgCo5ZrQCtaQDUMnHn+aFwo8baduKQ2N6qMyGHvfXIqqJFabnTkYDlLlqgNa3jpI3oKicaDTvPU3jFO42fJyVFWyAdQ8YS0RZdOXV+0xQdRnHrHHjhR8W7D7e0Jyx05RKq1ZEXvN7+x+YSE7ajrwy8riGuxR9a8smZAKkXC8T7KcZMRqtkd/9bpNS10bpw21KSxxp2GF52ekbu0xZYIIPdIj57me7HGubwNd1kXXgV+3L6sZ1IUAN0xnOOEUQD3z6hOWkrTEAmSbNRdYM", "encrypt the world"), ("beefdead", "true", "DEFAULT", "AQAhwKArLZqxrc44G2sG6+EwWeqn9JytaIyBpf/Yz2UZ0bLZthR3HPtGgOoKY9AkWpBuRzrw3zQ20ZRkq6q7XU+Stp1kB4OXhrmgbwydNUtYJmuTlpGohtHH8wVoT2n0bd7NuL9rJ2OAbkPXg8K1kJMSgen7Hyg3b+LFZmaA8wCHXdmHuP63Rk4NhSec4Ul/gRRn5jftojmbxVVQ6xRGAeFTZi70oAZ+tzdyXZmukorRZsUtnlgj94aSmGdMCGkukanCiEHHrh130Nigxba4qZ2F2e5n46De7+7EVwnIWWYa2sQH+3BQ+cp5OCebWMiGPdylqZzyTagkwo2jHv/OzW0/ytIF1Qo3AblMQgympSL3/PMPggllopaf2al4o7w63vWczXdv6YzdLchQMrdXRdkLrw==", - "encrypt the world"), - ("nohexsaltvalue", "true", "DEFAULT", - "AQA+sdMQ94WuW7DMBX7ZJQeWaybtFWJqAeVv9kmHyVCwil3yobQPXMxuoF/FGpZgYQu+9JyK52jnuIXiARdyqqaDKxY7ECN/8GLVXdcQi5ooO+ewyOrL53fycyyB2iQtZphbdgmzU2qKQkXvFcWQkauHCCtni6IemITLX/y9O3I6Ss9LEK86lSAWKD1Tikf9ly78vJsCJ01ahQhEQVMbkpTixnnFRgqSL7XZo+2FGMvsyYKHp9pQwEnLkbehI8AFODQlFsTcQ9YYab5lGa4OoYw+5oS3fFH8XlIvVSTfxipI18iyphppz3EefvuGd8FwgSGCbfIeQ2R2zcYxykfWgCgSH5ckev2EqeLaiyaK3tXFanumQBeLiSg7Uii80jg9LLJ62jyrR16m0+8CGqaw6uzZkQ==", "encrypt the world") ]; diff --git a/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs b/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs index efb06a3888..9ca35f7e87 100644 --- a/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs +++ b/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs @@ -85,6 +85,34 @@ public void Can_resolve_placeholder_to_decrypted_value() configuration["result"].Should().Be("start-SECRET-end"); } + [Fact] + public void Decrypted_values_in_placeholder_resolution_are_only_logged_at_trace_level() + { + var decryptor = new ToUpperCaseDecryptor(); + using var capturingProvider = new CapturingLoggerProvider(); + using var loggerFactory = new LoggerFactory([capturingProvider]); + + var appSettings = new Dictionary + { + ["secret"] = "{cipher}classified", + ["greeting"] = "hello-${secret}-world" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddDecryption(decryptor, loggerFactory); + builder.AddPlaceholderResolver(loggerFactory); + IConfiguration configuration = builder.Build(); + + _ = configuration["greeting"]; + + IList logLines = capturingProvider.GetAll(); + string[] sensitiveLines = [.. logLines.Where(message => message.Contains("CLASSIFIED", StringComparison.Ordinal))]; + + sensitiveLines.Should().NotBeEmpty(); + sensitiveLines.Should().AllSatisfy(message => message.Should().StartWith("TRCE")); + } + public void Dispose() { _loggerFactory.Dispose(); diff --git a/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj b/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj index 9e238c989c..93d3d367de 100644 --- a/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj +++ b/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Configuration/test/Kubernetes.ServiceBindings.Test/KubernetesServiceBindingConfigurationProviderTest.cs b/src/Configuration/test/Kubernetes.ServiceBindings.Test/KubernetesServiceBindingConfigurationProviderTest.cs index 5fef604467..7b92c1f5ca 100644 --- a/src/Configuration/test/Kubernetes.ServiceBindings.Test/KubernetesServiceBindingConfigurationProviderTest.cs +++ b/src/Configuration/test/Kubernetes.ServiceBindings.Test/KubernetesServiceBindingConfigurationProviderTest.cs @@ -123,7 +123,7 @@ private static string GetEmptyK8SResourcesDirectory() private sealed class TestPostProcessor : IConfigurationPostProcessor { - public bool PostProcessorCalled { get; set; } + public bool PostProcessorCalled { get; private set; } public void PostProcessConfiguration(PostProcessorConfigurationProvider provider, IDictionary configurationData) { diff --git a/src/Configuration/test/Kubernetes.ServiceBindings.Test/Steeltoe.Configuration.Kubernetes.ServiceBindings.Test.csproj b/src/Configuration/test/Kubernetes.ServiceBindings.Test/Steeltoe.Configuration.Kubernetes.ServiceBindings.Test.csproj index 5bfd14b7d6..ff3cd2f418 100644 --- a/src/Configuration/test/Kubernetes.ServiceBindings.Test/Steeltoe.Configuration.Kubernetes.ServiceBindings.Test.csproj +++ b/src/Configuration/test/Kubernetes.ServiceBindings.Test/Steeltoe.Configuration.Kubernetes.ServiceBindings.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs index c8f73a2c1f..f9f74b68a2 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs @@ -205,7 +205,8 @@ public void Substitutes_placeholders_in_key_lookups() ["key2"] = "${key1?not-found}", ["key3"] = "${no-key?not-found}", ["key4"] = "${no-key}", - ["key5"] = string.Empty + ["key5"] = string.Empty, + ["key6"] = null }; var builder = new ConfigurationBuilder(); @@ -219,6 +220,8 @@ public void Substitutes_placeholders_in_key_lookups() configuration["key3"].Should().Be("not-found"); configuration["key4"].Should().Be("${no-key}"); configuration["key5"].Should().BeEmpty(); + configuration["key6"].Should().BeNull(); + configuration["key7"].Should().BeNull(); configuration["no-key"] = "new-key-value"; @@ -333,16 +336,16 @@ public void Reloads_options_on_change(int placeholderCount) { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueA" - } - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueA" + } + } + """); var builder = new ConfigurationBuilder(); - builder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); #pragma warning disable SA1312 // Variable names should begin with lower-case letter foreach (int _ in Enumerable.Repeat(0, placeholderCount)) @@ -372,13 +375,13 @@ public void Reloads_options_on_change(int placeholderCount) _ = optionsMonitor.CurrentValue; configurer.ConfigureCount.Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueB" - } - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueB" + } + } + """); fileProvider.NotifyChanged(); @@ -408,6 +411,46 @@ static void AssertTypesInSourceTree(IList sources, int ind } } + [Fact] + public void Binding_property_against_null_overwrites_default_value() + { + var appSettings = new Dictionary + { + ["Root:TestOptions:Value"] = null + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(); + IConfigurationRoot configuration = builder.Build(); + + IConfigurationSection section = configuration.GetSection("Root:TestOptions"); + var options = section.Get(); + + options.Should().NotBeNull(); + options.Value.Should().BeNull(); + } + + [Fact] + public void Binding_property_against_empty_string_overwrites_default_value() + { + var appSettings = new Dictionary + { + ["Root:TestOptions:Value"] = string.Empty + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(); + IConfigurationRoot configuration = builder.Build(); + + IConfigurationSection section = configuration.GetSection("Root:TestOptions"); + var options = section.Get(); + + options.Should().NotBeNull(); + options.Value.Should().BeEmpty(); + } + public void Dispose() { _loggerFactory.Dispose(); diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs index 544688e850..3dcb81fb43 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs @@ -2,14 +2,12 @@ // 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 FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Configuration.Placeholder.Test; @@ -26,16 +24,15 @@ public PlaceholderWebApplicationTest(ITestOutputHelper testOutputHelper) [Fact] public async Task Reloads_options_on_change() { - const string appSettings = """ + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" { "TestRoot": { "AppName": "AppOne" } } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + """); var memorySettings = new Dictionary { @@ -44,9 +41,8 @@ public async Task Reloads_options_on_change() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Services.AddSingleton(_loggerFactory); - builder.Configuration.SetBasePath(sandbox.FullPath); builder.Configuration.AddInMemoryCollection(memorySettings); - builder.Configuration.AddJsonFile(MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Configuration.AddPlaceholderResolver(_loggerFactory); builder.Services.Configure(builder.Configuration.GetSection("TestRoot")); builder.Services.AddSingleton, ConfigureTestOptions>(); @@ -55,27 +51,27 @@ public async Task Reloads_options_on_change() var optionsMonitor = app.Services.GetRequiredService>(); optionsMonitor.CurrentValue.Value.Should().Be("AppOne"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppTwo" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppTwo" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppTwo"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppThree" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppThree" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppThree"); } @@ -121,11 +117,9 @@ public void Can_rebuild_sources() [Fact] public async Task Can_substitute_across_multiple_sources() { - const string appSettingsJsonFileName = "appsettings.json"; - const string appSettingsXmlFileName = "appsettings.xml"; - const string appSettingsIniFileName = "appsettings.ini"; + var fileProvider = new MemoryFileProvider(); - const string appSettingsJsonContent = """ + fileProvider.IncludeAppSettingsJsonFile(""" { "JsonTestRoot": { "JsonSubLevel": { @@ -134,9 +128,9 @@ public async Task Can_substitute_across_multiple_sources() } } } - """; + """); - const string appSettingsXmlContent = """ + fileProvider.IncludeAppSettingsXmlFile(""" @@ -145,13 +139,13 @@ public async Task Can_substitute_across_multiple_sources() - """; + """); - const string appSettingsIniContent = """ + fileProvider.IncludeAppSettingsIniFile(""" [IniTestRoot:IniSubLevel] IniKey=IniValue CmdSource=IniTo${CmdTestRoot:CmdSubLevel:CmdKey} - """; + """); string[] appSettingsCommandLine = [ @@ -159,16 +153,10 @@ public async Task Can_substitute_across_multiple_sources() "--CmdTestRoot:CmdSubLevel:JsonSource=CmdTo${JsonTestRoot:JsonSubLevel:JsonKey}" ]; - using var sandbox = new Sandbox(); - sandbox.CreateFile(appSettingsJsonFileName, appSettingsJsonContent); - sandbox.CreateFile(appSettingsXmlFileName, appSettingsXmlContent); - sandbox.CreateFile(appSettingsIniFileName, appSettingsIniContent); - WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.SetBasePath(sandbox.FullPath); - builder.Configuration.AddJsonFile(appSettingsJsonFileName); - builder.Configuration.AddXmlFile(appSettingsXmlFileName); - builder.Configuration.AddIniFile(appSettingsIniFileName); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsXmlFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsIniFile(fileProvider); builder.Configuration.AddCommandLine(appSettingsCommandLine); builder.Configuration.AddPlaceholderResolver(_loggerFactory); diff --git a/src/Configuration/test/Placeholder.Test/Steeltoe.Configuration.Placeholder.Test.csproj b/src/Configuration/test/Placeholder.Test/Steeltoe.Configuration.Placeholder.Test.csproj index 77b1d43a97..9226d06016 100644 --- a/src/Configuration/test/Placeholder.Test/Steeltoe.Configuration.Placeholder.Test.csproj +++ b/src/Configuration/test/Placeholder.Test/Steeltoe.Configuration.Placeholder.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs index ca6ce53fb3..572dccca30 100644 --- a/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs +++ b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs @@ -25,5 +25,5 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } [LoggerMessage(Level = LogLevel.Trace, Message = "Build ({Name}).")] - private static partial void LogBuild(ILogger logger, string name); + private static partial void LogBuild(ILogger logger, string name); } diff --git a/src/Configuration/test/Placeholder.Test/TestOptions.cs b/src/Configuration/test/Placeholder.Test/TestOptions.cs index bbbeca4572..7600520855 100644 --- a/src/Configuration/test/Placeholder.Test/TestOptions.cs +++ b/src/Configuration/test/Placeholder.Test/TestOptions.cs @@ -6,5 +6,5 @@ namespace Steeltoe.Configuration.Placeholder.Test; internal sealed class TestOptions { - public string? Value { get; set; } + public string? Value { get; set; } = "DefaultValue"; } diff --git a/src/Configuration/test/RandomValue.Test/Steeltoe.Configuration.RandomValue.Test.csproj b/src/Configuration/test/RandomValue.Test/Steeltoe.Configuration.RandomValue.Test.csproj index 4b782da24a..102dbdb165 100644 --- a/src/Configuration/test/RandomValue.Test/Steeltoe.Configuration.RandomValue.Test.csproj +++ b/src/Configuration/test/RandomValue.Test/Steeltoe.Configuration.RandomValue.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Configuration/test/SpringBoot.Test/SpringBootEnvironmentVariableProviderTest.cs b/src/Configuration/test/SpringBoot.Test/SpringBootEnvironmentVariableProviderTest.cs index ac78944af9..739a75f992 100644 --- a/src/Configuration/test/SpringBoot.Test/SpringBootEnvironmentVariableProviderTest.cs +++ b/src/Configuration/test/SpringBoot.Test/SpringBootEnvironmentVariableProviderTest.cs @@ -63,14 +63,16 @@ public void TryGet_Tree() value.Should().Be("q"); provider.TryGet("r", out value).Should().BeTrue(); - value.Should().BeEmpty(); + value.Should().BeNull(); provider.TryGet("s:t", out value).Should().BeTrue(); - value.Should().BeEmpty(); + value.Should().BeNull(); - provider.TryGet("u", out _).Should().BeFalse(); + provider.TryGet("u", out value).Should().BeTrue(); + value.Should().BeNull(); - provider.TryGet("v:w", out _).Should().BeFalse(); + provider.TryGet("v:w", out value).Should().BeTrue(); + value.Should().BeNull(); } [Fact] @@ -113,14 +115,16 @@ public void TryGet_Array() value.Should().Be("q"); provider.TryGet("a:b:c:2:r", out value).Should().BeTrue(); - value.Should().BeEmpty(); + value.Should().BeNull(); provider.TryGet("a:b:c:2:s:t", out value).Should().BeTrue(); - value.Should().BeEmpty(); + value.Should().BeNull(); - provider.TryGet("a:b:c:2:u", out _).Should().BeFalse(); + provider.TryGet("a:b:c:2:u", out value).Should().BeTrue(); + value.Should().BeNull(); - provider.TryGet("a:b:c:2:v:w", out _).Should().BeFalse(); + provider.TryGet("a:b:c:2:v:w", out value).Should().BeTrue(); + value.Should().BeNull(); } [Fact] diff --git a/src/Configuration/test/SpringBoot.Test/Steeltoe.Configuration.SpringBoot.Test.csproj b/src/Configuration/test/SpringBoot.Test/Steeltoe.Configuration.SpringBoot.Test.csproj index a9357e6088..0961801249 100644 --- a/src/Configuration/test/SpringBoot.Test/Steeltoe.Configuration.SpringBoot.Test.csproj +++ b/src/Configuration/test/SpringBoot.Test/Steeltoe.Configuration.SpringBoot.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Connectors/src/Connectors/ConnectionStringPostProcessor.cs b/src/Connectors/src/Connectors/ConnectionStringPostProcessor.cs index 964d867fa1..09a0e4eb08 100644 --- a/src/Connectors/src/Connectors/ConnectionStringPostProcessor.cs +++ b/src/Connectors/src/Connectors/ConnectionStringPostProcessor.cs @@ -15,7 +15,7 @@ namespace Steeltoe.Connectors; internal abstract class ConnectionStringPostProcessor : IConfigurationPostProcessor { private const string ConnectionStringName = "ConnectionString"; - public const string DefaultBindingName = "Default"; + private const string DefaultBindingName = "Default"; private static readonly string ClientBindingsConfigurationKey = ConfigurationPath.Combine("Steeltoe", "Client"); public static readonly string ServiceBindingsConfigurationKey = ConfigurationPath.Combine("steeltoe", "service-bindings"); @@ -37,7 +37,7 @@ public void PostProcessConfiguration(PostProcessorConfigurationProvider provider { bindingsByName.TryGetValue(DefaultBindingName, out BindingInfo? defaultBinding); - string? alternateBindingName = bindingsByName.Keys.SingleOrDefault(bindingName => bindingName != DefaultBindingName); + string? alternateBindingName = bindingsByName.Keys.SingleOrDefault(bindingName => !IsDefaultBindingName(bindingName)); BindingInfo? alternateBinding = alternateBindingName == null ? null : bindingsByName[alternateBindingName]; var bindingInfo = new BindingInfo @@ -55,20 +55,25 @@ public void PostProcessConfiguration(PostProcessorConfigurationProvider provider } else { - foreach ((string bindingName, BindingInfo bindingInfo) in bindingsByName.Where(binding => binding.Key != DefaultBindingName)) + foreach ((string bindingName, BindingInfo bindingInfo) in bindingsByName.Where(binding => !IsDefaultBindingName(binding.Key))) { SetConnectionString(configurationData, bindingName, bindingInfo); } } } + public static bool IsDefaultBindingName(string bindingName) + { + return string.Equals(bindingName, DefaultBindingName, StringComparison.OrdinalIgnoreCase); + } + private static bool ShouldSetDefault(Dictionary bindingsByName) { if (bindingsByName.Count == 1) { (string bindingName, BindingInfo binding) = bindingsByName.Single(); - if (bindingName == DefaultBindingName && binding.IsClientOnly) + if (IsDefaultBindingName(bindingName) && binding.IsClientOnly) { return true; } @@ -78,7 +83,7 @@ private static bool ShouldSetDefault(Dictionary bindingsByN if (bindingsByName.Count == 2 && bindingsByName.TryGetValue(DefaultBindingName, out BindingInfo? defaultBinding) && defaultBinding.IsClientOnly) { - BindingInfo alternateBinding = bindingsByName.Single(binding => binding.Key != DefaultBindingName).Value; + BindingInfo alternateBinding = bindingsByName.Single(binding => !IsDefaultBindingName(binding.Key)).Value; if (alternateBinding.IsServerOnly) { @@ -91,7 +96,7 @@ private static bool ShouldSetDefault(Dictionary bindingsByN private Dictionary GetBindingsByName(IConfiguration configuration) { - Dictionary bindingsByName = []; + var bindingsByName = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (IConfigurationSection clientBinding in GetBindingSections(configuration, ClientBindingsConfigurationKey)) { diff --git a/src/Connectors/src/Connectors/ConnectionStringPostProcessorConfigurationProvider.cs b/src/Connectors/src/Connectors/ConnectionStringPostProcessorConfigurationProvider.cs index abcd3c9ae6..1d4a6c463e 100644 --- a/src/Connectors/src/Connectors/ConnectionStringPostProcessorConfigurationProvider.cs +++ b/src/Connectors/src/Connectors/ConnectionStringPostProcessorConfigurationProvider.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Connectors; -internal sealed class ConnectionStringPostProcessorConfigurationProvider : PostProcessorConfigurationProvider, IDisposable +internal sealed class ConnectionStringPostProcessorConfigurationProvider : PostProcessorConfigurationProvider { private readonly IDisposable? _changeToken; @@ -28,8 +28,9 @@ public override void Load() OnReload(); } - public void Dispose() + public override void Dispose() { _changeToken?.Dispose(); + base.Dispose(); } } diff --git a/src/Connectors/src/Connectors/Connector.cs b/src/Connectors/src/Connectors/Connector.cs index 8ee78b99d0..5d885e8883 100644 --- a/src/Connectors/src/Connectors/Connector.cs +++ b/src/Connectors/src/Connectors/Connector.cs @@ -4,6 +4,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Connectors; @@ -25,7 +32,7 @@ public sealed class Connector : IDisposable private readonly bool _useSingletonConnection; private readonly IOptionsMonitor _optionsMonitor; - private readonly object _singletonLock = new(); + private readonly LockPrimitive _singletonLock = new(); private ConnectionWithOptionsSnapshot? _singletonSnapshot; private bool _singletonIsDisposed; diff --git a/src/Connectors/src/Connectors/ConnectorConfigureOptionsBuilder.cs b/src/Connectors/src/Connectors/ConnectorConfigureOptionsBuilder.cs index 85f4a87b56..ce887382a1 100644 --- a/src/Connectors/src/Connectors/ConnectorConfigureOptionsBuilder.cs +++ b/src/Connectors/src/Connectors/ConnectorConfigureOptionsBuilder.cs @@ -2,10 +2,14 @@ // 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.Configuration.CloudFoundry.ServiceBindings; + namespace Steeltoe.Connectors; public sealed class ConnectorConfigureOptionsBuilder { + internal CloudFoundryServiceBrokerTypes CloudFoundryBrokerTypes { get; set; } + /// /// Gets or sets a value indicating whether connection string changes are detected while the application is running. This is false by default to /// optimize startup performance. When set to true, existing configuration providers may get reloaded multiple times, potentially resulting in @@ -13,4 +17,87 @@ public sealed class ConnectorConfigureOptionsBuilder /// is false. /// public bool DetectConfigurationChanges { get; set; } + + /// + /// Gets or sets a value indicating whether to turn off the built-in service broker support. This is false by default, but should be set to + /// true when using custom logic to convert platform-based credentials to driver-specific configuration keys. + /// + /// + /// For example, to use a third-party Cloud Foundry service broker that sets the + /// + /// VCAP_SERVICES + /// + /// environment variable to: + /// + /// { + /// "custom-postgres-broker": [ + /// { + /// "name": "products-db", + /// "credentials": { + /// "custom-hostname-key": "example.cloud.com", + /// "custom-port-key": 2345, + /// "custom-username-key": "products-user", + /// "custom-password-key": "products-secret", + /// "custom-database-name-key": "product-database" + /// } + /// }, + /// { + /// "name": "orders-db", + /// "credentials": { + /// "custom-hostname-key": "example.cloud.com", + /// "custom-port-key": 2345, + /// "custom-username-key": "orders-user", + /// "custom-password-key": "orders-secret", + /// "custom-database-name-key": "order-database" + /// } + /// } + /// ] + /// } + /// + /// The following code can be used to map the PostgreSQL credentials to the format that + /// + /// NpgsqlConnectionStringBuilder + /// + /// expects: + /// configure.SkipDefaultServiceBindings = true, null); + /// var app = builder.Build(); + /// + /// var factory = app.Services.GetRequiredService>(); + /// + /// PostgreSqlOptions productsDbOptions = factory.Get("products-db").Options; + /// Console.WriteLine(productsDbOptions.ConnectionString); + /// // Database=product-database;Host=example.cloud.com;Password=products-secret;Port=2345;Username=products-user + /// + /// PostgreSqlOptions ordersDbOptions = factory.Get("orders-db").Options; + /// Console.WriteLine(ordersDbOptions.ConnectionString); + /// // Database=order-database;Host=example.cloud.com;Password=orders-secret;Port=2345;Username=orders-user + /// + /// void MapCustomServiceBindings(string brokerName) + /// { + /// var options = builder.Configuration.GetSection("vcap").Get(); + /// + /// foreach (CloudFoundryService service in options?.Services + /// .Where(pair => pair.Key == brokerName) + /// .SelectMany(pair => pair.Value) ?? []) + /// { + /// builder.Configuration.AddInMemoryCollection(new Dictionary + /// { + /// // Map credentials into the property names expected by NpgsqlConnectionStringBuilder. + /// [$"steeltoe:service-bindings:postgresql:{service.Name}:host"] = service.Credentials["custom-hostname-key"].Value, + /// [$"steeltoe:service-bindings:postgresql:{service.Name}:port"] = service.Credentials["custom-port-key"].Value, + /// [$"steeltoe:service-bindings:postgresql:{service.Name}:username"] = service.Credentials["custom-username-key"].Value, + /// [$"steeltoe:service-bindings:postgresql:{service.Name}:password"] = service.Credentials["custom-password-key"].Value, + /// [$"steeltoe:service-bindings:postgresql:{service.Name}:database"] = service.Credentials["custom-database-name-key"].Value + /// }); + /// } + /// } + /// ]]> + /// + /// + /// + public bool SkipDefaultServiceBindings { get; set; } } diff --git a/src/Connectors/src/Connectors/ConnectorConfigurer.cs b/src/Connectors/src/Connectors/ConnectorConfigurer.cs index 2bb05f61b5..5b5c489be1 100644 --- a/src/Connectors/src/Connectors/ConnectorConfigurer.cs +++ b/src/Connectors/src/Connectors/ConnectorConfigurer.cs @@ -3,25 +3,32 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Steeltoe.Configuration; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Configuration.Kubernetes.ServiceBindings; +using IServiceBindingsReader = Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader; namespace Steeltoe.Connectors; internal static class ConnectorConfigurer { - public static void Configure(IConfigurationBuilder builder, Action? configureAction, - TPostProcessor connectionStringPostProcessor) + private static readonly Predicate DefaultIgnoreKeyPredicate = _ => false; + + public static void Configure(IConfigurationBuilder builder, Action configureAction, + TPostProcessor connectionStringPostProcessor, IServiceBindingsReader? serviceBindingsReader, ILoggerFactory loggerFactory) where TPostProcessor : ConnectionStringPostProcessor { if (!IsConfigured(builder)) { var optionsBuilder = new ConnectorConfigureOptionsBuilder(); - configureAction?.Invoke(optionsBuilder); + configureAction.Invoke(optionsBuilder); - builder.AddCloudFoundryServiceBindings(); - builder.AddKubernetesServiceBindings(); + if (!optionsBuilder.SkipDefaultServiceBindings) + { + builder.AddCloudFoundryServiceBindings(DefaultIgnoreKeyPredicate, serviceBindingsReader, optionsBuilder.CloudFoundryBrokerTypes, loggerFactory); + builder.AddKubernetesServiceBindings(); + } RegisterPostProcessor(connectionStringPostProcessor, builder, optionsBuilder.DetectConfigurationChanges); } diff --git a/src/Connectors/src/Connectors/ConnectorOptionsBinder.cs b/src/Connectors/src/Connectors/ConnectorOptionsBinder.cs index a99d5a87c0..71c346611c 100644 --- a/src/Connectors/src/Connectors/ConnectorOptionsBinder.cs +++ b/src/Connectors/src/Connectors/ConnectorOptionsBinder.cs @@ -30,7 +30,7 @@ public static IReadOnlySet RegisterNamedOptions(IServiceCollec { string bindingName = childSection.Key; - if (bindingName == ConnectionStringPostProcessor.DefaultBindingName) + if (ConnectionStringPostProcessor.IsDefaultBindingName(bindingName)) { services.Configure(childSection); @@ -62,7 +62,7 @@ private static bool ContainsNamedServiceBindings(IConfigurationSection[] section return false; } - if (sections is [{ Key: ConnectionStringPostProcessor.DefaultBindingName }]) + if (sections is [{ } singleSection] && ConnectionStringPostProcessor.IsDefaultBindingName(singleSection.Key)) { return false; } @@ -81,6 +81,6 @@ private static void RegisterHealthContributor(IServiceCollection services, strin private static HashSet GetNamedOptions(IConfigurationSection[] childSections) { - return childSections.Select(section => section.Key == ConnectionStringPostProcessor.DefaultBindingName ? string.Empty : section.Key).ToHashSet(); + return childSections.Select(section => ConnectionStringPostProcessor.IsDefaultBindingName(section.Key) ? string.Empty : section.Key).ToHashSet(); } } diff --git a/src/Connectors/src/Connectors/CosmosDb/CosmosDbConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/CosmosDb/CosmosDbConfigurationBuilderExtensions.cs index 2d5ea2513b..24bf87d6a5 100644 --- a/src/Connectors/src/Connectors/CosmosDb/CosmosDbConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/CosmosDb/CosmosDbConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.CosmosDb; @@ -38,7 +40,13 @@ public static IConfigurationBuilder ConfigureCosmosDb(this IConfigurationBuilder { ArgumentNullException.ThrowIfNull(builder); - ConnectorConfigurer.Configure(builder, configureAction, new CosmosDbConnectionStringPostProcessor()); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + options.CloudFoundryBrokerTypes = CloudFoundryServiceBrokerTypes.None; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new CosmosDbConnectionStringPostProcessor(), null, NullLoggerFactory.Instance); return builder; } } diff --git a/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs b/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs index e1b1993a33..ffdcf04cab 100644 --- a/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs +++ b/src/Connectors/src/Connectors/CosmosDb/CosmosDbHealthContributor.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Connectors.CosmosDb; -internal sealed class CosmosDbHealthContributor : IHealthContributor, IDisposable +internal sealed partial class CosmosDbHealthContributor : IHealthContributor, IDisposable { private readonly CosmosClientShimFactory _clientFactory; private readonly ILogger _logger; @@ -40,7 +40,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -64,7 +64,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -75,7 +75,7 @@ public CosmosDbHealthContributor(string serviceName, IServiceProvider servicePro ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -91,6 +91,15 @@ public void Dispose() _cosmosClientShim = null; } + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); + private sealed class CosmosClientShimFactory { private readonly ConnectorShim _connectorShim; diff --git a/src/Connectors/src/Connectors/MongoDb/MongoDbConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/MongoDb/MongoDbConfigurationBuilderExtensions.cs index ef5fd61509..30e6d43a1e 100644 --- a/src/Connectors/src/Connectors/MongoDb/MongoDbConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/MongoDb/MongoDbConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.MongoDb; @@ -35,10 +37,24 @@ public static IConfigurationBuilder ConfigureMongoDb(this IConfigurationBuilder /// The incoming so that additional calls can be chained. /// public static IConfigurationBuilder ConfigureMongoDb(this IConfigurationBuilder builder, Action? configureAction) + { + return ConfigureMongoDb(builder, configureAction, null); + } + + internal static IConfigurationBuilder ConfigureMongoDb(this IConfigurationBuilder builder, Action? configureAction, + IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - ConnectorConfigurer.Configure(builder, configureAction, new MongoDbConnectionStringPostProcessor()); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + options.CloudFoundryBrokerTypes = options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.MongoDb; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new MongoDbConnectionStringPostProcessor(), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/MongoDb/MongoDbConnectionStringBuilder.cs b/src/Connectors/src/Connectors/MongoDb/MongoDbConnectionStringBuilder.cs index f70ac70a79..ec3a8c6c73 100644 --- a/src/Connectors/src/Connectors/MongoDb/MongoDbConnectionStringBuilder.cs +++ b/src/Connectors/src/Connectors/MongoDb/MongoDbConnectionStringBuilder.cs @@ -22,7 +22,7 @@ internal sealed class MongoDbConnectionStringBuilder : IConnectionStringBuilder public string ConnectionString { get => ToConnectionString(); - set => FromConnectionString(value, false); + set => FromConnectionString(value); } /// @@ -37,14 +37,8 @@ public object? this[string keyword] return ConnectionString; } - // Allow getting unknown keyword, if it was set earlier. We don't pretend to know all valid query string parameters. - if (_settings.TryGetValue(keyword, out string? value)) - { - return value; - } - - AssertIsKnownKeyword(keyword); - return null; + // Allow getting unknown keyword. We don't pretend to know all valid query string parameters. + return _settings.GetValueOrDefault(keyword); } set { @@ -52,7 +46,7 @@ public object? this[string keyword] if (string.Equals(keyword, KnownKeywords.Url, StringComparison.OrdinalIgnoreCase)) { - FromConnectionString(value?.ToString(), true); + ConnectionString = value?.ToString(); } else { @@ -106,9 +100,9 @@ private string ToConnectionString() var queryString = default(QueryString); - foreach ((string keyword, string value) in _settings.Where(pair => !KnownKeywords.Exists(pair.Key))) + foreach ((string name, string value) in _settings.Where(pair => !KnownKeywords.Exists(pair.Key))) { - queryString = queryString.Add(keyword, value); + queryString = queryString.Add(name, value); } builder.Query = queryString.Value; @@ -116,19 +110,9 @@ private string ToConnectionString() return builder.Uri.AbsoluteUri; } - private void FromConnectionString(string? connectionString, bool preserveUnknownSettings) + private void FromConnectionString(string? connectionString) { - if (preserveUnknownSettings) - { - foreach (string keywordToRemove in _settings.Keys.Where(KnownKeywords.Exists).ToArray()) - { - _settings.Remove(keywordToRemove); - } - } - else - { - _settings.Clear(); - } + _settings.Clear(); if (!string.IsNullOrEmpty(connectionString)) { @@ -140,7 +124,7 @@ private void FromConnectionString(string? connectionString, bool preserveUnknown #pragma warning restore S3717 // Track use of "NotImplementedException" } - // MongoDB allows semicolon as separator for query string parameters, to provide backwards compatibility. + // MongoDB allows semicolon as separator between query string parameters for backward compatibility. connectionString = connectionString.Replace(';', '&'); var uri = new Uri(connectionString); @@ -169,31 +153,20 @@ private void FromConnectionString(string? connectionString, bool preserveUnknown _settings[KnownKeywords.AuthenticationDatabase] = Uri.UnescapeDataString(uri.AbsolutePath[1..]); } - NameValueCollection queryCollection = HttpUtility.ParseQueryString(uri.Query); + NameValueCollection queryString = HttpUtility.ParseQueryString(uri.Query); - foreach (string? key in queryCollection.AllKeys) + foreach (string remainingKeyword in queryString.AllKeys.Where(key => key != null && !KnownKeywords.Exists(key)).Cast()) { - if (key != null) - { - string? value = queryCollection.Get(key); + string? value = queryString.Get(remainingKeyword); - if (value != null) - { - _settings[key] = value; - } + if (value != null) + { + _settings[remainingKeyword] = value; } } } } - private static void AssertIsKnownKeyword(string keyword) - { - if (!KnownKeywords.Exists(keyword)) - { - throw new ArgumentException($"Keyword not supported: '{keyword}'.", nameof(keyword)); - } - } - private static class KnownKeywords { public const string Url = "url"; diff --git a/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs b/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs index 311e6d0457..ed0b0f9f0a 100644 --- a/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs +++ b/src/Connectors/src/Connectors/MongoDb/MongoDbHealthContributor.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Connectors.MongoDb; -internal sealed class MongoDbHealthContributor : IHealthContributor +internal sealed partial class MongoDbHealthContributor : IHealthContributor { private readonly MongoClientInterfaceShimFactory _clientFactory; private readonly ILogger _logger; @@ -36,7 +36,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -59,7 +59,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -70,7 +70,7 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -80,6 +80,15 @@ public MongoDbHealthContributor(string serviceName, IServiceProvider serviceProv return result; } + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); + private sealed class MongoClientInterfaceShimFactory { private readonly ConnectorShim _connectorShim; diff --git a/src/Connectors/src/Connectors/MongoDb/MongoDbHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/MongoDb/MongoDbHostApplicationBuilderExtensions.cs index 021c8b4288..336673591f 100644 --- a/src/Connectors/src/Connectors/MongoDb/MongoDbHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/MongoDb/MongoDbHostApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.MongoDb; @@ -41,10 +42,16 @@ public static IHostApplicationBuilder AddMongoDb(this IHostApplicationBuilder bu /// public static IHostApplicationBuilder AddMongoDb(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) + { + return AddMongoDb(builder, configureAction, addAction, null); + } + + internal static IHostApplicationBuilder AddMongoDb(this IHostApplicationBuilder builder, Action? configureAction, + Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - builder.Configuration.ConfigureMongoDb(configureAction); + builder.Configuration.ConfigureMongoDb(configureAction, serviceBindingsReader); builder.Services.AddMongoDb(builder.Configuration, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/MySql/MySqlConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/MySql/MySqlConfigurationBuilderExtensions.cs index 5eda76fffc..7e37ffe389 100644 --- a/src/Connectors/src/Connectors/MySql/MySqlConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/MySql/MySqlConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Connectors.MySql.DynamicTypeAccess; namespace Steeltoe.Connectors.MySql; @@ -20,7 +22,7 @@ public static class MySqlConfigurationBuilderExtensions /// public static IConfigurationBuilder ConfigureMySql(this IConfigurationBuilder builder) { - return ConfigureMySql(builder, MySqlPackageResolver.Default); + return ConfigureMySql(builder, null); } /// @@ -37,16 +39,24 @@ public static IConfigurationBuilder ConfigureMySql(this IConfigurationBuilder bu /// public static IConfigurationBuilder ConfigureMySql(this IConfigurationBuilder builder, Action? configureAction) { - return ConfigureMySql(builder, MySqlPackageResolver.Default, configureAction); + return ConfigureMySql(builder, MySqlPackageResolver.Default, configureAction, null); } internal static IConfigurationBuilder ConfigureMySql(this IConfigurationBuilder builder, MySqlPackageResolver packageResolver, - Action? configureAction = null) + Action? configureAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(packageResolver); - ConnectorConfigurer.Configure(builder, configureAction, new MySqlConnectionStringPostProcessor(packageResolver)); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + options.CloudFoundryBrokerTypes = options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.MySql; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new MySqlConnectionStringPostProcessor(packageResolver), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/MySql/MySqlHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/MySql/MySqlHostApplicationBuilderExtensions.cs index d5454e4b24..56f4335355 100644 --- a/src/Connectors/src/Connectors/MySql/MySqlHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/MySql/MySqlHostApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Connectors.MySql.DynamicTypeAccess; namespace Steeltoe.Connectors.MySql; @@ -21,7 +22,7 @@ public static class MySqlHostApplicationBuilderExtensions /// public static IHostApplicationBuilder AddMySql(this IHostApplicationBuilder builder) { - return AddMySql(builder, MySqlPackageResolver.Default); + return AddMySql(builder, null, null); } /// @@ -43,16 +44,16 @@ public static IHostApplicationBuilder AddMySql(this IHostApplicationBuilder buil public static IHostApplicationBuilder AddMySql(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) { - return AddMySql(builder, MySqlPackageResolver.Default, configureAction, addAction); + return AddMySql(builder, MySqlPackageResolver.Default, configureAction, addAction, null); } internal static IHostApplicationBuilder AddMySql(this IHostApplicationBuilder builder, MySqlPackageResolver packageResolver, - Action? configureAction = null, Action? addAction = null) + Action? configureAction, Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(packageResolver); - builder.Configuration.ConfigureMySql(packageResolver, configureAction); + builder.Configuration.ConfigureMySql(packageResolver, configureAction, serviceBindingsReader); builder.Services.AddMySql(builder.Configuration, packageResolver, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/PostgreSql/PostgreSqlConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/PostgreSql/PostgreSqlConfigurationBuilderExtensions.cs index cff6a541be..06ee495a8f 100644 --- a/src/Connectors/src/Connectors/PostgreSql/PostgreSqlConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/PostgreSql/PostgreSqlConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Connectors.PostgreSql.DynamicTypeAccess; namespace Steeltoe.Connectors.PostgreSql; @@ -20,7 +22,7 @@ public static class PostgreSqlConfigurationBuilderExtensions /// public static IConfigurationBuilder ConfigurePostgreSql(this IConfigurationBuilder builder) { - return ConfigurePostgreSql(builder, PostgreSqlPackageResolver.Default); + return ConfigurePostgreSql(builder, null); } /// @@ -37,16 +39,26 @@ public static IConfigurationBuilder ConfigurePostgreSql(this IConfigurationBuild /// public static IConfigurationBuilder ConfigurePostgreSql(this IConfigurationBuilder builder, Action? configureAction) { - return ConfigurePostgreSql(builder, PostgreSqlPackageResolver.Default, configureAction); + return ConfigurePostgreSql(builder, PostgreSqlPackageResolver.Default, configureAction, null); } - private static IConfigurationBuilder ConfigurePostgreSql(this IConfigurationBuilder builder, PostgreSqlPackageResolver packageResolver, - Action? configureAction = null) + internal static IConfigurationBuilder ConfigurePostgreSql(this IConfigurationBuilder builder, PostgreSqlPackageResolver packageResolver, + Action? configureAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(packageResolver); - ConnectorConfigurer.Configure(builder, configureAction, new PostgreSqlConnectionStringPostProcessor(packageResolver)); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + + options.CloudFoundryBrokerTypes = + options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.PostgreSql; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new PostgreSqlConnectionStringPostProcessor(packageResolver), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/PostgreSql/PostgreSqlHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/PostgreSql/PostgreSqlHostApplicationBuilderExtensions.cs index 76c4f70101..8f51288222 100644 --- a/src/Connectors/src/Connectors/PostgreSql/PostgreSqlHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/PostgreSql/PostgreSqlHostApplicationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; +using Steeltoe.Connectors.PostgreSql.DynamicTypeAccess; namespace Steeltoe.Connectors.PostgreSql; @@ -41,10 +43,16 @@ public static IHostApplicationBuilder AddPostgreSql(this IHostApplicationBuilder /// public static IHostApplicationBuilder AddPostgreSql(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) + { + return AddPostgreSql(builder, configureAction, addAction, null); + } + + internal static IHostApplicationBuilder AddPostgreSql(this IHostApplicationBuilder builder, Action? configureAction, + Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - builder.Configuration.ConfigurePostgreSql(configureAction); + builder.Configuration.ConfigurePostgreSql(PostgreSqlPackageResolver.Default, configureAction, serviceBindingsReader); builder.Services.AddPostgreSql(builder.Configuration, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/PublicAPI.Shipped.txt b/src/Connectors/src/Connectors/PublicAPI.Shipped.txt index 85c0cffef8..013c9d4e4a 100644 --- a/src/Connectors/src/Connectors/PublicAPI.Shipped.txt +++ b/src/Connectors/src/Connectors/PublicAPI.Shipped.txt @@ -63,6 +63,8 @@ Steeltoe.Connectors.ConnectorConfigureOptionsBuilder Steeltoe.Connectors.ConnectorConfigureOptionsBuilder.ConnectorConfigureOptionsBuilder() -> void Steeltoe.Connectors.ConnectorConfigureOptionsBuilder.DetectConfigurationChanges.get -> bool Steeltoe.Connectors.ConnectorConfigureOptionsBuilder.DetectConfigurationChanges.set -> void +Steeltoe.Connectors.ConnectorConfigureOptionsBuilder.SkipDefaultServiceBindings.get -> bool +Steeltoe.Connectors.ConnectorConfigureOptionsBuilder.SkipDefaultServiceBindings.set -> void Steeltoe.Connectors.ConnectorCreateConnection Steeltoe.Connectors.ConnectorCreateHealthContributor Steeltoe.Connectors.ConnectorFactory @@ -110,5 +112,3 @@ Steeltoe.Connectors.SqlServer.SqlServerHostApplicationBuilderExtensions Steeltoe.Connectors.SqlServer.SqlServerOptions Steeltoe.Connectors.SqlServer.SqlServerOptions.SqlServerOptions() -> void Steeltoe.Connectors.SqlServer.SqlServerServiceCollectionExtensions -virtual Steeltoe.Connectors.ConnectorCreateConnection.Invoke(System.IServiceProvider! serviceProvider, string! serviceBindingName) -> object! -virtual Steeltoe.Connectors.ConnectorCreateHealthContributor.Invoke(System.IServiceProvider! serviceProvider, string! serviceBindingName) -> Steeltoe.Common.HealthChecks.IHealthContributor! diff --git a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConfigurationBuilderExtensions.cs index 41f6ecccd2..fd7f1e2b20 100644 --- a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.RabbitMQ; @@ -35,10 +37,26 @@ public static IConfigurationBuilder ConfigureRabbitMQ(this IConfigurationBuilder /// The incoming so that additional calls can be chained. /// public static IConfigurationBuilder ConfigureRabbitMQ(this IConfigurationBuilder builder, Action? configureAction) + { + return ConfigureRabbitMQ(builder, configureAction, null); + } + + internal static IConfigurationBuilder ConfigureRabbitMQ(this IConfigurationBuilder builder, Action? configureAction, + IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - ConnectorConfigurer.Configure(builder, configureAction, new RabbitMQConnectionStringPostProcessor()); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + + options.CloudFoundryBrokerTypes = + options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.RabbitMQ; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new RabbitMQConnectionStringPostProcessor(), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConnectionStringBuilder.cs b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConnectionStringBuilder.cs index 9fe7382933..ccc369197f 100644 --- a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConnectionStringBuilder.cs +++ b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQConnectionStringBuilder.cs @@ -2,8 +2,11 @@ // 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.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Web; +using Microsoft.AspNetCore.Http; namespace Steeltoe.Connectors.RabbitMQ; @@ -28,19 +31,18 @@ public object? this[string keyword] get { ArgumentException.ThrowIfNullOrWhiteSpace(keyword); - AssertIsKnownKeyword(keyword); if (string.Equals(keyword, KnownKeywords.Url, StringComparison.OrdinalIgnoreCase)) { return ConnectionString; } + // Allow getting unknown keyword. We don't pretend to know all valid query string parameters. return _settings.GetValueOrDefault(keyword); } set { ArgumentException.ThrowIfNullOrWhiteSpace(keyword); - AssertIsKnownKeyword(keyword); if (string.Equals(keyword, KnownKeywords.Url, StringComparison.OrdinalIgnoreCase)) { @@ -56,6 +58,7 @@ public object? this[string keyword] } else { + // Allow setting unknown keyword. We don't pretend to know all valid query string parameters. _settings[keyword] = stringValue; } } @@ -97,6 +100,15 @@ private string ToConnectionString() builder.Path = Uri.EscapeDataString(virtualHost); } + var queryString = default(QueryString); + + foreach ((string name, string value) in _settings.Where(pair => !KnownKeywords.Exists(pair.Key))) + { + queryString = queryString.Add(name, value); + } + + builder.Query = queryString.Value; + return builder.Uri.AbsoluteUri; } @@ -132,14 +144,18 @@ private void FromConnectionString(string? connectionString) { _settings[KnownKeywords.VirtualHost] = Uri.UnescapeDataString(uri.AbsolutePath[1..]); } - } - } - private static void AssertIsKnownKeyword(string keyword) - { - if (!KnownKeywords.Exists(keyword)) - { - throw new ArgumentException($"Keyword not supported: '{keyword}'.", nameof(keyword)); + NameValueCollection queryString = HttpUtility.ParseQueryString(uri.Query); + + foreach (string remainingKeyword in queryString.AllKeys.Where(key => key != null && !KnownKeywords.Exists(key)).Cast()) + { + string? value = queryString.Get(remainingKeyword); + + if (value != null) + { + _settings[remainingKeyword] = value; + } + } } } diff --git a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs index b534510529..a324a4ca11 100644 --- a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs +++ b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHealthContributor.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Connectors.RabbitMQ; -internal sealed class RabbitMQHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RabbitMQHealthContributor : IHealthContributor, IDisposable { private readonly ILogger _logger; private readonly ConnectionFactoryInterfaceShim _connectionFactoryInterfaceShim; @@ -34,7 +34,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -68,7 +68,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -79,7 +79,7 @@ public RabbitMQHealthContributor(object connectionFactory, string host, ILogger< ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -94,4 +94,13 @@ public void Dispose() _connectionInterfaceShim?.Dispose(); _connectionInterfaceShim = null; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHostApplicationBuilderExtensions.cs index 23a98f46b6..9a851e8947 100644 --- a/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/RabbitMQ/RabbitMQHostApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.RabbitMQ; @@ -41,10 +42,16 @@ public static IHostApplicationBuilder AddRabbitMQ(this IHostApplicationBuilder b /// public static IHostApplicationBuilder AddRabbitMQ(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) + { + return AddRabbitMQ(builder, configureAction, addAction, null); + } + + internal static IHostApplicationBuilder AddRabbitMQ(this IHostApplicationBuilder builder, Action? configureAction, + Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - builder.Configuration.ConfigureRabbitMQ(configureAction); + builder.Configuration.ConfigureRabbitMQ(configureAction, serviceBindingsReader); builder.Services.AddRabbitMQ(builder.Configuration, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/Redis/RedisConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/Redis/RedisConfigurationBuilderExtensions.cs index 5fc8caf70b..f912a2f42d 100644 --- a/src/Connectors/src/Connectors/Redis/RedisConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/Redis/RedisConfigurationBuilderExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.Redis; @@ -35,10 +37,24 @@ public static IConfigurationBuilder ConfigureRedis(this IConfigurationBuilder bu /// The incoming so that additional calls can be chained. /// public static IConfigurationBuilder ConfigureRedis(this IConfigurationBuilder builder, Action? configureAction) + { + return ConfigureRedis(builder, configureAction, null); + } + + internal static IConfigurationBuilder ConfigureRedis(this IConfigurationBuilder builder, Action? configureAction, + IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - ConnectorConfigurer.Configure(builder, configureAction, new RedisConnectionStringPostProcessor()); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + options.CloudFoundryBrokerTypes = options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.Redis; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new RedisConnectionStringPostProcessor(), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/Redis/RedisConnectionStringBuilder.cs b/src/Connectors/src/Connectors/Redis/RedisConnectionStringBuilder.cs index 8d791a0ebf..3b15fb52b9 100644 --- a/src/Connectors/src/Connectors/Redis/RedisConnectionStringBuilder.cs +++ b/src/Connectors/src/Connectors/Redis/RedisConnectionStringBuilder.cs @@ -29,14 +29,8 @@ public object? this[string keyword] { ArgumentException.ThrowIfNullOrWhiteSpace(keyword); - // Allow getting unknown keyword, if it was set earlier. We don't pretend to know all valid keywords. - if (_settings.TryGetValue(keyword, out string? value)) - { - return value; - } - - AssertIsKnownKeyword(keyword); - return null; + // Allow getting unknown keyword. We don't pretend to know all valid query string parameters. + return _settings.GetValueOrDefault(keyword); } set { @@ -50,7 +44,7 @@ public object? this[string keyword] } else { - // Allow setting unknown keyword. We don't pretend to know all valid keywords. + // Allow setting unknown keyword. We don't pretend to know all valid query string parameters. _settings[keyword] = stringValue; } } @@ -120,14 +114,6 @@ private void FromConnectionString(string? connectionString) } } - private static void AssertIsKnownKeyword(string keyword) - { - if (!KnownKeywords.Exists(keyword)) - { - throw new ArgumentException($"Keyword not supported: '{keyword}'.", nameof(keyword)); - } - } - private static class KnownKeywords { public const string Host = "host"; diff --git a/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs b/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs index 9a2e0eb98c..efa9496da0 100644 --- a/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs +++ b/src/Connectors/src/Connectors/Redis/RedisHealthContributor.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Connectors.Redis; -internal sealed class RedisHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RedisHealthContributor : IHealthContributor, IDisposable { private readonly ILogger _logger; private readonly string? _connectionString; @@ -54,7 +54,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -80,7 +80,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) result.Status = HealthStatus.Up; result.Details.Add("ping", roundTripTime.TotalMilliseconds); - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -91,7 +91,7 @@ internal void SetConnectionMultiplexer(object connectionMultiplexer) ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -106,4 +106,13 @@ public void Dispose() _connectionMultiplexerInterfaceShim?.Dispose(); _connectionMultiplexerInterfaceShim = null; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/Connectors/Redis/RedisHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/Redis/RedisHostApplicationBuilderExtensions.cs index db17d66be3..c60d508ebb 100644 --- a/src/Connectors/src/Connectors/Redis/RedisHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/Redis/RedisHostApplicationBuilderExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; namespace Steeltoe.Connectors.Redis; @@ -46,10 +47,16 @@ public static IHostApplicationBuilder AddRedis(this IHostApplicationBuilder buil /// public static IHostApplicationBuilder AddRedis(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) + { + return AddRedis(builder, configureAction, addAction, null); + } + + internal static IHostApplicationBuilder AddRedis(this IHostApplicationBuilder builder, Action? configureAction, + Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); - builder.Configuration.ConfigureRedis(configureAction); + builder.Configuration.ConfigureRedis(configureAction, serviceBindingsReader); builder.Services.AddRedis(builder.Configuration, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs b/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs index 04b1773a41..a2fcfd0dc7 100644 --- a/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs +++ b/src/Connectors/src/Connectors/RelationalDatabaseHealthContributor.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Connectors; -internal sealed class RelationalDatabaseHealthContributor : IHealthContributor, IDisposable +internal sealed partial class RelationalDatabaseHealthContributor : IHealthContributor, IDisposable { private readonly DbConnection _connection; private readonly ILogger _logger; @@ -32,7 +32,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host public async Task CheckHealthAsync(CancellationToken cancellationToken) { - _logger.LogTrace("Checking {DbConnection} health at {Host}", Id, Host); + LogCheckingHealth(Id, Host); var result = new HealthCheckResult { @@ -56,7 +56,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host result.Status = HealthStatus.Up; - _logger.LogTrace("{DbConnection} at {Host} is up!", Id, Host); + LogHealthUp(Id, Host); } catch (Exception exception) { @@ -67,7 +67,7 @@ public RelationalDatabaseHealthContributor(DbConnection connection, string? host ExceptionDispatchInfo.Capture(exception).Throw(); } - _logger.LogError(exception, "{DbConnection} at {Host} is down!", Id, Host); + LogHealthDown(exception, Id, Host); result.Status = HealthStatus.Down; result.Description = $"{Id} health check failed"; @@ -96,4 +96,13 @@ public void Dispose() { _connection.Dispose(); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Checking {DbConnection} health at {Host}.")] + private partial void LogCheckingHealth(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{DbConnection} at {Host} is up.")] + private partial void LogHealthUp(string dbConnection, string host); + + [LoggerMessage(Level = LogLevel.Error, Message = "{DbConnection} at {Host} is down.")] + private partial void LogHealthDown(Exception exception, string dbConnection, string host); } diff --git a/src/Connectors/src/Connectors/SqlServer/SqlServerConfigurationBuilderExtensions.cs b/src/Connectors/src/Connectors/SqlServer/SqlServerConfigurationBuilderExtensions.cs index 8aa20df6df..a3bd5092e8 100644 --- a/src/Connectors/src/Connectors/SqlServer/SqlServerConfigurationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/SqlServer/SqlServerConfigurationBuilderExtensions.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Connectors.SqlServer.RuntimeTypeAccess; +using IServiceBindingsReader = Steeltoe.Configuration.CloudFoundry.ServiceBindings.IServiceBindingsReader; namespace Steeltoe.Connectors.SqlServer; @@ -20,7 +23,7 @@ public static class SqlServerConfigurationBuilderExtensions /// public static IConfigurationBuilder ConfigureSqlServer(this IConfigurationBuilder builder) { - return ConfigureSqlServer(builder, SqlServerPackageResolver.Default); + return ConfigureSqlServer(builder, null); } /// @@ -37,16 +40,26 @@ public static IConfigurationBuilder ConfigureSqlServer(this IConfigurationBuilde /// public static IConfigurationBuilder ConfigureSqlServer(this IConfigurationBuilder builder, Action? configureAction) { - return ConfigureSqlServer(builder, SqlServerPackageResolver.Default, configureAction); + return ConfigureSqlServer(builder, SqlServerPackageResolver.Default, configureAction, null); } internal static IConfigurationBuilder ConfigureSqlServer(this IConfigurationBuilder builder, SqlServerPackageResolver packageResolver, - Action? configureAction = null) + Action? configureAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(packageResolver); - ConnectorConfigurer.Configure(builder, configureAction, new SqlServerConnectionStringPostProcessor(packageResolver)); + Action overrideConfigureAction = options => + { + configureAction?.Invoke(options); + + options.CloudFoundryBrokerTypes = + options.SkipDefaultServiceBindings ? CloudFoundryServiceBrokerTypes.None : CloudFoundryServiceBrokerTypes.SqlServer; + }; + + ConnectorConfigurer.Configure(builder, overrideConfigureAction, new SqlServerConnectionStringPostProcessor(packageResolver), serviceBindingsReader, + NullLoggerFactory.Instance); + return builder; } } diff --git a/src/Connectors/src/Connectors/SqlServer/SqlServerHostApplicationBuilderExtensions.cs b/src/Connectors/src/Connectors/SqlServer/SqlServerHostApplicationBuilderExtensions.cs index e47ff4a6ec..238e0c5aad 100644 --- a/src/Connectors/src/Connectors/SqlServer/SqlServerHostApplicationBuilderExtensions.cs +++ b/src/Connectors/src/Connectors/SqlServer/SqlServerHostApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Hosting; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Connectors.SqlServer.RuntimeTypeAccess; namespace Steeltoe.Connectors.SqlServer; @@ -21,7 +22,7 @@ public static class SqlServerHostApplicationBuilderExtensions /// public static IHostApplicationBuilder AddSqlServer(this IHostApplicationBuilder builder) { - return AddSqlServer(builder, SqlServerPackageResolver.Default); + return AddSqlServer(builder, null, null); } /// @@ -43,16 +44,16 @@ public static IHostApplicationBuilder AddSqlServer(this IHostApplicationBuilder public static IHostApplicationBuilder AddSqlServer(this IHostApplicationBuilder builder, Action? configureAction, Action? addAction) { - return AddSqlServer(builder, SqlServerPackageResolver.Default, configureAction, addAction); + return AddSqlServer(builder, SqlServerPackageResolver.Default, configureAction, addAction, null); } internal static IHostApplicationBuilder AddSqlServer(this IHostApplicationBuilder builder, SqlServerPackageResolver packageResolver, - Action? configureAction = null, Action? addAction = null) + Action? configureAction, Action? addAction, IServiceBindingsReader? serviceBindingsReader) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(packageResolver); - builder.Configuration.ConfigureSqlServer(packageResolver, configureAction); + builder.Configuration.ConfigureSqlServer(packageResolver, configureAction, serviceBindingsReader); builder.Services.AddSqlServer(builder.Configuration, packageResolver, addAction); return builder; } diff --git a/src/Connectors/src/Connectors/Steeltoe.Connectors.csproj b/src/Connectors/src/Connectors/Steeltoe.Connectors.csproj index 3f1411d7b2..5401c5d45a 100644 --- a/src/Connectors/src/Connectors/Steeltoe.Connectors.csproj +++ b/src/Connectors/src/Connectors/Steeltoe.Connectors.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Connectors for using service bindings in your application. connectors;service-bindings;vcap_services;cnb;tanzu true diff --git a/src/Connectors/src/EntityFrameworkCore/ConfigurationSchema.json b/src/Connectors/src/EntityFrameworkCore/ConfigurationSchema.json new file mode 100644 index 0000000000..d07e30ca58 --- /dev/null +++ b/src/Connectors/src/EntityFrameworkCore/ConfigurationSchema.json @@ -0,0 +1,17 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Connectors": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Connectors.EntityFrameworkCore": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs b/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs index c4b899b49e..c127054e38 100644 --- a/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs +++ b/src/Connectors/src/EntityFrameworkCore/MigrateDbContextTask.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Connectors.EntityFrameworkCore; /// /// The to run migrations from. /// -public sealed class MigrateDbContextTask : IApplicationTask +public sealed partial class MigrateDbContextTask : IApplicationTask where TDbContext : DbContext { public const string Name = "migrate"; @@ -50,7 +50,7 @@ public async Task RunAsync(CancellationToken cancellationToken) isNewDatabase = true; } - _logger.LogInformation("Starting database migration..."); + LogStartingMigration(); await _dbContext.Database.MigrateAsync(cancellationToken); if (isNewDatabase) @@ -60,12 +60,21 @@ public async Task RunAsync(CancellationToken cancellationToken) if (migrations.Length > 0) { - string migrationNames = string.Join(", ", migrations); - _logger.LogInformation("The following migrations have been successfully applied: {MigrationNames}.", migrationNames); + string migrationNames = string.Join(", ", migrations.Select(migration => $"'{migration}'")); + LogMigrationsApplied(migrationNames); } else { - _logger.LogInformation("Database is already up to date."); + LogAlreadyUpToDate(); } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting database migration.")] + private partial void LogStartingMigration(); + + [LoggerMessage(Level = LogLevel.Information, Message = "The following migrations have been successfully applied: {MigrationNames}.")] + private partial void LogMigrationsApplied(string migrationNames); + + [LoggerMessage(Level = LogLevel.Information, Message = "Database is already up to date.")] + private partial void LogAlreadyUpToDate(); } diff --git a/src/Connectors/src/EntityFrameworkCore/Properties/AssemblyInfo.cs b/src/Connectors/src/EntityFrameworkCore/Properties/AssemblyInfo.cs index df6dc2105e..02b620e310 100644 --- a/src/Connectors/src/EntityFrameworkCore/Properties/AssemblyInfo.cs +++ b/src/Connectors/src/EntityFrameworkCore/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.Connectors", "Steeltoe.Connectors.EntityFrameworkCore")] [assembly: InternalsVisibleTo("Steeltoe.Connectors.EntityFrameworkCore.Test")] diff --git a/src/Connectors/src/EntityFrameworkCore/Steeltoe.Connectors.EntityFrameworkCore.csproj b/src/Connectors/src/EntityFrameworkCore/Steeltoe.Connectors.EntityFrameworkCore.csproj index 7c621a888d..ccd06f1162 100644 --- a/src/Connectors/src/EntityFrameworkCore/Steeltoe.Connectors.EntityFrameworkCore.csproj +++ b/src/Connectors/src/EntityFrameworkCore/Steeltoe.Connectors.EntityFrameworkCore.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Extensions for using Steeltoe Connectors with Entity Framework Core. connectors;EFCore;EntityFrameworkCore;EF;Entity;Framework;Core;entity-framework-core;services;tanzu true diff --git a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs index 9d9aca9d3e..c16d6df3bb 100644 --- a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs +++ b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Npgsql; @@ -35,9 +34,9 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.AddPostgreSql(configureOptions => configureOptions.DetectConfigurationChanges = true, null); await using WebApplication app = builder.Build(); @@ -63,7 +62,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -83,7 +82,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -110,11 +109,11 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -142,7 +141,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -162,7 +161,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -189,11 +188,11 @@ public void Applies_local_configuration_changes_using_HostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -221,7 +220,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -241,7 +240,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; diff --git a/src/Connectors/test/Connectors.Test/CosmosDb/CosmosDbHealthContributorTest.cs b/src/Connectors/test/Connectors.Test/CosmosDb/CosmosDbHealthContributorTest.cs index 258f2d82c0..303f5c0c21 100644 --- a/src/Connectors/test/Connectors.Test/CosmosDb/CosmosDbHealthContributorTest.cs +++ b/src/Connectors/test/Connectors.Test/CosmosDb/CosmosDbHealthContributorTest.cs @@ -105,7 +105,7 @@ public async Task Canceled_Throws() cosmosClientMock.Setup(client => client.ReadAccountAsync()).Returns(async () => { await Task.Delay(3.Seconds(), TestContext.Current.CancellationToken); - return null!; + return null; }); await using ServiceProvider serviceProvider = CreateServiceProvider(serviceName, connectionString, cosmosClientMock.Object); diff --git a/src/Connectors/test/Connectors.Test/CustomBrokerTests.cs b/src/Connectors/test/Connectors.Test/CustomBrokerTests.cs new file mode 100644 index 0000000000..e30896fa1a --- /dev/null +++ b/src/Connectors/test/Connectors.Test/CustomBrokerTests.cs @@ -0,0 +1,266 @@ +// 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.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Steeltoe.Common.TestResources; +using Steeltoe.Configuration.CloudFoundry; +using Steeltoe.Configuration.CloudFoundry.ServiceBindings; +using Steeltoe.Connectors.PostgreSql; +using Steeltoe.Connectors.RabbitMQ; + +namespace Steeltoe.Connectors.Test; + +public sealed class CustomBrokerTests +{ + [Fact] + public async Task Binds_options_with_third_party_service_bindings() + { + var appSettings = new Dictionary + { + ["Steeltoe:Client:PostgreSql:products-db:ConnectionString"] = "Include Error Detail=true;host=localhost", + ["Steeltoe:Client:PostgreSql:orders-db:ConnectionString"] = "Log Parameters=true;port=9999" + }; + + var reader = new CloudFoundryMemorySettingsReader + { + ServicesJson = """ + { + "custom-postgres-broker": [ + { + "name": "products-db", + "credentials": { + "custom-hostname-key": "example.cloud.com", + "custom-port-key": 2345, + "custom-username-key": "products-user", + "custom-password-key": "products-secret", + "custom-database-name-key": "product-database", + "host": "IGNORED" + } + }, + { + "name": "orders-db", + "credentials": { + "custom-hostname-key": "example.cloud.com", + "custom-port-key": 2345, + "custom-username-key": "orders-user", + "custom-password-key": "orders-secret", + "custom-database-name-key": "order-database" + } + } + ] + } + """ + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Configuration.AddCloudFoundry(reader); + MapServiceBindingsForCustomPostgreSqlBroker("custom-postgres-broker"); + builder.AddPostgreSql(options => options.SkipDefaultServiceBindings = true, null); + await using WebApplication app = builder.Build(); + + var optionsMonitor = app.Services.GetRequiredService>(); + PostgreSqlOptions productsDbOptions = optionsMonitor.Get("products-db"); + PostgreSqlOptions ordersDbOptions = optionsMonitor.Get("orders-db"); + + ExtractConnectionStringParameters(productsDbOptions.ConnectionString).Should().BeEquivalentTo(new List + { + "Include Error Detail=True", + "Host=example.cloud.com", + "Port=2345", + "Database=product-database", + "Username=products-user", + "Password=products-secret" + }, options => options.WithoutStrictOrdering()); + + ExtractConnectionStringParameters(ordersDbOptions.ConnectionString).Should().BeEquivalentTo(new List + { + "Log Parameters=True", + "Host=example.cloud.com", + "Port=2345", + "Database=order-database", + "Username=orders-user", + "Password=orders-secret" + }, options => options.WithoutStrictOrdering()); + + void MapServiceBindingsForCustomPostgreSqlBroker(string brokerName) + { + var options = builder.Configuration.GetSection("vcap").Get(); + + foreach (CloudFoundryService service in options?.Services.Where(pair => pair.Key == brokerName).SelectMany(pair => pair.Value) ?? []) + { + builder.Configuration.AddInMemoryCollection(new Dictionary + { + // Map credentials into the property names expected by NpgsqlConnectionStringBuilder. + [$"steeltoe:service-bindings:postgresql:{service.Name}:host"] = service.Credentials["custom-hostname-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:port"] = service.Credentials["custom-port-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:username"] = service.Credentials["custom-username-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:password"] = service.Credentials["custom-password-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:database"] = service.Credentials["custom-database-name-key"].Value + }); + } + } + } + + [Fact] + public async Task Third_party_service_bindings_can_be_combined_with_builtin() + { + var appSettings = new Dictionary + { + ["Steeltoe:Client:PostgreSql:products-db:ConnectionString"] = "Include Error Detail=true;host=localhost", + ["Steeltoe:Client:PostgreSql:orders-db:ConnectionString"] = "Log Parameters=true;port=9999", + ["Steeltoe:Client:RabbitMQ:transaction-queue:ConnectionString"] = "amqp://localhost?connection_timeout=5000&heartbeat=5&unknown=local" + }; + + const string vcapServicesJson = """ + { + "postgres": [ + { + "name": "products-db", + "credentials": { + "custom-hostname-key": "example.cloud.com", + "custom-port-key": 2345, + "custom-username-key": "products-user", + "custom-password-key": "products-secret", + "custom-database-name-key": "product-database" + } + }, + { + "name": "orders-db", + "credentials": { + "custom-hostname-key": "example.cloud.com", + "custom-port-key": 2345, + "custom-username-key": "orders-user", + "custom-password-key": "orders-secret", + "custom-database-name-key": "order-database" + } + } + ], + "p.rabbitmq": [ + { + "name": "transaction-queue", + "tags": [ + "rabbitmq" + ], + "credentials": { + "protocols": { + "amqp+ssl": { + "host": "messaging.cloud.com", + "port": 2765, + "username": "app-user", + "password": "secret", + "vhost": "transactions" + } + }, + "ssl": true, + "unknown": "remote" + } + } + ] + } + """; + + var reader = new CloudFoundryMemorySettingsReader + { + ServicesJson = vcapServicesJson + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Configuration.AddCloudFoundry(reader); + MapServiceBindingsForCustomPostgreSqlBroker("postgres"); + builder.AddPostgreSql(options => options.SkipDefaultServiceBindings = true, null); + MapExtraRabbitMQServiceBindingsUsingDefaultBroker("p.rabbitmq"); + builder.AddRabbitMQ(null, null, new StringServiceBindingsReader(vcapServicesJson)); + await using WebApplication app = builder.Build(); + + var postgreSqlOptionsMonitor = app.Services.GetRequiredService>(); + PostgreSqlOptions productsDbOptions = postgreSqlOptionsMonitor.Get("products-db"); + PostgreSqlOptions ordersDbOptions = postgreSqlOptionsMonitor.Get("orders-db"); + + var rabbitMQOptionsMonitor = app.Services.GetRequiredService>(); + RabbitMQOptions transactionQueueOptions = rabbitMQOptionsMonitor.Get("transaction-queue"); + + ExtractConnectionStringParameters(productsDbOptions.ConnectionString).Should().BeEquivalentTo(new List + { + "Include Error Detail=True", + "Host=example.cloud.com", + "Port=2345", + "Database=product-database", + "Username=products-user", + "Password=products-secret" + }, options => options.WithoutStrictOrdering()); + + ExtractConnectionStringParameters(ordersDbOptions.ConnectionString).Should().BeEquivalentTo(new List + { + "Log Parameters=True", + "Host=example.cloud.com", + "Port=2345", + "Database=order-database", + "Username=orders-user", + "Password=orders-secret" + }, options => options.WithoutStrictOrdering()); + + transactionQueueOptions.ConnectionString.Should().Be( + "amqps://app-user:secret@messaging.cloud.com:2765/transactions?connection_timeout=5000&heartbeat=5&unknown=remote"); + + void MapServiceBindingsForCustomPostgreSqlBroker(string brokerName) + { + var options = builder.Configuration.GetSection("vcap").Get(); + + foreach (CloudFoundryService service in options?.Services.Where(pair => pair.Key == brokerName).SelectMany(pair => pair.Value) ?? []) + { + builder.Configuration.AddInMemoryCollection(new Dictionary + { + // Map third-party credentials into the property names expected by NpgsqlConnectionStringBuilder. + [$"steeltoe:service-bindings:postgresql:{service.Name}:host"] = service.Credentials["custom-hostname-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:port"] = service.Credentials["custom-port-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:username"] = service.Credentials["custom-username-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:password"] = service.Credentials["custom-password-key"].Value, + [$"steeltoe:service-bindings:postgresql:{service.Name}:database"] = service.Credentials["custom-database-name-key"].Value + }); + } + } + + void MapExtraRabbitMQServiceBindingsUsingDefaultBroker(string brokerName) + { + var options = builder.Configuration.GetSection("vcap").Get(); + + foreach (CloudFoundryService service in options?.Services.Where(pair => pair.Key == brokerName).SelectMany(pair => pair.Value) ?? []) + { + builder.Configuration.AddInMemoryCollection(new Dictionary + { + // Map third-party 'unknown' credential, in addition to built-in broker parameters. + [$"steeltoe:service-bindings:rabbitmq:{service.Name}:unknown"] = service.Credentials["unknown"].Value + }); + } + } + } + + private static List ExtractConnectionStringParameters(string? connectionString) + { + List entries = []; + + if (connectionString != null) + { + foreach (string parameter in connectionString.Split(';')) + { + string[] nameValuePair = parameter.Split('=', 2); + + if (nameValuePair.Length == 2) + { + string name = nameValuePair[0]; + string value = nameValuePair[1]; + + entries.Add($"{name}={value}"); + } + } + } + + return entries; + } +} diff --git a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectionStringBuilderTest.cs b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectionStringBuilderTest.cs index 7c10250f61..a5d339489d 100644 --- a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectionStringBuilderTest.cs +++ b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectionStringBuilderTest.cs @@ -82,13 +82,13 @@ public void Returns_null_when_getting_known_keyword() } [Fact] - public void Throws_when_getting_unknown_keyword() + public void Returns_null_when_getting_unknown_keyword() { var builder = new MongoDbConnectionStringBuilder(); - Action action = () => _ = builder["bad"]; + object? some = builder["some"]; - action.Should().ThrowExactly().WithMessage("Keyword not supported: 'bad'.*"); + some.Should().BeNull(); } [Fact] diff --git a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectorTest.cs b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectorTest.cs index d9935f8902..96765a8d07 100644 --- a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbConnectorTest.cs @@ -148,9 +148,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMongoDb(); + builder.AddMongoDb(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -158,7 +157,7 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() MongoDbOptions optionsOne = optionsMonitor.Get("myMongoDbServiceOne"); optionsOne.ConnectionString.Should().Be( - "mongodb://csb0230eada-2354-4c73-b3e4-8a1aaa996894:AiNtEyASbdXR5neJmTStMzKGItX2xvKuyEkcy65rviKD0ggZR19E1iVFIJ5ZAIY1xvvAiS5tOXsmACDbKDJIhQ%3D%3D@csb0230eada-2354-4c73-b3e4-8a1aaa996894.mongo.cosmos.cloud-hostname.com:10255/csb-db0230eada-2354-4c73-b3e4-8a1aaa996894?connectTimeoutMS=5000&ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@csb0230eada-2354-4c73-b3e4-8a1aaa996894@"); + "mongodb://csb0230eada-2354-4c73-b3e4-8a1aaa996894:AiNtEyASbdXR5neJmTStMzKGItX2xvKuyEkcy65rviKD0ggZR19E1iVFIJ5ZAIY1xvvAiS5tOXsmACDbKDJIhQ%3D%3D@csb0230eada-2354-4c73-b3e4-8a1aaa996894.mongo.cosmos.cloud-hostname.com:10255/csb-db0230eada-2354-4c73-b3e4-8a1aaa996894?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@csb0230eada-2354-4c73-b3e4-8a1aaa996894@"); optionsOne.Database.Should().Be("csb-db0230eada-2354-4c73-b3e4-8a1aaa996894"); @@ -270,8 +269,7 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddMongoDb(); + builder.AddMongoDb(null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); diff --git a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbHealthContributorTest.cs b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbHealthContributorTest.cs index 321ecc35b5..abf1a00a0a 100644 --- a/src/Connectors/test/Connectors.Test/MongoDb/MongoDbHealthContributorTest.cs +++ b/src/Connectors/test/Connectors.Test/MongoDb/MongoDbHealthContributorTest.cs @@ -39,7 +39,7 @@ public async Task Not_Connected_Returns_Down_Status() result.Description.Should().Be("MongoDB health check failed"); result.Details.Should().Contain("host", "localhost"); result.Details.Should().Contain("service", "Example"); - result.Details.Should().ContainKey("error").WhoseValue.As().Should().StartWith("TimeoutException: A timeout occurred after 1ms selecting "); + result.Details.Should().ContainKey("error").WhoseValue.As().Should().StartWith("TimeoutException: A timeout occurred after"); } [Fact] diff --git a/src/Connectors/test/Connectors.Test/MySql/MySqlConnector/MySqlConnectorTest.cs b/src/Connectors/test/Connectors.Test/MySql/MySqlConnector/MySqlConnectorTest.cs index 4aec658cbf..29a17920e0 100644 --- a/src/Connectors/test/Connectors.Test/MySql/MySqlConnector/MySqlConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/MySql/MySqlConnector/MySqlConnectorTest.cs @@ -115,7 +115,7 @@ public async Task Binds_options_without_service_bindings() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); builder.Services.Configure("myMySqlServiceOne", options => options.ConnectionString += ";Use Compression=false"); await using WebApplication app = builder.Build(); @@ -153,9 +153,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -235,7 +234,7 @@ public async Task Registers_ConnectorFactory() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -262,7 +261,7 @@ public async Task Registers_HealthContributors() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); await using WebApplication app = builder.Build(); RelationalDatabaseHealthContributor[] contributors = @@ -283,8 +282,7 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -312,7 +310,7 @@ public async Task Registers_default_connection_string_when_only_default_client_b WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); diff --git a/src/Connectors/test/Connectors.Test/MySql/Oracle/MySqlConnectorTest.cs b/src/Connectors/test/Connectors.Test/MySql/Oracle/MySqlConnectorTest.cs index 05f9d0696c..144e1bd47a 100644 --- a/src/Connectors/test/Connectors.Test/MySql/Oracle/MySqlConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/MySql/Oracle/MySqlConnectorTest.cs @@ -115,7 +115,7 @@ public async Task Binds_options_without_service_bindings() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); builder.Services.Configure("myMySqlServiceOne", options => options.ConnectionString += ";Use Compression=false"); await using WebApplication app = builder.Build(); @@ -153,9 +153,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -235,7 +234,7 @@ public async Task Registers_ConnectorFactory() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -262,7 +261,7 @@ public async Task Registers_HealthContributors() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); await using WebApplication app = builder.Build(); RelationalDatabaseHealthContributor[] contributors = @@ -283,8 +282,7 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -312,7 +310,7 @@ public async Task Registers_default_connection_string_when_only_default_client_b WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); diff --git a/src/Connectors/test/Connectors.Test/PostgreSql/PostgreSqlConnectorTest.cs b/src/Connectors/test/Connectors.Test/PostgreSql/PostgreSqlConnectorTest.cs index 9a158bb430..fca421abc7 100644 --- a/src/Connectors/test/Connectors.Test/PostgreSql/PostgreSqlConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/PostgreSql/PostgreSqlConnectorTest.cs @@ -232,9 +232,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -349,8 +348,6 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() -----END RSA PRIVATE KEY----- """ }, options => options.WithoutStrictOrdering().Using(IgnoreLineEndingsComparer.Instance)); - - CleanupTempFiles(optionsAzureOne.ConnectionString, optionsAzureTwo.ConnectionString, optionsGoogle.ConnectionString); } [Fact] @@ -461,7 +458,7 @@ public async Task Skips_HealthContributors_when_AspNetCore_health_checks_are_reg WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); builder.Services.AddHealthChecks(); - builder.AddPostgreSql(null, null); + builder.AddPostgreSql(); await using WebApplication app = builder.Build(); app.Services.GetServices().Should().BeEmpty(); @@ -489,13 +486,12 @@ public async Task Registers_default_connection_string_when_single_server_binding { var appSettings = new Dictionary { - ["Steeltoe:Client:PostgreSql:Default:ConnectionString"] = "SERVER=localhost;DB=myDb;UID=myUser;PWD=myPass;Log Parameters=True" + ["Steeltoe:Client:PostgreSql:DEFAULT:ConnectionString"] = "SERVER=localhost;DB=myDb;UID=myUser;PWD=myPass;Log Parameters=True" }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -526,8 +522,7 @@ public async Task Registers_default_connection_string_when_single_server_binding public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -624,9 +619,8 @@ public async Task Registers_no_default_connection_string_when_multiple_server_bi }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -652,9 +646,8 @@ public async Task Registers_no_default_connection_string_when_single_server_bind }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -677,9 +670,8 @@ public async Task Registers_no_default_connection_string_when_service_and_client }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddPostgreSql(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -697,6 +689,36 @@ public async Task Registers_no_default_connection_string_when_service_and_client app.Services.GetServices().Should().HaveCount(2); } + [Fact] + public async Task Deletes_temporary_files() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.AddPostgreSql(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); + + List tempPaths = []; + + await using (WebApplication app = builder.Build()) + { + var optionsMonitor = app.Services.GetRequiredService>(); + + PostgreSqlOptions optionsGoogle = optionsMonitor.Get("myPostgreSqlServiceGoogle"); + + foreach (string key in TempFileKeys) + { + string? tempPath = ExtractRawConnectionStringParameter(optionsGoogle.ConnectionString, key); + tempPath.Should().NotBeNull(); + File.Exists(tempPath).Should().BeTrue(); + + tempPaths.Add(tempPath); + } + } + + foreach (string tempPath in tempPaths) + { + File.Exists(tempPath).Should().BeFalse(); + } + } + [Fact] public void Subsequent_registrations_are_ignored() { @@ -717,48 +739,50 @@ private static List ExtractConnectionStringParameters(string? connection { List entries = []; - if (connectionString != null) + foreach (KeyValuePair pair in EnumerateConnectionStringParameters(connectionString)) { - foreach (string parameter in connectionString.Split(';')) + string value = pair.Value; + + if (TempFileKeys.Contains(pair.Key)) { - string[] nameValuePair = parameter.Split('=', 2); + value = File.ReadAllText(value); + } - if (nameValuePair.Length == 2) - { - string name = nameValuePair[0]; - string value = nameValuePair[1]; + value = value.Replace("\n", Environment.NewLine, StringComparison.Ordinal); - if (TempFileKeys.Contains(name)) - { - value = File.ReadAllText(value); - } + entries.Add($"{pair.Key}={value}"); + } - value = value.Replace("\n", Environment.NewLine, StringComparison.Ordinal); + return entries; + } - entries.Add($"{name}={value}"); - } + private static string? ExtractRawConnectionStringParameter(string? connectionString, string parameterName) + { + foreach (KeyValuePair pair in EnumerateConnectionStringParameters(connectionString)) + { + if (string.Equals(pair.Key, parameterName, StringComparison.OrdinalIgnoreCase)) + { + return pair.Value; } } - return entries; + return null; } - private static void CleanupTempFiles(params string?[] connectionStrings) + private static IEnumerable> EnumerateConnectionStringParameters(string? connectionString) { - foreach (string? connectionString in connectionStrings) + if (connectionString != null) { - if (!string.IsNullOrEmpty(connectionString)) + foreach (string parameter in connectionString.Split(';')) { - foreach (string entry in connectionString.Split(';')) + string[] nameValuePair = parameter.Split('=', 2); + + if (nameValuePair.Length == 2) { - string[] pair = entry.Split('=', 2); - string key = pair[0]; - string value = pair[1]; - - if (TempFileKeys.Contains(key) && File.Exists(value)) - { - File.Delete(value); - } + string name = nameValuePair[0]; + string value = nameValuePair[1]; + + yield return new KeyValuePair(name, value); } } } diff --git a/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectionStringBuilderTest.cs b/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectionStringBuilderTest.cs index a82c343c9a..4dc3cb7800 100644 --- a/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectionStringBuilderTest.cs +++ b/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectionStringBuilderTest.cs @@ -41,6 +41,21 @@ public void Decodes_properties_with_special_characters() builder["virtualHost"].Should().Be("my virtual= host"); } + [Fact] + public void Preserves_query_string_parameters_when_setting_URL() + { + var builder = new RabbitMQConnectionStringBuilder + { + ConnectionString = "amqps://localhost:999/virtual-host-1?first=one&second=two" + }; + + const string url = "amqps://localhost:999/virtual-host-1?first=one&second=number2"; + builder["url"] = url; + + builder.ConnectionString.Should().Be("amqps://localhost:999/virtual-host-1?first=one&second=number2"); + builder["url"].Should().Be(builder.ConnectionString); + } + [Fact] public void Returns_null_when_getting_known_keyword() { @@ -52,22 +67,24 @@ public void Returns_null_when_getting_known_keyword() } [Fact] - public void Throws_when_getting_unknown_keyword() + public void Returns_null_when_getting_unknown_keyword() { var builder = new RabbitMQConnectionStringBuilder(); - Action action = () => _ = builder["bad"]; + object? some = builder["some"]; - action.Should().ThrowExactly().WithMessage("Keyword not supported: 'bad'.*"); + some.Should().BeNull(); } [Fact] - public void Throws_when_setting_unknown_keyword() + public void Can_get_unknown_keyword_that_was_set_earlier() { - var builder = new RabbitMQConnectionStringBuilder(); - - Action action = () => builder["bad"] = "some"; + var builder = new RabbitMQConnectionStringBuilder + { + ["some"] = "other" + }; - action.Should().ThrowExactly().WithMessage("Keyword not supported: 'bad'.*"); + object? value = builder["some"]; + value.Should().Be("other"); } } diff --git a/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectorTest.cs b/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectorTest.cs index ebca84c1fe..6c944dc0b1 100644 --- a/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/RabbitMQ/RabbitMQConnectorTest.cs @@ -190,8 +190,8 @@ public async Task Binds_options_without_service_bindings() { var appSettings = new Dictionary { - ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceOne:ConnectionString"] = "amqp://user1:pass1@host1:5672/virtual-host-1", - ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceTwo:ConnectionString"] = "amqps://user2:pass2@host2:5672/virtual-host-2" + ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceOne:ConnectionString"] = "amqp://user1:pass1@host1:5672/virtual-host-1?heartbeat=5", + ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceTwo:ConnectionString"] = "amqps://user2:pass2@host2:5672/virtual-host-2?connection_timeout=5000" }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); @@ -203,10 +203,10 @@ public async Task Binds_options_without_service_bindings() var optionsSnapshot = scope.ServiceProvider.GetRequiredService>(); RabbitMQOptions optionsOne = optionsSnapshot.Get("myRabbitMQServiceOne"); - optionsOne.ConnectionString.Should().Be("amqp://user1:pass1@host1:5672/virtual-host-1"); + optionsOne.ConnectionString.Should().Be("amqp://user1:pass1@host1:5672/virtual-host-1?heartbeat=5"); RabbitMQOptions optionsTwo = optionsSnapshot.Get("myRabbitMQServiceTwo"); - optionsTwo.ConnectionString.Should().Be("amqps://user2:pass2@host2:5672/virtual-host-2"); + optionsTwo.ConnectionString.Should().Be("amqps://user2:pass2@host2:5672/virtual-host-2?connection_timeout=5000"); } [Fact] @@ -214,13 +214,12 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() { var appSettings = new Dictionary { - ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceOne:ConnectionString"] = "amqps://user:pass@localhost:5672" + ["Steeltoe:Client:RabbitMQ:myRabbitMQServiceOne:ConnectionString"] = "amqps://user:pass@localhost:5672?connection_timeout=5000&heartbeat=5" }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddRabbitMQ(); + builder.AddRabbitMQ(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -228,7 +227,7 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() RabbitMQOptions optionsOne = optionsMonitor.Get("myRabbitMQServiceOne"); optionsOne.ConnectionString.Should().Be( - "amqp://d2fd2c9d-ef84-406b-8401-f2ffacaafda6:AqntL6IwehKOGssE51psrJYd@q-s0.rabbitmq-server.benicia-services-subnet.service-instance-377d9d72-e951-4a1c-82e8-99c3c4933368.bosh:5672/377d9d72-e951-4a1c-82e8-99c3c4933368"); + "amqp://d2fd2c9d-ef84-406b-8401-f2ffacaafda6:AqntL6IwehKOGssE51psrJYd@q-s0.rabbitmq-server.benicia-services-subnet.service-instance-377d9d72-e951-4a1c-82e8-99c3c4933368.bosh:5672/377d9d72-e951-4a1c-82e8-99c3c4933368?connection_timeout=5000&heartbeat=5"); RabbitMQOptions optionsTwo = optionsMonitor.Get("myRabbitMQServiceTwo"); @@ -371,7 +370,6 @@ public async Task Can_connect_to_running_server() public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); builder.AddRabbitMQ(null, addOptions => { @@ -382,7 +380,7 @@ public async Task Registers_default_connection_string_when_only_single_server_bi return new FakeConnection(options.ConnectionString); }; - }); + }, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); diff --git a/src/Connectors/test/Connectors.Test/Redis/RedisConnectionStringBuilderTest.cs b/src/Connectors/test/Connectors.Test/Redis/RedisConnectionStringBuilderTest.cs index f04440d3ed..e6dfa388fc 100644 --- a/src/Connectors/test/Connectors.Test/Redis/RedisConnectionStringBuilderTest.cs +++ b/src/Connectors/test/Connectors.Test/Redis/RedisConnectionStringBuilderTest.cs @@ -46,13 +46,13 @@ public void Returns_null_when_getting_known_keyword() } [Fact] - public void Throws_when_getting_unknown_keyword() + public void Returns_null_when_getting_unknown_keyword() { var builder = new RedisConnectionStringBuilder(); - Action action = () => _ = builder["bad"]; + object? some = builder["some"]; - action.Should().ThrowExactly().WithMessage("Keyword not supported: 'bad'.*"); + some.Should().BeNull(); } [Fact] diff --git a/src/Connectors/test/Connectors.Test/Redis/RedisConnectorTest.cs b/src/Connectors/test/Connectors.Test/Redis/RedisConnectorTest.cs index afd08db3f1..5d5e23f609 100644 --- a/src/Connectors/test/Connectors.Test/Redis/RedisConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/Redis/RedisConnectorTest.cs @@ -131,9 +131,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddRedis(); + builder.AddRedis(null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -308,7 +307,6 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); builder.AddRedis(null, addOptions => { @@ -319,7 +317,7 @@ public async Task Registers_default_connection_string_when_only_single_server_bi return GetMockedConnectionMultiplexer(options.ConnectionString); }; - }); + }, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); diff --git a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs index 267d293dac..42da1e5d2d 100644 --- a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs +++ b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs @@ -111,7 +111,10 @@ public async Task SQLServer_Not_Connected_Returns_Down_Status() result.Description.Should().Be("SQL Server health check failed"); result.Details.Should().Contain("host", "localhost"); result.Details.Should().Contain("service", "Example"); - result.Details.Should().ContainKey("error").WhoseValue.As().Should().StartWith("SqlException: Connection Timeout Expired."); + + result.Details.Should().ContainKey("error").WhoseValue.As().Should().Match(exception => + exception.StartsWith("SqlException: Connection Timeout Expired.", StringComparison.Ordinal) || + exception.StartsWith("SqlException: A network-related or instance-specific error", StringComparison.Ordinal)); } [Fact(Skip = "Integration test - Requires local SQL Server instance")] diff --git a/src/Connectors/test/Connectors.Test/SqlServer/MicrosoftData/SqlServerConnectorTest.cs b/src/Connectors/test/Connectors.Test/SqlServer/MicrosoftData/SqlServerConnectorTest.cs index 506646654f..35ed4b9ad2 100644 --- a/src/Connectors/test/Connectors.Test/SqlServer/MicrosoftData/SqlServerConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/SqlServer/MicrosoftData/SqlServerConnectorTest.cs @@ -147,7 +147,7 @@ public async Task Binds_options_without_service_bindings() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); builder.Services.Configure("mySqlServerServiceOne", options => options.ConnectionString += ";Encrypt=false"); await using WebApplication app = builder.Build(); @@ -185,9 +185,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -225,7 +224,7 @@ public async Task Registers_ConnectorFactory() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -252,7 +251,7 @@ public async Task Registers_HealthContributors() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); await using WebApplication app = builder.Build(); RelationalDatabaseHealthContributor[] contributors = @@ -273,8 +272,7 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -302,7 +300,7 @@ public async Task Registers_default_connection_string_when_only_default_client_b WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); diff --git a/src/Connectors/test/Connectors.Test/SqlServer/SystemData/SqlServerConnectorTest.cs b/src/Connectors/test/Connectors.Test/SqlServer/SystemData/SqlServerConnectorTest.cs index 42972a6339..3dd16da833 100644 --- a/src/Connectors/test/Connectors.Test/SqlServer/SystemData/SqlServerConnectorTest.cs +++ b/src/Connectors/test/Connectors.Test/SqlServer/SystemData/SqlServerConnectorTest.cs @@ -149,7 +149,7 @@ public async Task Binds_options_without_service_bindings() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, null); builder.Services.Configure("mySqlServerServiceOne", options => options.ConnectionString += ";Encrypt=false"); await using WebApplication app = builder.Build(); @@ -187,9 +187,8 @@ public async Task Binds_options_with_CloudFoundry_service_bindings() }; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(MultiVcapServicesJson)); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, new StringServiceBindingsReader(MultiVcapServicesJson)); await using WebApplication app = builder.Build(); var optionsMonitor = app.Services.GetRequiredService>(); @@ -227,7 +226,7 @@ public async Task Registers_ConnectorFactory() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -254,7 +253,7 @@ public async Task Registers_HealthContributors() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, null); await using WebApplication app = builder.Build(); RelationalDatabaseHealthContributor[] contributors = @@ -275,8 +274,7 @@ .. app.Services.GetServices().Should().HaveCount(2).And.AllB public async Task Registers_default_connection_string_when_only_single_server_binding_found() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddCloudFoundryServiceBindings(new StringServiceBindingsReader(SingleVcapServicesJson)); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, new StringServiceBindingsReader(SingleVcapServicesJson)); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); @@ -304,7 +302,7 @@ public async Task Registers_default_connection_string_when_only_default_client_b WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.SystemDataOnly, null, null, null); await using WebApplication app = builder.Build(); var connectorFactory = app.Services.GetRequiredService>(); diff --git a/src/Connectors/test/Connectors.Test/Steeltoe.Connectors.Test.csproj b/src/Connectors/test/Connectors.Test/Steeltoe.Connectors.Test.csproj index b7414c8f5d..bf2405d3f8 100644 --- a/src/Connectors/test/Connectors.Test/Steeltoe.Connectors.Test.csproj +++ b/src/Connectors/test/Connectors.Test/Steeltoe.Connectors.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Connectors/test/EntityFrameworkCore.Test/MySql/Oracle/MySqlDbContextOptionsBuilderExtensionsTest.cs b/src/Connectors/test/EntityFrameworkCore.Test/MySql/Oracle/MySqlDbContextOptionsBuilderExtensionsTest.cs index bbab683b37..735addf178 100644 --- a/src/Connectors/test/EntityFrameworkCore.Test/MySql/Oracle/MySqlDbContextOptionsBuilderExtensionsTest.cs +++ b/src/Connectors/test/EntityFrameworkCore.Test/MySql/Oracle/MySqlDbContextOptionsBuilderExtensionsTest.cs @@ -26,7 +26,7 @@ public async Task Registers_connection_string_for_default_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); builder.Services.Configure(options => options.ConnectionString += ";Use Compression=false"); builder.Services.AddDbContext((serviceProvider, options) => SteeltoeExtensions.UseMySql(options, serviceProvider, @@ -51,7 +51,7 @@ public async Task Registers_connection_string_for_named_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.OracleOnly); + builder.AddMySql(MySqlPackageResolver.OracleOnly, null, null, null); builder.Services.Configure("myMySqlService", options => options.ConnectionString += ";Use Compression=false"); builder.Services.AddDbContext((serviceProvider, options) => SteeltoeExtensions.UseMySql(options, serviceProvider, diff --git a/src/Connectors/test/EntityFrameworkCore.Test/MySql/Pomelo/MySqlDbContextOptionsBuilderExtensionsTest.cs b/src/Connectors/test/EntityFrameworkCore.Test/MySql/Pomelo/MySqlDbContextOptionsBuilderExtensionsTest.cs index 048e5f9085..7e8e974935 100644 --- a/src/Connectors/test/EntityFrameworkCore.Test/MySql/Pomelo/MySqlDbContextOptionsBuilderExtensionsTest.cs +++ b/src/Connectors/test/EntityFrameworkCore.Test/MySql/Pomelo/MySqlDbContextOptionsBuilderExtensionsTest.cs @@ -16,7 +16,11 @@ namespace Steeltoe.Connectors.EntityFrameworkCore.Test.MySql.Pomelo; public sealed class MySqlDbContextOptionsBuilderExtensionsTest { +#if NET10_0_OR_GREATER + [Fact(Skip = "Temporary workaround: Unstable EF Core 10 package for Pomelo.EntityFrameworkCore.MySql is not available yet.")] +#else [Fact] +#endif public async Task Registers_connection_string_for_default_service_binding() { var appSettings = new Dictionary @@ -26,7 +30,7 @@ public async Task Registers_connection_string_for_default_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); builder.Services.Configure(options => options.ConnectionString += ";Use Compression=false"); builder.Services.AddDbContext((serviceProvider, options) => SteeltoeExtensions.UseMySql(options, serviceProvider, @@ -42,7 +46,11 @@ public async Task Registers_connection_string_for_default_service_binding() "Server=localhost;User ID=steeltoe;Password=steeltoe;Database=myDb;Allow User Variables=True;Connection Timeout=15;Use Affected Rows=False;Use Compression=False"); } +#if NET10_0_OR_GREATER + [Fact(Skip = "Temporary workaround: Unstable EF Core 10 package for Pomelo.EntityFrameworkCore.MySql is not available yet.")] +#else [Fact] +#endif public async Task Registers_connection_string_for_named_service_binding() { var appSettings = new Dictionary @@ -52,7 +60,7 @@ public async Task Registers_connection_string_for_named_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); builder.Services.Configure("myMySqlService", options => options.ConnectionString += ";Use Compression=false"); builder.Services.AddDbContext((serviceProvider, options) => SteeltoeExtensions.UseMySql(options, serviceProvider, @@ -72,7 +80,7 @@ public async Task Registers_connection_string_for_named_service_binding() public async Task Throws_for_missing_connection_string_with_version_detection() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly); + builder.AddMySql(MySqlPackageResolver.MySqlConnectorOnly, null, null, null); builder.Services.AddDbContext((serviceProvider, options) => SteeltoeExtensions.UseMySql(options, serviceProvider, MySqlEntityFrameworkCorePackageResolver.PomeloOnly)); diff --git a/src/Connectors/test/EntityFrameworkCore.Test/SqlServer/SqlServerDbContextOptionsBuilderExtensionsTest.cs b/src/Connectors/test/EntityFrameworkCore.Test/SqlServer/SqlServerDbContextOptionsBuilderExtensionsTest.cs index 66d5001276..ab6df7b404 100644 --- a/src/Connectors/test/EntityFrameworkCore.Test/SqlServer/SqlServerDbContextOptionsBuilderExtensionsTest.cs +++ b/src/Connectors/test/EntityFrameworkCore.Test/SqlServer/SqlServerDbContextOptionsBuilderExtensionsTest.cs @@ -25,7 +25,7 @@ public async Task Registers_connection_string_for_default_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); builder.Services.Configure(options => options.ConnectionString += ";Encrypt=false"); builder.Services.AddDbContext((serviceProvider, options) => options.UseSqlServer(serviceProvider)); await using WebApplication app = builder.Build(); @@ -34,7 +34,7 @@ public async Task Registers_connection_string_for_default_service_binding() await using var dbContext = scope.ServiceProvider.GetRequiredService(); string? connectionString = dbContext.Database.GetConnectionString(); - connectionString.Should().Be("Data Source=localhost;Initial Catalog=myDb;User ID=steeltoe;Password=steeltoe;Max Pool Size=50;Encrypt=false"); + connectionString.Should().StartWith("Data Source=localhost;Initial Catalog=myDb;User ID=steeltoe;Password=steeltoe;Max Pool Size=50;Encrypt="); } [Fact] @@ -47,7 +47,7 @@ public async Task Registers_connection_string_for_named_service_binding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); - builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly); + builder.AddSqlServer(SqlServerPackageResolver.MicrosoftDataOnly, null, null, null); builder.Services.Configure("mySqlServerService", options => options.ConnectionString += ";Encrypt=false"); builder.Services.AddDbContext((serviceProvider, options) => options.UseSqlServer(serviceProvider, "mySqlServerService")); await using WebApplication app = builder.Build(); @@ -56,6 +56,6 @@ public async Task Registers_connection_string_for_named_service_binding() await using var dbContext = scope.ServiceProvider.GetRequiredService(); string? connectionString = dbContext.Database.GetConnectionString(); - connectionString.Should().Be("Data Source=localhost;Initial Catalog=myDb;User ID=steeltoe;Password=steeltoe;Max Pool Size=50;Encrypt=false"); + connectionString.Should().StartWith("Data Source=localhost;Initial Catalog=myDb;User ID=steeltoe;Password=steeltoe;Max Pool Size=50;Encrypt="); } } diff --git a/src/Connectors/test/EntityFrameworkCore.Test/Steeltoe.Connectors.EntityFrameworkCore.Test.csproj b/src/Connectors/test/EntityFrameworkCore.Test/Steeltoe.Connectors.EntityFrameworkCore.Test.csproj index ab56fa7758..626d5effb2 100644 --- a/src/Connectors/test/EntityFrameworkCore.Test/Steeltoe.Connectors.EntityFrameworkCore.Test.csproj +++ b/src/Connectors/test/EntityFrameworkCore.Test/Steeltoe.Connectors.EntityFrameworkCore.Test.csproj @@ -1,16 +1,27 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 + + + + $(NoWarn);NU1608 + + + + - + diff --git a/src/Discovery/src/Configuration/ConfigurationDiscoveryClient.cs b/src/Discovery/src/Configuration/ConfigurationDiscoveryClient.cs index 86aaad4529..37a787c777 100644 --- a/src/Discovery/src/Configuration/ConfigurationDiscoveryClient.cs +++ b/src/Discovery/src/Configuration/ConfigurationDiscoveryClient.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.Collections.ObjectModel; using Microsoft.Extensions.Options; using Steeltoe.Common.Discovery; @@ -10,17 +11,69 @@ namespace Steeltoe.Discovery.Configuration; /// /// A discovery client that reads service instances from app configuration. /// -public sealed class ConfigurationDiscoveryClient : IDiscoveryClient +public sealed class ConfigurationDiscoveryClient : IDiscoveryClient, IDisposable { private readonly IOptionsMonitor _optionsMonitor; + private readonly IDisposable? _changeTokenRegistration; public string Description => "A discovery client that returns service instances from app configuration."; + /// + /// Occurs when the configuration of service instances has been reloaded. + /// + public event EventHandler? InstancesFetched; + public ConfigurationDiscoveryClient(IOptionsMonitor optionsMonitor) { ArgumentNullException.ThrowIfNull(optionsMonitor); _optionsMonitor = optionsMonitor; + _changeTokenRegistration = optionsMonitor.OnChange(OnOptionsChanged); + } + + private void OnOptionsChanged(ConfigurationDiscoveryOptions options) + { + if (InstancesFetched != null) + { + ReadOnlyDictionary> instancesByServiceId = ToServiceInstanceMap(options.Services); + var eventArgs = new DiscoveryInstancesFetchedEventArgs(instancesByServiceId); + RaiseFetchEvent(eventArgs); + } + } + + private static ReadOnlyDictionary> ToServiceInstanceMap(IList services) + { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:wrap_before_first_method_call true + + return services + .Where(service => service.ServiceId != null) + .GroupBy(service => service.ServiceId!, StringComparer.OrdinalIgnoreCase) + .ToDictionary(grouping => grouping.Key, grouping => (IReadOnlyList)grouping + .Cast() + .ToList() + .AsReadOnly(), StringComparer.OrdinalIgnoreCase) + .AsReadOnly(); + + // @formatter:wrap_before_first_method_call restore + // @formatter:wrap_chained_method_calls restore + } + + private void RaiseFetchEvent(DiscoveryInstancesFetchedEventArgs eventArgs) + { + // Execute on separate thread, so we won't block the configuration system in case the handler logic is expensive. + ThreadPool.QueueUserWorkItem(_ => + { + try + { + InstancesFetched?.Invoke(this, eventArgs); + } + catch (Exception) + { + // Intentionally left empty. Adding a logger to the constructor is a breaking change. + // Adding an extra constructor confuses the service container. + } + }); } /// @@ -54,4 +107,10 @@ public Task ShutdownAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } + + /// + public void Dispose() + { + _changeTokenRegistration?.Dispose(); + } } diff --git a/src/Discovery/src/Configuration/ConfigurationServiceCollectionExtensions.cs b/src/Discovery/src/Configuration/ConfigurationServiceCollectionExtensions.cs index e2692969e7..4d5bd83405 100644 --- a/src/Discovery/src/Configuration/ConfigurationServiceCollectionExtensions.cs +++ b/src/Discovery/src/Configuration/ConfigurationServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Steeltoe.Common; using Steeltoe.Common.Discovery; namespace Steeltoe.Discovery.Configuration; @@ -49,11 +50,19 @@ public static IServiceCollection AddConfigurationDiscoveryClient(this IServiceCo { ArgumentNullException.ThrowIfNull(services); - services.AddOptions().BindConfiguration(ConfigurationDiscoveryOptions.ConfigurationPrefix); + if (!IsRegistered(services)) + { + services.AddOptions().BindConfiguration(ConfigurationDiscoveryOptions.ConfigurationPrefix); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.AddHostedService(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.AddHostedService(); + } return services; } + + private static bool IsRegistered(IServiceCollection services) + { + return services.Any(descriptor => descriptor.SafeGetImplementationType() == typeof(ConfigurationDiscoveryClient)); + } } diff --git a/src/Discovery/src/Configuration/ConfigurationServiceInstance.cs b/src/Discovery/src/Configuration/ConfigurationServiceInstance.cs index 4b96049165..a5dd35b917 100644 --- a/src/Discovery/src/Configuration/ConfigurationServiceInstance.cs +++ b/src/Discovery/src/Configuration/ConfigurationServiceInstance.cs @@ -22,6 +22,9 @@ public sealed class ConfigurationServiceInstance : IServiceInstance [Required] public string? ServiceId { get; set; } + /// + public string InstanceId => string.Empty; + /// [Required] public string? Host { get; set; } @@ -35,6 +38,12 @@ public sealed class ConfigurationServiceInstance : IServiceInstance /// public Uri Uri => new($"{(IsSecure ? Uri.UriSchemeHttps : Uri.UriSchemeHttp)}{Uri.SchemeDelimiter}{Host}:{Port}"); + /// + public Uri? NonSecureUri => IsSecure ? null : Uri; + + /// + public Uri? SecureUri => IsSecure ? Uri : null; + /// public IDictionary Metadata { get; } = new Dictionary(); } diff --git a/src/Discovery/src/Configuration/PublicAPI.Shipped.txt b/src/Discovery/src/Configuration/PublicAPI.Shipped.txt index 103d547fe5..be721725bb 100644 --- a/src/Discovery/src/Configuration/PublicAPI.Shipped.txt +++ b/src/Discovery/src/Configuration/PublicAPI.Shipped.txt @@ -3,9 +3,11 @@ static Steeltoe.Discovery.Configuration.ConfigurationServiceCollectionExtensions Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.ConfigurationDiscoveryClient(Microsoft.Extensions.Options.IOptionsMonitor! optionsMonitor) -> void Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.Description.get -> string! +Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.Dispose() -> void Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.GetInstancesAsync(string! serviceId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.GetLocalServiceInstance() -> Steeltoe.Common.Discovery.IServiceInstance? Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.GetServiceIdsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.InstancesFetched -> System.EventHandler? Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.ShutdownAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Steeltoe.Discovery.Configuration.ConfigurationDiscoveryOptions Steeltoe.Discovery.Configuration.ConfigurationDiscoveryOptions.ConfigurationDiscoveryOptions() -> void @@ -15,11 +17,14 @@ Steeltoe.Discovery.Configuration.ConfigurationServiceInstance Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.ConfigurationServiceInstance() -> void Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Host.get -> string? Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Host.set -> void +Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.InstanceId.get -> string! Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.IsSecure.get -> bool Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.IsSecure.set -> void Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Metadata.get -> System.Collections.Generic.IDictionary! +Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.NonSecureUri.get -> System.Uri? Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Port.get -> int Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Port.set -> void +Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.SecureUri.get -> System.Uri? Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.ServiceId.get -> string? Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.ServiceId.set -> void Steeltoe.Discovery.Configuration.ConfigurationServiceInstance.Uri.get -> System.Uri! diff --git a/src/Discovery/src/Configuration/Steeltoe.Discovery.Configuration.csproj b/src/Discovery/src/Configuration/Steeltoe.Discovery.Configuration.csproj index 6cca535d40..db968e9da4 100644 --- a/src/Discovery/src/Configuration/Steeltoe.Discovery.Configuration.csproj +++ b/src/Discovery/src/Configuration/Steeltoe.Discovery.Configuration.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Client for service discovery from application configuration. service-discovery;service-registry;configuration-based true diff --git a/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs b/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs index 430a479aa8..93e3b565f5 100644 --- a/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs +++ b/src/Discovery/src/Consul/Configuration/ConsulDiscoveryOptions.cs @@ -15,6 +15,7 @@ public sealed class ConsulDiscoveryOptions internal bool IsHeartbeatEnabled => Heartbeat is { Enabled: true }; internal bool IsRetryEnabled => Retry is { Enabled: true }; + internal string EffectiveScheme => Scheme ?? "http"; /// /// Gets or sets a value indicating whether to enable the Consul client. Default value: true. @@ -58,9 +59,9 @@ public sealed class ConsulDiscoveryOptions public bool QueryPassing { get; set; } = true; /// - /// Gets or sets the scheme to register the running app with ("http" or "https"). Default value: http. + /// Gets or sets the scheme to register the running app with ("http" or "https"). /// - public string? Scheme { get; set; } = "http"; + public string? Scheme { get; set; } /// /// Gets or sets a value indicating whether to enable periodic health checking for the running app. Default value: true. @@ -174,6 +175,8 @@ public sealed class ConsulDiscoveryOptions /// /// Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true. + /// + /// This property is ignored when or is explicitly configured. /// public bool UseAspNetCoreUrls { get; set; } = true; } diff --git a/src/Discovery/src/Consul/ConfigurationSchema.json b/src/Discovery/src/Consul/ConfigurationSchema.json index 74e9bf9afe..58acfc8ab2 100644 --- a/src/Discovery/src/Consul/ConfigurationSchema.json +++ b/src/Discovery/src/Consul/ConfigurationSchema.json @@ -173,7 +173,7 @@ }, "Scheme": { "type": "string", - "description": "Gets or sets the scheme to register the running app with (\"http\" or \"https\"). Default value: http." + "description": "Gets or sets the scheme to register the running app with (\"http\" or \"https\")." }, "ServiceName": { "type": "string", @@ -188,7 +188,7 @@ }, "UseAspNetCoreUrls": { "type": "boolean", - "description": "Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true." + "description": "Gets or sets a value indicating whether to register with the port number ASP.NET Core is listening on. Default value: true.\n\nThis property is ignored when 'Steeltoe.Discovery.Consul.Configuration.ConsulDiscoveryOptions.Port' or 'Steeltoe.Discovery.Consul.Configuration.ConsulDiscoveryOptions.Scheme' is explicitly configured." }, "UseNetworkInterfaces": { "type": "boolean", diff --git a/src/Discovery/src/Consul/ConsulDiscoveryClient.cs b/src/Discovery/src/Consul/ConsulDiscoveryClient.cs index 22d3df0ef3..2521aba036 100644 --- a/src/Discovery/src/Consul/ConsulDiscoveryClient.cs +++ b/src/Discovery/src/Consul/ConsulDiscoveryClient.cs @@ -30,6 +30,13 @@ public sealed class ConsulDiscoveryClient : IDiscoveryClient /// public string Description => "A discovery client for HashiCorp Consul."; + /// + /// This event is never raised. The Consul client doesn't implement caching. + /// +#pragma warning disable CS0067 // The event is never used + public event EventHandler? InstancesFetched; +#pragma warning restore CS0067 // The event is never used + /// /// Initializes a new instance of the class. /// diff --git a/src/Discovery/src/Consul/ConsulServiceCollectionExtensions.cs b/src/Discovery/src/Consul/ConsulServiceCollectionExtensions.cs index 93fc2f924d..bb1fdc71c2 100644 --- a/src/Discovery/src/Consul/ConsulServiceCollectionExtensions.cs +++ b/src/Discovery/src/Consul/ConsulServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Steeltoe.Common; using Steeltoe.Common.Discovery; using Steeltoe.Common.Extensions; using Steeltoe.Common.HealthChecks; @@ -32,12 +33,20 @@ public static IServiceCollection AddConsulDiscoveryClient(this IServiceCollectio { ArgumentNullException.ThrowIfNull(services); - ConfigureConsulServices(services); - AddConsulServices(services); + if (!IsRegistered(services)) + { + ConfigureConsulServices(services); + AddConsulServices(services); + } return services; } + private static bool IsRegistered(IServiceCollection services) + { + return services.Any(descriptor => descriptor.SafeGetImplementationType() == typeof(PostConfigureConsulDiscoveryOptions)); + } + private static void ConfigureConsulServices(IServiceCollection services) { services.AddApplicationInstanceInfo(); @@ -96,6 +105,6 @@ private static void AddConsulServices(IServiceCollection services) services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.AddHostedService(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.AddSingleton(); } } diff --git a/src/Discovery/src/Consul/ConsulServiceInstance.cs b/src/Discovery/src/Consul/ConsulServiceInstance.cs index 5c31174d2f..bb8d5d626b 100644 --- a/src/Discovery/src/Consul/ConsulServiceInstance.cs +++ b/src/Discovery/src/Consul/ConsulServiceInstance.cs @@ -13,9 +13,15 @@ namespace Steeltoe.Discovery.Consul; /// internal sealed class ConsulServiceInstance : IServiceInstance { + private static readonly IReadOnlyList EmptyStringList = Array.Empty(); + private static readonly IReadOnlyDictionary EmptyStringDictionary = new Dictionary().AsReadOnly(); + /// public string ServiceId { get; } + /// + public string InstanceId { get; } + /// public string Host { get; } @@ -28,6 +34,12 @@ internal sealed class ConsulServiceInstance : IServiceInstance /// public Uri Uri { get; } + /// + public Uri? NonSecureUri { get; } + + /// + public Uri? SecureUri { get; } + public IReadOnlyList Tags { get; } /// @@ -44,11 +56,14 @@ internal ConsulServiceInstance(ServiceEntry serviceEntry) ArgumentNullException.ThrowIfNull(serviceEntry); Host = ConsulServerUtils.FindHost(serviceEntry); - Tags = serviceEntry.Service.Tags; - Metadata = serviceEntry.Service.Meta.AsReadOnly(); - IsSecure = serviceEntry.Service.Meta != null && serviceEntry.Service.Meta.TryGetValue("secure", out string? secureString) && bool.Parse(secureString); + Tags = serviceEntry.Service.Tags ?? EmptyStringList; + Metadata = serviceEntry.Service.Meta?.AsReadOnly() ?? EmptyStringDictionary; + IsSecure = Metadata.TryGetValue("secure", out string? secureString) && secureString != null && bool.Parse(secureString); ServiceId = serviceEntry.Service.Service; + InstanceId = serviceEntry.Service.ID; Port = serviceEntry.Service.Port; Uri = new Uri($"{(IsSecure ? "https" : "http")}://{Host}:{Port}"); + NonSecureUri = IsSecure ? null : Uri; + SecureUri = IsSecure ? Uri : null; } } diff --git a/src/Discovery/src/Consul/PeriodicHeartbeat.cs b/src/Discovery/src/Consul/PeriodicHeartbeat.cs index 66a6379932..95d4c3c398 100644 --- a/src/Discovery/src/Consul/PeriodicHeartbeat.cs +++ b/src/Discovery/src/Consul/PeriodicHeartbeat.cs @@ -5,10 +5,11 @@ using Consul; using Microsoft.Extensions.Logging; using Steeltoe.Common.Extensions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul; -internal sealed class PeriodicHeartbeat : IAsyncDisposable +internal sealed partial class PeriodicHeartbeat : IAsyncDisposable { private readonly string _serviceId; private readonly PeriodicTimer _periodicTimer; @@ -37,11 +38,11 @@ private async Task TimerLoopAsync() { try { - _logger.LogDebug("Start sending periodic Consul heartbeats for '{ServiceId}' with interval {Interval}.", _serviceId, Interval); + LogStartSendingHeartbeats(_serviceId, Interval); while (await _periodicTimer.WaitForNextTickAsync(_cancellationTokenSource.Token)) { - _logger.LogDebug("Sending Consul heartbeat for '{ServiceId}'.", _serviceId); + LogSendingHeartbeat(_serviceId); try { @@ -49,16 +50,13 @@ private async Task TimerLoopAsync() } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to send Consul heartbeat for '{ServiceId}'.", _serviceId); + LogFailedToSendHeartbeat(exception, _serviceId); } } } catch (OperationCanceledException) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - // Justification: The exception contains no useful information. Logging it suggests something crashed, while this is expected behavior. - _logger.LogDebug("Stop sending periodic Consul heartbeats for '{ServiceId}'.", _serviceId); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogSendingHeartbeatsStopped(_serviceId); } } @@ -68,7 +66,7 @@ public void ChangeInterval(TimeSpan interval) { _periodicTimer.Period = interval; Interval = interval; - _logger.LogDebug("Periodic Consul heartbeat interval for '{ServiceId}' changed to {Interval}.", _serviceId, interval); + LogHeartbeatIntervalChanged(_serviceId, interval); } } @@ -80,4 +78,19 @@ public async ValueTask DisposeAsync() _task.Dispose(); _periodicTimer.Dispose(); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Start sending periodic Consul heartbeats for '{ServiceId}' with interval {Interval}.")] + private partial void LogStartSendingHeartbeats(string serviceId, TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending Consul heartbeat for '{ServiceId}'.")] + private partial void LogSendingHeartbeat(string serviceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to send Consul heartbeat for '{ServiceId}'.")] + private partial void LogFailedToSendHeartbeat(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Stopped sending periodic Consul heartbeats for '{ServiceId}'.")] + private partial void LogSendingHeartbeatsStopped(string serviceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Periodic Consul heartbeat interval for '{ServiceId}' changed to {Interval}.")] + private partial void LogHeartbeatIntervalChanged(string serviceId, TimeSpan interval); } diff --git a/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs b/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs index 72c3a55856..424cec5f9f 100644 --- a/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs +++ b/src/Discovery/src/Consul/PostConfigureConsulDiscoveryOptions.cs @@ -62,10 +62,10 @@ public void PostConfigure(string? name, ConsulDiscoveryOptions options) options.HostName = options.IPAddress; } - if (options.Port == 0) + if (options is { UseAspNetCoreUrls: true, Port: 0, Scheme: null }) { ICollection addresses = _configuration.GetListenAddresses(); - SetPortsFromListenAddresses(options, addresses); + SetSchemeWithPortFromListenAddresses(options, addresses); } options.InstanceId = GetInstanceId(options); @@ -77,36 +77,35 @@ private string GetServiceName(ConsulDiscoveryOptions options) return NormalizeForConsul(serviceName, nameof(ConsulDiscoveryOptions.ServiceName)); } - private void SetPortsFromListenAddresses(ConsulDiscoveryOptions options, IEnumerable listenOnAddresses) + private void SetSchemeWithPortFromListenAddresses(ConsulDiscoveryOptions options, IEnumerable listenOnAddresses) { - // Try to pull some values out of server configuration to override defaults, but only if not using NetUtils. - // If NetUtils are configured, the user probably wants to define their own behavior. - if (options is { UseAspNetCoreUrls: true, Port: 0 }) + int? listenHttpPort = null; + int? listenHttpsPort = null; + + foreach (string address in listenOnAddresses) { - int? listenHttpPort = null; - int? listenHttpsPort = null; + BindingAddress bindingAddress = BindingAddress.Parse(address); - foreach (string address in listenOnAddresses) + if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) { - BindingAddress bindingAddress = BindingAddress.Parse(address); - - if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) - { - listenHttpPort = bindingAddress.Port; - } - else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) - { - listenHttpsPort = bindingAddress.Port; - } + listenHttpPort = bindingAddress.Port; } - - int? listenPort = listenHttpsPort ?? listenHttpPort; - - if (listenPort != null) + else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) { - options.Port = listenPort.Value; + listenHttpsPort = bindingAddress.Port; } } + + if (listenHttpsPort != null) + { + options.Port = listenHttpsPort.Value; + options.Scheme = "https"; + } + else if (listenHttpPort != null) + { + options.Port = listenHttpPort.Value; + options.Scheme = "http"; + } } private string GetInstanceId(ConsulDiscoveryOptions options) diff --git a/src/Discovery/src/Consul/PublicAPI.Shipped.txt b/src/Discovery/src/Consul/PublicAPI.Shipped.txt index 9c94915e9a..e3e9916c3d 100644 --- a/src/Discovery/src/Consul/PublicAPI.Shipped.txt +++ b/src/Discovery/src/Consul/PublicAPI.Shipped.txt @@ -106,5 +106,6 @@ Steeltoe.Discovery.Consul.ConsulDiscoveryClient.GetInstancesAsync(string! servic Steeltoe.Discovery.Consul.ConsulDiscoveryClient.GetLocalServiceInstance() -> Steeltoe.Common.Discovery.IServiceInstance? Steeltoe.Discovery.Consul.ConsulDiscoveryClient.GetServiceIdsAsync(string? dataCenter, Consul.Filtering.Filter? filter, Consul.QueryOptions! queryOptions, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! Steeltoe.Discovery.Consul.ConsulDiscoveryClient.GetServiceIdsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Steeltoe.Discovery.Consul.ConsulDiscoveryClient.InstancesFetched -> System.EventHandler? Steeltoe.Discovery.Consul.ConsulDiscoveryClient.ShutdownAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Steeltoe.Discovery.Consul.ConsulServiceCollectionExtensions diff --git a/src/Discovery/src/Consul/Registry/ConsulRegistration.cs b/src/Discovery/src/Consul/Registry/ConsulRegistration.cs index d51902e6d7..81bd573afc 100644 --- a/src/Discovery/src/Consul/Registry/ConsulRegistration.cs +++ b/src/Discovery/src/Consul/Registry/ConsulRegistration.cs @@ -22,9 +22,7 @@ internal sealed class ConsulRegistration : IServiceInstance /// public string ServiceId { get; } - /// - /// Gets the instance ID as registered by the Consul server. - /// + /// public string InstanceId { get; } /// @@ -34,10 +32,16 @@ internal sealed class ConsulRegistration : IServiceInstance public int Port { get; } /// - public bool IsSecure => _optionsMonitor.CurrentValue.Scheme == "https"; + public bool IsSecure => _optionsMonitor.CurrentValue.EffectiveScheme == "https"; + + /// + public Uri Uri => FormatUri(); + + /// + public Uri? NonSecureUri => IsSecure ? null : Uri; /// - public Uri Uri => new($"{_optionsMonitor.CurrentValue.Scheme}://{Host}:{Port}"); + public Uri? SecureUri => IsSecure ? Uri : null; public IReadOnlyList Tags { get; } @@ -69,6 +73,20 @@ internal ConsulRegistration(AgentServiceRegistration innerRegistration, IOptions Metadata = innerRegistration.Meta.AsReadOnly(); } + private Uri FormatUri() + { + string scheme = _optionsMonitor.CurrentValue.EffectiveScheme; + + try + { + return new Uri($"{scheme}://{Host}:{Port}"); + } + catch (UriFormatException exception) + { + throw new UriFormatException($"Failed to build URI from components. Scheme={scheme}, Host={Host},Port={Port}.", exception); + } + } + /// /// Creates a registration for the currently running app, to be submitted to the Consul server. /// @@ -114,7 +132,7 @@ private static Dictionary CreateMetadata(ConsulDiscoveryOptions } // store the secure flag in the metadata so that clients will be able to figure out whether to use http or https automatically - metadata.TryAdd("secure", options.Scheme == "https" ? "true" : "false"); + metadata.TryAdd("secure", options.EffectiveScheme == "https" ? "true" : "false"); return metadata; } @@ -145,7 +163,7 @@ public static AgentServiceCheck CreateCheck(int port, ConsulDiscoveryOptions opt } else { - var uri = new Uri($"{options.Scheme}://{options.HostName}:{port}{options.HealthCheckPath}"); + var uri = new Uri($"{options.EffectiveScheme}://{options.HostName}:{port}{options.HealthCheckPath}"); check.HTTP = uri.ToString(); } diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs index e7d264f9a2..17b8c93391 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Discovery.Consul.Registry; /// /// A registrar used to register a service in a Consul server. /// -internal sealed class ConsulServiceRegistrar : IAsyncDisposable +internal sealed partial class ConsulServiceRegistrar : IAsyncDisposable { private const int NotRunning = 0; private const int Running = 1; @@ -73,7 +73,7 @@ public async Task StartAsync(CancellationToken cancellationToken) { if (!Options.Enabled) { - _logger.LogDebug("Consul discovery client is turned off."); + LogDiscoveryClientTurnedOff(); return; } @@ -94,7 +94,7 @@ private async Task RegisterAsync(CancellationToken cancellationToken) { if (!Options.Register) { - _logger.LogDebug("Consul registration is turned off."); + LogRegistrationTurnedOff(); return; } @@ -105,7 +105,7 @@ private async Task DeregisterAsync(CancellationToken cancellationToken) { if (!Options.Register || !Options.Deregister) { - _logger.LogDebug("Consul deregistration is turned off."); + LogDeregistrationTurnedOff(); return; } @@ -116,7 +116,7 @@ private async Task DoWithRetryAsync(Func retryable, Con { ArgumentNullException.ThrowIfNull(retryable); - _logger.LogDebug("Starting retryable action."); + LogStartingRetryableAction(); int attempts = 0; int backOff = options.InitialInterval; @@ -126,7 +126,7 @@ private async Task DoWithRetryAsync(Func retryable, Con try { await retryable(cancellationToken); - _logger.LogDebug("Finished retryable action."); + LogRetryableActionFinished(); return; } catch (Exception exception) when (!exception.IsCancellation()) @@ -135,14 +135,14 @@ private async Task DoWithRetryAsync(Func retryable, Con if (attempts < options.MaxAttempts) { - _logger.LogError(exception, "Exception during {Attempt} attempts of retryable action, will retry", attempts); - Thread.CurrentThread.Join(backOff); + LogStartingRetry(exception, attempts); + await Task.Delay(backOff, cancellationToken); int nextBackOff = (int)(backOff * options.Multiplier); backOff = Math.Min(nextBackOff, options.MaxInterval); } else { - _logger.LogError(exception, "Exception during {Attempt} attempts of retryable action, done with retry", attempts); + LogRetryFailed(exception, attempts); throw; } } @@ -164,4 +164,25 @@ public async ValueTask DisposeAsync() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul discovery client is turned off.")] + private partial void LogDiscoveryClientTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul registration is turned off.")] + private partial void LogRegistrationTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Consul deregistration is turned off.")] + private partial void LogDeregistrationTurnedOff(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting retryable action.")] + private partial void LogStartingRetryableAction(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Finished retryable action.")] + private partial void LogRetryableActionFinished(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Exception during {Attempt} attempts of retryable action, will retry.")] + private partial void LogStartingRetry(Exception exception, int attempt); + + [LoggerMessage(Level = LogLevel.Error, Message = "Exception during {Attempt} attempts of retryable action, done with retries.")] + private partial void LogRetryFailed(Exception exception, int attempt); } diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs index 2809e500ee..a18b270df8 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistry.cs @@ -8,13 +8,14 @@ using Microsoft.Extensions.Options; using Steeltoe.Common.Extensions; using Steeltoe.Discovery.Consul.Configuration; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul.Registry; /// /// A service registry that uses Consul. /// -internal sealed class ConsulServiceRegistry : IAsyncDisposable +internal sealed partial class ConsulServiceRegistry : IAsyncDisposable { private const string Up = "UP"; private const string OutOfService = "OUT_OF_SERVICE"; @@ -67,7 +68,7 @@ public async Task RegisterAsync(ConsulRegistration registration, CancellationTok { ArgumentNullException.ThrowIfNull(registration); - _logger.LogInformation("Registering service {ServiceId} with Consul.", registration.ServiceId); + LogRegistering(registration.ServiceId); try { @@ -82,11 +83,11 @@ public async Task RegisterAsync(ConsulRegistration registration, CancellationTok { if (Options.FailFast) { - _logger.LogError(exception, "Error registering service {ServiceId} with Consul.", registration.ServiceId); + LogRegisterFailed(exception, registration.ServiceId); throw; } - _logger.LogWarning(exception, "FailFast is false. Error registering service {ServiceId} with Consul.", registration.ServiceId); + LogWarnForRegisterFailed(exception, registration.ServiceId); } } @@ -110,12 +111,12 @@ public async Task DeregisterAsync(ConsulRegistration registration, CancellationT await _scheduler.RemoveAsync(registration.InstanceId); } - _logger.LogInformation("Deregistering service {InstanceId} with Consul.", registration.InstanceId); + LogDeregistering(registration.InstanceId); await _client.Agent.ServiceDeregister(registration.InstanceId, cancellationToken); } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Error deregistering service {ServiceId} with Consul.", registration.ServiceId); + LogDeregisterFailed(exception, registration.ServiceId); } } @@ -184,4 +185,19 @@ public async ValueTask DisposeAsync() await _scheduler.DisposeAsync(); } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Registering service {ServiceId} with Consul.")] + private partial void LogRegistering(string serviceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Error registering service {ServiceId} with Consul.")] + private partial void LogRegisterFailed(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Warning, Message = "FailFast is false. Error registering service {ServiceId} with Consul.")] + private partial void LogWarnForRegisterFailed(Exception exception, string serviceId); + + [LoggerMessage(Level = LogLevel.Information, Message = "Deregistering service {InstanceId} with Consul.")] + private partial void LogDeregistering(string instanceId); + + [LoggerMessage(Level = LogLevel.Error, Message = "Error deregistering service {ServiceId} with Consul.")] + private partial void LogDeregisterFailed(Exception exception, string serviceId); } diff --git a/src/Discovery/src/Consul/Steeltoe.Discovery.Consul.csproj b/src/Discovery/src/Consul/Steeltoe.Discovery.Consul.csproj index 965bebd0f1..85ae4ecaff 100644 --- a/src/Discovery/src/Consul/Steeltoe.Discovery.Consul.csproj +++ b/src/Discovery/src/Consul/Steeltoe.Discovery.Consul.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Client for service discovery and registration with Hashicorp Consul. service-discovery;service-registry;Consul;hashicorp true diff --git a/src/Discovery/src/Consul/ThisServiceInstance.cs b/src/Discovery/src/Consul/ThisServiceInstance.cs index 6453bd6909..73a15ce002 100644 --- a/src/Discovery/src/Consul/ThisServiceInstance.cs +++ b/src/Discovery/src/Consul/ThisServiceInstance.cs @@ -15,6 +15,9 @@ internal sealed class ThisServiceInstance : IServiceInstance /// public string ServiceId { get; } + /// + public string InstanceId { get; } + /// public string Host { get; } @@ -27,6 +30,12 @@ internal sealed class ThisServiceInstance : IServiceInstance /// public Uri Uri { get; } + /// + public Uri? NonSecureUri { get; } + + /// + public Uri? SecureUri { get; } + /// public IReadOnlyDictionary Metadata { get; } @@ -35,10 +44,13 @@ public ThisServiceInstance(ConsulRegistration registration) ArgumentNullException.ThrowIfNull(registration); ServiceId = registration.ServiceId; + InstanceId = registration.InstanceId; Host = registration.Host; IsSecure = registration.IsSecure; Port = registration.Port; Metadata = registration.Metadata; Uri = registration.Uri; + NonSecureUri = registration.NonSecureUri; + SecureUri = registration.SecureUri; } } diff --git a/src/Discovery/src/Consul/TtlScheduler.cs b/src/Discovery/src/Consul/TtlScheduler.cs index 972a06342f..792c5a3a0d 100644 --- a/src/Discovery/src/Consul/TtlScheduler.cs +++ b/src/Discovery/src/Consul/TtlScheduler.cs @@ -8,13 +8,14 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Steeltoe.Discovery.Consul.Configuration; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Steeltoe.Discovery.Consul; /// /// Scheduler used to issue TTL (time-to-live) requests to the Consul server. /// -internal sealed class TtlScheduler : IAsyncDisposable +internal sealed partial class TtlScheduler : IAsyncDisposable { private const string InstancePrefix = "service:"; @@ -80,7 +81,7 @@ public void Add(string instanceId) private void AddOrUpdate(string instanceId, ConsulHeartbeatOptions heartbeatOptions) { - _schedulerLogger.LogDebug("Adding/updating instance '{InstanceId}'.", instanceId); + LogAddingOrUpdatingInstance(_schedulerLogger, instanceId); TimeSpan interval = heartbeatOptions.ComputeHeartbeatInterval(); string checkId = instanceId; @@ -109,7 +110,7 @@ public async Task RemoveAsync(string instanceId) if (ServiceHeartbeats.TryRemove(instanceId, out PeriodicHeartbeat? heartbeat)) { - _schedulerLogger.LogDebug("Removing instance '{InstanceId}'.", instanceId); + LogRemovingInstance(_schedulerLogger, instanceId); await heartbeat.DisposeAsync(); } } @@ -130,4 +131,10 @@ public async ValueTask DisposeAsync() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Adding/updating instance '{InstanceId}'.")] + private static partial void LogAddingOrUpdatingInstance(ILogger logger, string instanceId); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Removing instance '{InstanceId}'.")] + private static partial void LogRemovingInstance(ILogger logger, string instanceId); } diff --git a/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs b/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs index df11be4347..257a55202d 100644 --- a/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs +++ b/src/Discovery/src/Eureka/AppInfo/ApplicationInfo.cs @@ -53,7 +53,7 @@ private List GetInstancesSnapshot() /// public override string ToString() { - return JsonSerializer.Serialize(this, DebugSerializerOptions.Instance); + return JsonSerializer.Serialize(this, DebugJsonSerializerContext.Default.ApplicationInfo); } internal void Add(InstanceInfo instance) diff --git a/src/Discovery/src/Eureka/AppInfo/ApplicationInfoCollection.cs b/src/Discovery/src/Eureka/AppInfo/ApplicationInfoCollection.cs index d77eb1f76e..e42f48370e 100644 --- a/src/Discovery/src/Eureka/AppInfo/ApplicationInfoCollection.cs +++ b/src/Discovery/src/Eureka/AppInfo/ApplicationInfoCollection.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Collections.ObjectModel; using System.Text; using System.Text.Json; using Steeltoe.Common; @@ -17,11 +18,8 @@ namespace Steeltoe.Discovery.Eureka.AppInfo; /// public sealed class ApplicationInfoCollection : IReadOnlyCollection { - private readonly object _addRemoveInstanceLock = new(); - internal ConcurrentDictionary ApplicationMap { get; } = new(); internal ConcurrentDictionary> VipInstanceMap { get; } = new(); - internal ConcurrentDictionary> SecureVipInstanceMap { get; } = new(); public string? AppsHashCode { get; internal set; } public long? Version { get; private set; } @@ -51,24 +49,31 @@ internal ApplicationInfoCollection(IList apps) return ApplicationMap.GetValueOrDefault(appName.ToUpperInvariant()); } - internal List GetInstancesBySecureVipAddress(string secureVipAddress) + internal ReadOnlyCollection GetInstancesByVipAddress(string vipAddress) { - ArgumentException.ThrowIfNullOrWhiteSpace(secureVipAddress); + ArgumentException.ThrowIfNullOrWhiteSpace(vipAddress); - return GetByVipAddress(secureVipAddress, SecureVipInstanceMap); - } + List result = []; + string addressUpper = vipAddress.ToUpperInvariant(); - internal List GetInstancesByVipAddress(string vipAddress) - { - ArgumentException.ThrowIfNullOrWhiteSpace(vipAddress); + if (VipInstanceMap.TryGetValue(addressUpper, out ConcurrentDictionary? instancesById)) + { + foreach (InstanceInfo instance in instancesById.Values) + { + if ((ReturnUpInstancesOnly && instance.EffectiveStatus == InstanceStatus.Up) || !ReturnUpInstancesOnly) + { + result.Add(instance); + } + } + } - return GetByVipAddress(vipAddress, VipInstanceMap); + return result.AsReadOnly(); } /// public override string ToString() { - return JsonSerializer.Serialize(this, DebugSerializerOptions.Instance); + return JsonSerializer.Serialize(this, DebugJsonSerializerContext.Default.ApplicationInfoCollection); } IEnumerator IEnumerable.GetEnumerator() @@ -92,79 +97,51 @@ internal void Add(ApplicationInfo app) foreach (InstanceInfo instance in app.Instances) { - AddInstanceToVip(instance); + AddToVipInstanceMap(instance); } } - private void AddInstanceToVip(InstanceInfo instance) + private void AddToVipInstanceMap(InstanceInfo instance) { - foreach (string vipAddress in ExpandVipAddresses(instance.VipAddress)) + foreach (string vipAddress in ExpandVipAddresses(instance)) { - AddInstanceToVip(instance, vipAddress, VipInstanceMap); - } + string addressUpper = vipAddress.ToUpperInvariant(); - foreach (string secureVipAddress in ExpandVipAddresses(instance.SecureVipAddress)) - { - AddInstanceToVip(instance, secureVipAddress, SecureVipInstanceMap); + ConcurrentDictionary instancesById = VipInstanceMap.GetOrAdd(addressUpper, new ConcurrentDictionary()); + instancesById.AddOrUpdate(instance.InstanceId, _ => instance, (_, _) => instance); } } - private void AddInstanceToVip(InstanceInfo instance, string address, ConcurrentDictionary> dictionary) + private static HashSet ExpandVipAddresses(InstanceInfo instance) { - lock (_addRemoveInstanceLock) - { - string addressUpper = address.ToUpperInvariant(); - - if (!dictionary.TryGetValue(addressUpper, out ConcurrentDictionary? instances)) - { - instances = new ConcurrentDictionary(); - dictionary[addressUpper] = instances; - } + HashSet addresses = []; - instances[instance.InstanceId] = instance; + if (instance.SecureVipAddress != null) + { + string[] secureAddresses = instance.SecureVipAddress.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + addresses.UnionWith(secureAddresses); } - } - private static string[] ExpandVipAddresses(string? addresses) - { - if (string.IsNullOrWhiteSpace(addresses)) + if (instance.VipAddress != null) { - return []; + string[] vipAddresses = instance.VipAddress.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + addresses.UnionWith(vipAddresses); } - return addresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + return addresses; } - internal void RemoveInstanceFromVip(InstanceInfo instance) + internal void RemoveFromVipInstanceMap(InstanceInfo instance) { ArgumentNullException.ThrowIfNull(instance); - foreach (string vipAddress in ExpandVipAddresses(instance.VipAddress)) - { - RemoveInstanceFromVip(instance, vipAddress, VipInstanceMap); - } - - foreach (string secureVipAddress in ExpandVipAddresses(instance.SecureVipAddress)) - { - RemoveInstanceFromVip(instance, secureVipAddress, SecureVipInstanceMap); - } - } - - private void RemoveInstanceFromVip(InstanceInfo instance, string address, - ConcurrentDictionary> dictionary) - { - lock (_addRemoveInstanceLock) + foreach (string vipAddress in ExpandVipAddresses(instance)) { - string addressUpper = address.ToUpperInvariant(); + string addressUpper = vipAddress.ToUpperInvariant(); - if (dictionary.TryGetValue(addressUpper, out ConcurrentDictionary? instances)) + if (VipInstanceMap.TryGetValue(addressUpper, out ConcurrentDictionary? instancesById)) { - _ = instances.TryRemove(instance.InstanceId, out _); - - if (instances.IsEmpty) - { - _ = dictionary.TryRemove(addressUpper, out _); - } + instancesById.TryRemove(instance.InstanceId, out _); } } } @@ -190,11 +167,11 @@ internal void UpdateFromDelta(ApplicationInfoCollection delta) case ActionType.Added: case ActionType.Modified: existingApp.Add(instance); - AddInstanceToVip(instance); + AddToVipInstanceMap(instance); break; case ActionType.Deleted: existingApp.Remove(instance); - RemoveInstanceFromVip(instance); + RemoveFromVipInstanceMap(instance); break; } } @@ -261,22 +238,4 @@ internal static ApplicationInfoCollection FromJson(JsonApplications? jsonApplica return apps; } - - private List GetByVipAddress(string name, ConcurrentDictionary> dictionary) - { - List result = []; - - if (dictionary.TryGetValue(name.ToUpperInvariant(), out ConcurrentDictionary? instances)) - { - foreach (InstanceInfo instance in instances.Values.ToArray()) - { - if ((ReturnUpInstancesOnly && instance.EffectiveStatus == InstanceStatus.Up) || !ReturnUpInstancesOnly) - { - result.Add(instance); - } - } - } - - return result; - } } diff --git a/src/Discovery/src/Eureka/AppInfo/DataCenterName.cs b/src/Discovery/src/Eureka/AppInfo/DataCenterName.cs index 5cf05013d3..ede53c628b 100644 --- a/src/Discovery/src/Eureka/AppInfo/DataCenterName.cs +++ b/src/Discovery/src/Eureka/AppInfo/DataCenterName.cs @@ -6,8 +6,11 @@ #pragma warning disable SA1602 // Enumeration items should be documented #endif +using System.Text.Json.Serialization; + namespace Steeltoe.Discovery.Eureka.AppInfo; +[JsonConverter(typeof(JsonStringEnumConverter))] public enum DataCenterName { Netflix, diff --git a/src/Discovery/src/Eureka/AppInfo/InstanceInfo.cs b/src/Discovery/src/Eureka/AppInfo/InstanceInfo.cs index 9aa58d5d6f..0015f8078e 100644 --- a/src/Discovery/src/Eureka/AppInfo/InstanceInfo.cs +++ b/src/Discovery/src/Eureka/AppInfo/InstanceInfo.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Text.Json; +using Steeltoe.Common.Discovery; using Steeltoe.Discovery.Eureka.Configuration; using Steeltoe.Discovery.Eureka.Transport; using Steeltoe.Discovery.Eureka.Util; @@ -267,7 +268,7 @@ public override int GetHashCode() /// public override string ToString() { - return JsonSerializer.Serialize(this, DebugSerializerOptions.Instance); + return JsonSerializer.Serialize(this, DebugJsonSerializerContext.Default.InstanceInfo); } internal static InstanceInfo FromConfiguration(EurekaInstanceOptions options, TimeProvider timeProvider) @@ -574,6 +575,11 @@ private static bool IsMetadataEqual(IReadOnlyDictionary left, I return ReferenceEquals(left, right) || left.SequenceEqual(right, KeyValuePairEqualityComparer.Instance); } + public IServiceInstance ToServiceInstance() + { + return new EurekaServiceInstance(this); + } + private sealed class KeyValuePairEqualityComparer : IEqualityComparer> { public static KeyValuePairEqualityComparer Instance { get; } = new(); diff --git a/src/Discovery/src/Eureka/AppInfo/LeaseInfo.cs b/src/Discovery/src/Eureka/AppInfo/LeaseInfo.cs index ecab87747e..2a1d76b2f1 100644 --- a/src/Discovery/src/Eureka/AppInfo/LeaseInfo.cs +++ b/src/Discovery/src/Eureka/AppInfo/LeaseInfo.cs @@ -48,7 +48,7 @@ private LeaseInfo() /// public override string ToString() { - return JsonSerializer.Serialize(this, DebugSerializerOptions.Instance); + return JsonSerializer.Serialize(this, DebugJsonSerializerContext.Default.LeaseInfo); } internal static LeaseInfo? FromJson(JsonLeaseInfo? jsonLeaseInfo) diff --git a/src/Discovery/src/Eureka/Configuration/DataCenterInfo.cs b/src/Discovery/src/Eureka/Configuration/DataCenterInfo.cs index bbf7e7096d..f23ef234db 100644 --- a/src/Discovery/src/Eureka/Configuration/DataCenterInfo.cs +++ b/src/Discovery/src/Eureka/Configuration/DataCenterInfo.cs @@ -34,7 +34,15 @@ public sealed class DataCenterInfo }; } - throw new ArgumentException($"Unsupported datacenter name '{jsonDataCenterInfo.Name}'.", nameof(jsonDataCenterInfo)); + if (jsonDataCenterInfo.Name == nameof(DataCenterName.Netflix)) + { + return new DataCenterInfo + { + Name = DataCenterName.Netflix + }; + } + + return null; } internal JsonDataCenterInfo ToJson() diff --git a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs index ef18b9fb65..24b391e3da 100644 --- a/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs +++ b/src/Discovery/src/Eureka/Configuration/EurekaInstanceOptions.cs @@ -14,13 +14,17 @@ namespace Steeltoe.Discovery.Eureka.Configuration; -public sealed class EurekaInstanceOptions +public sealed partial class EurekaInstanceOptions { internal const string ConfigurationPrefix = "eureka:instance"; internal const string DefaultStatusPageUrlPath = "/info"; internal const string DefaultHealthCheckUrlPath = "/health"; - private bool UseAspNetCoreUrls => !Platform.IsCloudFoundry || IsContainerToContainerMethod() || IsForceHostNameMethod(); + private bool IsAnyPortConfigured => + this is { IsNonSecurePortEnabled: true, NonSecurePort: not null } or { IsSecurePortEnabled: true, SecurePort: not null }; + + private bool IsSuitableRegistrationMethod => !Platform.IsCloudFoundry || IsContainerToContainerMethod() || IsForceHostNameMethod(); + internal bool ShouldSetPortsFromListenAddresses => UseAspNetCoreUrls && IsSuitableRegistrationMethod && !IsAnyPortConfigured; internal TimeSpan LeaseRenewalInterval => TimeSpan.FromSeconds(LeaseRenewalIntervalInSeconds); internal TimeSpan LeaseExpirationDuration => TimeSpan.FromSeconds(LeaseExpirationDurationInSeconds); @@ -205,6 +209,13 @@ public sealed class EurekaInstanceOptions Name = DataCenterName.MyOwn }; + /// + /// Gets or sets a value indicating whether to register with the port number(s) ASP.NET Core is listening on. Default value: true. + /// + /// This property is ignored when or is explicitly configured. + /// + public bool UseAspNetCoreUrls { get; set; } = true; + /// /// Gets or sets a value indicating whether is used to determine and /// . Default value: false. @@ -231,75 +242,88 @@ internal bool IsForceHostNameMethod() internal void SetPortsFromListenAddresses(IEnumerable listenOnAddresses, string source, ILogger logger) { - if (UseAspNetCoreUrls) + int? listenHttpPort = null; + int? listenHttpsPort = null; + + foreach (string address in listenOnAddresses) { - int? listenHttpPort = null; - int? listenHttpsPort = null; + BindingAddress bindingAddress = BindingAddress.Parse(address); - foreach (string address in listenOnAddresses) + if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) { - BindingAddress bindingAddress = BindingAddress.Parse(address); - - if (bindingAddress is { Scheme: "http", Port: > 0 } && listenHttpPort == null) - { - listenHttpPort = bindingAddress.Port; - } - else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) - { - listenHttpsPort = bindingAddress.Port; - } + listenHttpPort = bindingAddress.Port; } + else if (bindingAddress is { Scheme: "https", Port: > 0 } && listenHttpsPort == null) + { + listenHttpsPort = bindingAddress.Port; + } + } - int? nonSecurePort = IsNonSecurePortEnabled ? NonSecurePort : null; - int? securePort = IsSecurePortEnabled ? SecurePort : null; + int? nonSecurePort = IsNonSecurePortEnabled ? NonSecurePort : null; + int? securePort = IsSecurePortEnabled ? SecurePort : null; - if (nonSecurePort != listenHttpPort) + if (nonSecurePort != listenHttpPort) + { + if (listenHttpPort != null) { - if (listenHttpPort != null) + if (nonSecurePort == null) { - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (nonSecurePort == null) - { - logger.LogDebug("Activating non-secure port {NonSecurePort} from {Source}.", listenHttpPort, source); - } - else - { - logger.LogDebug("Changing non-secure port to {NonSecurePort} from {Source}.", listenHttpPort, source); - } - - NonSecurePort = listenHttpPort.Value; - IsNonSecurePortEnabled = true; + LogActivatingNonSecurePort(logger, listenHttpPort, source); } - else if (nonSecurePort != null) + else { - logger.LogDebug("Deactivating non-secure port from {Source}.", source); - IsNonSecurePortEnabled = false; + LogChangingNonSecurePort(logger, listenHttpPort, source); } + + NonSecurePort = listenHttpPort.Value; + IsNonSecurePortEnabled = true; + } + else if (nonSecurePort != null) + { + LogDeactivatingNonSecurePort(logger, source); + IsNonSecurePortEnabled = false; } + } - if (securePort != listenHttpsPort) + if (securePort != listenHttpsPort) + { + if (listenHttpsPort != null) { - if (listenHttpsPort != null) + if (securePort == null) { - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (securePort == null) - { - logger.LogDebug("Activating secure port {SecurePort} from {Source}.", listenHttpsPort, source); - } - else - { - logger.LogDebug("Changing secure port to {SecurePort} from {Source}.", listenHttpsPort, source); - } - - SecurePort = listenHttpsPort.Value; - IsSecurePortEnabled = true; + LogActivatingSecurePort(logger, listenHttpsPort, source); } - else if (securePort != null) + else { - logger.LogDebug("Deactivating secure port from {Source}.", source); - IsSecurePortEnabled = false; + LogChangingSecurePort(logger, listenHttpsPort, source); } + + SecurePort = listenHttpsPort.Value; + IsSecurePortEnabled = true; + } + else if (securePort != null) + { + LogDeactivatingSecurePort(logger, source); + IsSecurePortEnabled = false; } } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Activating non-secure port {NonSecurePort} from {Source}.")] + private static partial void LogActivatingNonSecurePort(ILogger logger, int? nonSecurePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing non-secure port to {NonSecurePort} from {Source}.")] + private static partial void LogChangingNonSecurePort(ILogger logger, int? nonSecurePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deactivating non-secure port from {Source}.")] + private static partial void LogDeactivatingNonSecurePort(ILogger logger, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Activating secure port {SecurePort} from {Source}.")] + private static partial void LogActivatingSecurePort(ILogger logger, int? securePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing secure port to {SecurePort} from {Source}.")] + private static partial void LogChangingSecurePort(ILogger logger, int? securePort, string source); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deactivating secure port from {Source}.")] + private static partial void LogDeactivatingSecurePort(ILogger logger, string source); } diff --git a/src/Discovery/src/Eureka/ConfigurationSchema.json b/src/Discovery/src/Eureka/ConfigurationSchema.json index 2e1c600204..95634e3416 100644 --- a/src/Discovery/src/Eureka/ConfigurationSchema.json +++ b/src/Discovery/src/Eureka/ConfigurationSchema.json @@ -258,6 +258,10 @@ "type": "string", "description": "Gets or sets the relative path to the status page for the instance. The status page URL is then constructed out of the 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.HostName' and the type of communication - secure or non-secure, as specified in 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.SecurePort' and 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.NonSecurePort'. It is normally used for informational purposes for other services to find out about the status of the instance. Users can provide a simple HTML page indicating what the current status of the instance is. Default value: /info." }, + "UseAspNetCoreUrls": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to register with the port number(s) ASP.NET Core is listening on. Default value: true.\n\nThis property is ignored when 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.NonSecurePort' or 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.SecurePort' is explicitly configured." + }, "UseNetworkInterfaces": { "type": "boolean", "description": "Gets or sets a value indicating whether 'System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces' is used to determine 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.IPAddress' and 'Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.HostName'. Default value: false." diff --git a/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs b/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs index fa036c5b2f..da517432b7 100644 --- a/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs +++ b/src/Discovery/src/Eureka/DynamicPortAssignmentHostedService.cs @@ -95,7 +95,7 @@ public Task StoppedAsync(CancellationToken cancellationToken) } /// - /// Enables to trigger change in . + /// Enables triggering change in . /// internal sealed class EurekaInstanceOptionsChangeTokenSource : IOptionsChangeTokenSource { @@ -136,7 +136,7 @@ public void PostConfigure(string? name, EurekaInstanceOptions options) { ArgumentNullException.ThrowIfNull(options); - if (_listenState.ListenOnAddresses != null) + if (_listenState.ListenOnAddresses != null && options.ShouldSetPortsFromListenAddresses) { options.SetPortsFromListenAddresses(_listenState.ListenOnAddresses, "address features", _optionsLogger); } diff --git a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs index 86a915b8a4..dfb1702476 100644 --- a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs +++ b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs @@ -6,25 +6,32 @@ using Microsoft.Extensions.Options; using Steeltoe.Discovery.Eureka.AppInfo; using Steeltoe.Discovery.Eureka.Configuration; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Discovery.Eureka; /// /// Provides access to the Eureka instance that represents the currently running application. /// -public sealed class EurekaApplicationInfoManager : IDisposable +public sealed partial class EurekaApplicationInfoManager : IDisposable { private readonly IOptionsMonitor _clientOptionsMonitor; private readonly IOptionsMonitor _instanceOptionsMonitor; private readonly TimeProvider _timeProvider; private readonly IDisposable? _instanceOptionsChangeToken; private readonly ILogger _logger; - private readonly object _instanceWriteLock = new(); + private readonly LockPrimitive _instanceWriteLock = new(); // Readers must never be blocked, as it may delay the periodic heartbeat. // Updates from user code must be synchronized with configuration changes. // After update, the readonly snapshot is replaced. Volatile prevents reading stale data. - // Once metadata has been set from user code, it overrules what's in configuration. + // Once metadata has been set from user code, it overrides what's in configuration. private volatile InstanceInfo _instance; private IReadOnlyDictionary? _explicitMetadata; @@ -62,7 +69,7 @@ public EurekaApplicationInfoManager(IOptionsMonitor clientO private void HandleInstanceOptionsChanged(EurekaInstanceOptions instanceOptions) { - _logger.LogDebug("Responding to changed configuration."); + LogRespondingToChangedConfiguration(); try { @@ -70,7 +77,7 @@ private void HandleInstanceOptionsChanged(EurekaInstanceOptions instanceOptions) } catch (Exception exception) { - _logger.LogError(exception, "Failed to update Eureka instance from changed configuration."); + LogFailedToUpdateInstance(exception); } } @@ -114,11 +121,11 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool } catch (Exception exception) { - _logger.LogError(exception, "Failed to adapt to configuration changes. Discarding updated configuration."); + LogFailedToAdaptConfiguration(exception); newInstance = previousInstance; } - // Status in configuration is the initial startup status. New or previous instance status always overrules it. + // Status in configuration is the initial startup status. New or previous instance status always overrides it. newInstance.ReplaceStatus(newStatus ?? previousInstance.Status); if (newOverriddenStatus != null) @@ -136,13 +143,13 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool if (newInstance.IsDirty) { - _logger.LogDebug("Instance has changed."); + LogInstanceHasChanged(); _instance = newInstance; eventArgs = new InstanceChangedEventArgs(newInstance, previousInstance); } else { - _logger.LogDebug("Instance has not changed."); + LogInstanceHasNotChanged(); } } @@ -157,14 +164,14 @@ private InstanceInfo MergeInstanceWithConfiguration(EurekaInstanceOptions instan if (instanceOptions.InstanceId != previousInstance.InstanceId) { // A change of InstanceId would require unregister, then re-register. - _logger.LogWarning("Discarding change of InstanceId, which is not supported."); + LogDiscardingInstanceIdChange(); instanceOptions.InstanceId = previousInstance.InstanceId; } if (!string.Equals(instanceOptions.AppName, previousInstance.AppName, StringComparison.OrdinalIgnoreCase)) { // A change of AppName would require unregister, then re-register. - _logger.LogWarning("Discarding change of AppName, which is not supported."); + LogDiscardingAppNameChange(); instanceOptions.AppName = previousInstance.AppName; } @@ -191,4 +198,25 @@ public void Dispose() { _instanceOptionsChangeToken?.Dispose(); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Responding to changed configuration.")] + private partial void LogRespondingToChangedConfiguration(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to update Eureka instance from changed configuration.")] + private partial void LogFailedToUpdateInstance(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to adapt to configuration changes. Discarding updated configuration.")] + private partial void LogFailedToAdaptConfiguration(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Instance has changed.")] + private partial void LogInstanceHasChanged(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Instance has not changed.")] + private partial void LogInstanceHasNotChanged(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Discarding change of InstanceId, which is not supported.")] + private partial void LogDiscardingInstanceIdChange(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Discarding change of AppName, which is not supported.")] + private partial void LogDiscardingAppNameChange(); } diff --git a/src/Discovery/src/Eureka/EurekaClient.cs b/src/Discovery/src/Eureka/EurekaClient.cs index 77d73754b5..912d5b06ae 100644 --- a/src/Discovery/src/Eureka/EurekaClient.cs +++ b/src/Discovery/src/Eureka/EurekaClient.cs @@ -8,7 +8,6 @@ using System.Net.Http.Json; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -25,7 +24,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// Sends HTTP requests to Eureka servers. /// -public sealed class EurekaClient +public sealed partial class EurekaClient { // HTTP endpoints are described at: https://github.com/Netflix/eureka/wiki/Eureka-REST-operations // Self preservation is described at: https://www.baeldung.com/eureka-self-preservation-renewal @@ -37,21 +36,6 @@ public sealed class EurekaClient private static readonly Task TaskOfNull = Task.FromResult(null); private static readonly TimeSpan GetAccessTokenTimeout = TimeSpan.FromSeconds(10); - private static readonly JsonSerializerOptions RequestSerializerOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - private static readonly JsonSerializerOptions ResponseSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = - { - new JsonApplicationConverter(), - new JsonInstanceInfoConverter() - } - }; - private readonly IHttpClientFactory _httpClientFactory; private readonly IOptionsMonitor _optionsMonitor; private readonly EurekaServiceUriStateManager _eurekaServiceUriStateManager; @@ -92,14 +76,13 @@ public async Task RegisterAsync(InstanceInfo instance, CancellationToken cancell if ((Platform.IsContainerized || Platform.IsCloudHosted) && string.Equals(instance.HostName, "localhost", StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("Registering with hostname 'localhost' in containerized or cloud environments may not be valid. " + - "Please configure Eureka:Instance:HostName with a non-localhost address."); + LogHostNamePotentiallyInvalid(); } string requestBody = JsonSerializer.Serialize(new JsonInstanceInfoRoot { Instance = instance.ToJson() - }, RequestSerializerOptions); + }, EurekaJsonSerializerContext.Default.JsonInstanceInfoRoot); string path = $"apps/{WebUtility.UrlEncode(instance.AppName)}"; await ExecuteRequestAsync(HttpMethod.Post, path, null, requestBody, cancellationToken); @@ -225,7 +208,7 @@ private async Task GetApplicationsAtPathAsync(string { return await ExecuteRequestAsync(HttpMethod.Get, path, null, null, async response => { - var root = await response.Content.ReadFromJsonAsync(ResponseSerializerOptions, cancellationToken); + JsonApplicationsRoot? root = await response.Content.ReadFromJsonAsync(EurekaJsonSerializerContext.Default.JsonApplicationsRoot, cancellationToken); return ApplicationInfoCollection.FromJson(root?.Applications, _timeProvider); }, cancellationToken); } @@ -253,24 +236,39 @@ private async Task ExecuteRequestAsync(HttpMethod method, stri Uri requestUri = GetRequestUri(serviceUri, path, queryString); HttpContent? requestContent = requestBody != null ? new StringContent(requestBody, Encoding.UTF8, MediaType) : null; - HttpRequestMessage request = await GetRequestMessageAsync(method, requestUri, requestContent, cancellationToken); + HttpRequestMessage request; + + try + { + request = await GetRequestMessageAsync(clientOptions, method, requestUri, requestContent, cancellationToken); + } + catch (Exception exception) when (!exception.IsCancellation()) + { + if (!string.IsNullOrEmpty(clientOptions.AccessTokenUri)) + { + var accessTokenUri = new Uri(clientOptions.AccessTokenUri); + LogFailedToFetchAccessToken(exception, accessTokenUri, attempt); + + continue; + } + + throw; + } if (!string.IsNullOrEmpty(requestBody)) { - _logger.LogDebug("Sending {RequestMethod} request to '{RequestUri}' with body: {RequestBody}.", request.Method, requestUri.ToMaskedString(), - requestBody); + LogSendingRequestWithBody(request.Method, requestUri, requestBody); } else { - _logger.LogDebug("Sending {RequestMethod} request to '{RequestUri}' without request body.", request.Method, requestUri.ToMaskedString()); + LogSendingRequestWithoutBody(request.Method, requestUri); } try { using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); - _logger.LogDebug("HTTP {RequestMethod} request to '{RequestUri}' returned status {StatusCode} in attempt {Attempt}.", request.Method, - requestUri.ToMaskedString(), (int)response.StatusCode, attempt); + LogRequestReturnedStatus(request.Method, requestUri, (int)response.StatusCode, attempt); if (response.IsSuccessStatusCode) { @@ -282,22 +280,19 @@ private async Task ExecuteRequestAsync(HttpMethod method, stri } catch (JsonException exception) when (!exception.IsCancellation()) { - _logger.LogDebug(exception, "Failed to deserialize HTTP response from {RequestMethod} '{RequestUri}'.", request.Method, - requestUri.ToMaskedString()); + LogFailedToDeserializeResponse(exception, request.Method, requestUri); } } else { string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogInformation("HTTP {RequestMethod} request to '{RequestUri}' failed with status {StatusCode}: {ResponseBody}", request.Method, - requestUri.ToMaskedString(), (int)response.StatusCode, responseBody); + LogRequestFailed(request.Method, requestUri, (int)response.StatusCode, responseBody); } } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Failed to execute HTTP {RequestMethod} request to '{RequestUri}' in attempt {Attempt}.", request.Method, - requestUri.ToMaskedString(), attempt); + LogAttemptFailed(exception, request.Method, requestUri, attempt); } _eurekaServiceUriStateManager.MarkFailingServiceUri(serviceUri); @@ -328,31 +323,30 @@ private static Uri GetRequestUri(Uri baseUri, string path, IDictionary GetRequestMessageAsync(HttpMethod method, Uri requestUri, HttpContent? content, CancellationToken cancellationToken) + private async Task GetRequestMessageAsync(EurekaClientOptions optionsSnapshot, HttpMethod method, Uri requestUri, HttpContent? content, + CancellationToken cancellationToken) { var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped)); var requestMessage = new HttpRequestMessage(method, 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 { - EurekaClientOptions clientOptions = _optionsMonitor.CurrentValue; - - if (!string.IsNullOrEmpty(clientOptions.AccessTokenUri)) + if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri)) { using HttpClient httpClient = CreateHttpClient("AccessTokenForEureka", GetAccessTokenTimeout); - var accessTokenUri = new Uri(clientOptions.AccessTokenUri); + 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); } } @@ -364,4 +358,36 @@ private async Task GetRequestMessageAsync(HttpMethod method, return requestMessage; } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Registering with hostname 'localhost' in containerized or cloud environments may not be valid. " + + "Please configure Eureka:Instance:HostName with a non-localhost address.")] + private partial void LogHostNamePotentiallyInvalid(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending {RequestMethod} request to '{RequestUri}' with body: '{RequestBody}'.")] + private partial void LogSendingRequestWithBody(HttpMethod requestMethod, MaskedUri requestUri, string? requestBody); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending {RequestMethod} request to '{RequestUri}' without request body.")] + private partial void LogSendingRequestWithoutBody(HttpMethod requestMethod, MaskedUri requestUri); + + [LoggerMessage(Level = LogLevel.Debug, Message = "HTTP {RequestMethod} request to '{RequestUri}' returned status {StatusCode} in attempt {Attempt}.")] + private partial void LogRequestReturnedStatus(HttpMethod requestMethod, MaskedUri requestUri, int statusCode, int attempt); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to deserialize HTTP response from {RequestMethod} '{RequestUri}'.")] + private partial void LogFailedToDeserializeResponse(Exception exception, HttpMethod requestMethod, MaskedUri requestUri); + + [LoggerMessage(Level = LogLevel.Information, Message = "HTTP {RequestMethod} request to '{RequestUri}' failed with status {StatusCode}: '{ResponseBody}'.")] + private partial void LogRequestFailed(HttpMethod requestMethod, MaskedUri requestUri, int statusCode, string responseBody); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to execute HTTP {RequestMethod} request to '{RequestUri}' in attempt {Attempt}.")] + private partial void LogAttemptFailed(Exception exception, HttpMethod requestMethod, MaskedUri requestUri, int attempt); + + [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}' in attempt {Attempt}.")] + private partial void LogFailedToFetchAccessToken(Exception exception, MaskedUri accessTokenUri, int attempt); } diff --git a/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs b/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs index 4d9a9d2357..db5a32a0f5 100644 --- a/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs +++ b/src/Discovery/src/Eureka/EurekaDiscoveryClient.cs @@ -2,6 +2,8 @@ // 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.Collections.ObjectModel; +using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common.Discovery; @@ -22,7 +24,7 @@ namespace Steeltoe.Discovery.Eureka; /// /// . /// -public sealed class EurekaDiscoveryClient : IDiscoveryClient +public sealed partial class EurekaDiscoveryClient : IDiscoveryClient { private readonly EurekaApplicationInfoManager _appInfoManager; private readonly EurekaClient _eurekaClient; @@ -35,6 +37,7 @@ public sealed class EurekaDiscoveryClient : IDiscoveryClient private readonly Timer? _cacheRefreshTimer; private readonly SemaphoreSlim _registerUnregisterAsyncLock = new(1); private readonly SemaphoreSlim _registryFetchAsyncLock = new(1); + private volatile bool _hasRegistered; private volatile bool _hasFirstHeartbeatCompleted; private volatile ApplicationInfoCollection _remoteApps; @@ -68,6 +71,9 @@ internal ApplicationInfoCollection Applications /// public event EventHandler? ApplicationsFetched; + /// + public event EventHandler? InstancesFetched; + public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, EurekaClient eurekaClient, IOptionsMonitor clientOptionsMonitor, HealthCheckHandlerProvider healthCheckHandlerProvider, TimeProvider timeProvider, ILogger logger) @@ -114,10 +120,10 @@ public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, Eureka } catch (Exception exception) { - _logger.LogInformation(exception, "Initial registration failed."); + LogInitialRegistrationFailed(exception); } - _logger.LogInformation("Starting heartbeat timer."); + LogStartingHeartbeatTimer(); _heartbeatTimer = StartTimer(leaseRenewalInterval.Value, HeartbeatAsyncTask); _appInfoManager.InstanceChanged += AppInfoManagerOnInstanceChanged; @@ -135,10 +141,10 @@ public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, Eureka } catch (Exception exception) { - _logger.LogInformation(exception, "Initial fetch registry failed."); + LogInitialFetchRegistryFailed(exception); } - _logger.LogInformation("Starting applications cache refresh timer."); + LogStartingCacheRefreshTimer(); _cacheRefreshTimer = StartTimer(clientOptions.RegistryFetchInterval, CacheRefreshAsyncTask); _clientOptionsChangeToken = _clientOptionsMonitor.OnChange(options => @@ -156,12 +162,11 @@ public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, Eureka return Applications.GetRegisteredApplication(appName); } - internal IReadOnlyList GetInstancesByVipAddress(string vipAddress, bool secure) + private ReadOnlyCollection GetInstancesByVipAddress(string vipAddress) { ArgumentException.ThrowIfNullOrWhiteSpace(vipAddress); - List instances = secure ? Applications.GetInstancesBySecureVipAddress(vipAddress) : Applications.GetInstancesByVipAddress(vipAddress); - return instances.AsReadOnly(); + return Applications.GetInstancesByVipAddress(vipAddress); } /// @@ -191,14 +196,14 @@ public async Task ShutdownAsync(CancellationToken cancellationToken) try { - if (!ReferenceEquals(_appInfoManager.Instance, InstanceInfo.Disabled)) + if (_hasRegistered && !ReferenceEquals(_appInfoManager.Instance, InstanceInfo.Disabled)) { await DeregisterAsync(cancellationToken); } } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Deregister failed during shutdown."); + LogDeregisterFailedDuringShutdown(exception); } _appInfoManager.Dispose(); @@ -216,7 +221,7 @@ private async void AppInfoManagerOnInstanceChanged(object? sender, InstanceChang try { - _logger.LogDebug("Instance changed event handler: New={NewInstance}, Previous={PreviousInstance}", args.NewInstance, args.PreviousInstance); + LogInstanceChangedEvent(args.NewInstance, args.PreviousInstance); if (args.NewInstance.LeaseInfo?.RenewalInterval != args.PreviousInstance.LeaseInfo?.RenewalInterval) { @@ -230,7 +235,7 @@ private async void AppInfoManagerOnInstanceChanged(object? sender, InstanceChang { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to handle {EventName} event.", nameof(EurekaApplicationInfoManager.InstanceChanged)); + LogFailedToHandleEvent(exception, nameof(EurekaApplicationInfoManager.InstanceChanged)); } } } @@ -259,11 +264,12 @@ internal async Task RegisterAsync(bool requireDirtyInstance, CancellationToken c if (!requireDirtyInstance || snapshot.IsDirty) { - _logger.LogDebug("Registering {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogRegistering(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.RegisterAsync(snapshot, cancellationToken); - _logger.LogDebug("Register {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogRegistrationSucceeded(snapshot.AppName, snapshot.InstanceId); snapshot.IsDirty = false; + _hasRegistered = true; } } finally @@ -280,9 +286,9 @@ internal async Task DeregisterAsync(CancellationToken cancellationToken) { InstanceInfo snapshot = _appInfoManager.Instance; - _logger.LogDebug("Deregistering {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogDeregistering(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.DeregisterAsync(snapshot.AppName, snapshot.InstanceId, cancellationToken); - _logger.LogDebug("Deregister {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogDeregistrationSucceeded(snapshot.AppName, snapshot.InstanceId); } finally { @@ -299,16 +305,15 @@ internal async Task RenewAsync(CancellationToken cancellationToken) { InstanceInfo snapshot = _appInfoManager.Instance; - _logger.LogDebug("Sending heartbeat for {Application}/{Instance}.", snapshot.AppName, snapshot.InstanceId); + LogSendingHeartbeat(snapshot.AppName, snapshot.InstanceId); await _eurekaClient.HeartbeatAsync(snapshot.AppName, snapshot.InstanceId, snapshot.LastDirtyTimeUtc, cancellationToken); - _logger.LogDebug("Heartbeat for {Application}/{Instance} succeeded.", snapshot.AppName, snapshot.InstanceId); + LogHeartbeatSucceeded(snapshot.AppName, snapshot.InstanceId); _lastGoodHeartbeatTimeUtc = new NullableValueWrapper(_timeProvider.GetUtcNow().UtcDateTime); } catch (EurekaTransportException exception) { - _logger.LogWarning(exception, - "Eureka heartbeat failed. This could happen if Eureka was offline during app startup. Attempting to (re)register now."); + LogHeartbeatFailed(exception); await RegisterAsync(false, cancellationToken); } @@ -327,7 +332,8 @@ internal async Task FetchRegistryAsync(bool doFullUpdate, CancellationToken canc await _registryFetchAsyncLock.WaitAsync(cancellationToken); - ApplicationsFetchedEventArgs eventArgs; + ApplicationsFetchedEventArgs? applicationsEventArgs = null; + DiscoveryInstancesFetchedEventArgs? instancesEventArgs = null; try { @@ -345,46 +351,87 @@ internal async Task FetchRegistryAsync(bool doFullUpdate, CancellationToken canc UpdateLastRemoteInstanceStatusFromCache(); - eventArgs = new ApplicationsFetchedEventArgs(_remoteApps); + if (ApplicationsFetched != null) + { + applicationsEventArgs = new ApplicationsFetchedEventArgs(_remoteApps); + } + + if (InstancesFetched != null) + { + ReadOnlyDictionary> instancesByServiceId = ToServiceInstanceMap(_remoteApps); + instancesEventArgs = new DiscoveryInstancesFetchedEventArgs(instancesByServiceId); + } } finally { _registryFetchAsyncLock.Release(); } - OnApplicationsFetched(eventArgs); + if (applicationsEventArgs != null || instancesEventArgs != null) + { + RaiseFetchEvents(applicationsEventArgs, instancesEventArgs); + } } - private void OnApplicationsFetched(ApplicationsFetchedEventArgs? args) + private static ReadOnlyDictionary> ToServiceInstanceMap(ApplicationInfoCollection apps) { - if (args != null) + var dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (string vipAddress in apps.VipInstanceMap.Keys.ToArray()) { - // Execute on separate thread, so we won't block the periodic refresh in case the handler logic is expensive. - ThreadPool.QueueUserWorkItem(_ => + ReadOnlyCollection instancesByVipAddress = apps.GetInstancesByVipAddress(vipAddress); + + if (instancesByVipAddress.Count > 0) { - try + dictionary[vipAddress] = instancesByVipAddress.Select(instance => instance.ToServiceInstance()).ToList().AsReadOnly(); + } + } + + return new ReadOnlyDictionary>(dictionary); + } + + private void RaiseFetchEvents(ApplicationsFetchedEventArgs? applicationsEventArgs, DiscoveryInstancesFetchedEventArgs? instancesEventArgs) + { + // Execute on separate thread, so we won't block the periodic refresh in case the handler logic is expensive. + ThreadPool.QueueUserWorkItem(_ => + { + try + { + if (applicationsEventArgs != null) { - ApplicationsFetched?.Invoke(this, args); + ApplicationsFetched?.Invoke(this, applicationsEventArgs); } - catch (Exception exception) + } + catch (Exception exception) + { + LogFailedToHandleEvent(exception, nameof(ApplicationsFetched)); + } + + try + { + if (instancesEventArgs != null) { - _logger.LogError(exception, "Failed to handle {EventName} event.", nameof(ApplicationsFetched)); + InstancesFetched?.Invoke(this, instancesEventArgs); } - }); - } + } + catch (Exception exception) + { + LogFailedToHandleEvent(exception, nameof(InstancesFetched)); + } + }); } internal async Task FetchFullRegistryAsync(CancellationToken cancellationToken) { EurekaClientOptions clientOptions = _clientOptionsMonitor.CurrentValue; - _logger.LogDebug("Sending request to fetch applications."); + LogFetchingApplications(); ApplicationInfoCollection applications = string.IsNullOrWhiteSpace(clientOptions.RegistryRefreshSingleVipAddress) ? await _eurekaClient.GetApplicationsAsync(cancellationToken) : await _eurekaClient.GetByVipAsync(clientOptions.RegistryRefreshSingleVipAddress, cancellationToken); - _logger.LogDebug("Full registry fetch succeeded with {Count} applications.", applications.Count); + LogFullRegistryFetchSucceeded(applications.Count); return applications; } @@ -394,29 +441,28 @@ internal async Task FetchRegistryDeltaAsync(Cancellat try { - _logger.LogDebug("Sending request to fetch applications delta."); + LogFetchingApplicationsDelta(); delta = await _eurekaClient.GetDeltaAsync(cancellationToken); } catch (EurekaTransportException exception) { - _logger.LogDebug(exception, "Failed to fetch registry delta. Trying full fetch."); + LogFailedToFetchDelta(exception); return await FetchFullRegistryAsync(cancellationToken); } - _logger.LogDebug("Registry delta fetched, updating local cache."); + LogRegistryDeltaFetched(); _remoteApps.UpdateFromDelta(delta); string hashCode = _remoteApps.ComputeHashCode(); if (hashCode != delta.AppsHashCode) { - _logger.LogWarning("Discarding fetched registry delta due to hash codes mismatch (Local={HashLocal}, Remote={HashRemote}). Trying full fetch.", - hashCode, delta.AppsHashCode); + LogDeltaHashCodeMismatch(hashCode, delta.AppsHashCode); return await FetchFullRegistryAsync(cancellationToken); } - _logger.LogDebug("Registry delta fetch succeeded with {Count} changes.", delta.Count); + LogDeltaFetchSucceeded(delta.Count); _remoteApps.AppsHashCode = delta.AppsHashCode; return _remoteApps; } @@ -427,26 +473,26 @@ internal async Task RunHealthChecksAsync(CancellationToken cancellationToken) { if (_appInfoManager.Instance.Status == InstanceStatus.Starting) { - _logger.LogDebug("Skipping health check handler in starting state."); + LogSkippingHealthCheck(); return; } try { InstanceStatus aggregatedStatus = await HealthCheckHandler.GetStatusAsync(_hasFirstHeartbeatCompleted, cancellationToken); - _logger.LogDebug("Health check handler returned status {Status}.", aggregatedStatus); + LogHealthCheckStatus(aggregatedStatus); InstanceInfo snapshot = _appInfoManager.Instance; if (aggregatedStatus != snapshot.Status) { - _logger.LogDebug("Changing instance status from {LocalStatus} to {RemoteStatus}.", snapshot.Status, aggregatedStatus); + LogChangingInstanceStatus(snapshot.Status, aggregatedStatus); _appInfoManager.UpdateStatusWithoutRaisingEvent(aggregatedStatus); } } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Failed to determine health status."); + LogFailedToDetermineHealthStatus(exception); } } } @@ -461,8 +507,7 @@ private void UpdateLastRemoteInstanceStatusFromCache() { if (remoteInstance.EffectiveStatus != snapshot.EffectiveStatus) { - _logger.LogWarning("Remote instance status {RemoteStatus} differs from local status {LocalStatus}.", remoteInstance.EffectiveStatus, - snapshot.EffectiveStatus); + LogRemoteStatusDiffers(remoteInstance.EffectiveStatus, snapshot.EffectiveStatus); } // We have ownership of the local instance, so don't take the remote status. @@ -486,7 +531,7 @@ private async void HeartbeatAsyncTask() { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Periodic renew failed."); + LogPeriodicRenewFailed(exception); } } } @@ -507,7 +552,7 @@ private async void CacheRefreshAsyncTask() { if (!exception.IsCancellation()) { - _logger.LogError(exception, "Periodic fetch of applications failed."); + LogPeriodicFetchFailed(exception); } } } @@ -533,21 +578,133 @@ public Task> GetInstancesAsync(string serviceId, Cancell { ArgumentException.ThrowIfNullOrWhiteSpace(serviceId); - IReadOnlyList nonSecureInstances = GetInstancesByVipAddress(serviceId, false); - IReadOnlyList secureInstances = GetInstancesByVipAddress(serviceId, true); - - IEnumerable instances = secureInstances.Concat(nonSecureInstances).DistinctBy(instance => instance.InstanceId); + ReadOnlyCollection instances = GetInstancesByVipAddress(serviceId); IServiceInstance[] serviceInstances = instances.Select(instance => new EurekaServiceInstance(instance)).Cast().ToArray(); - _logger.LogDebug("Returning {Count} service instances: {ServiceInstances}", serviceInstances.Length, - string.Join(", ", serviceInstances.Select(instance => $"{instance.ServiceId}={instance.Uri}"))); + if (_logger.IsEnabled(LogLevel.Debug)) + { + string instanceNames = string.Join(", ", serviceInstances.Select(FormatServiceInstance)); + LogReturningServiceInstances(serviceInstances.Length, serviceId, instanceNames); + } return Task.FromResult>(serviceInstances); } + private static string FormatServiceInstance(IServiceInstance instance) + { + var builder = new StringBuilder(); + + if (instance.SecureUri != null) + { + builder.Append(instance.SecureUri); + } + + if (instance.NonSecureUri != null) + { + if (builder.Length > 0) + { + builder.Append(';'); + } + + builder.Append(instance.NonSecureUri); + } + + return $"{instance.InstanceId}={builder}"; + } + /// public IServiceInstance GetLocalServiceInstance() { return new EurekaServiceInstance(_appInfoManager.Instance); } + + [LoggerMessage(Level = LogLevel.Information, Message = "Initial registration failed.")] + private partial void LogInitialRegistrationFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting heartbeat timer.")] + private partial void LogStartingHeartbeatTimer(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Initial fetch registry failed.")] + private partial void LogInitialFetchRegistryFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Information, Message = "Starting applications cache refresh timer.")] + private partial void LogStartingCacheRefreshTimer(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Deregister failed during shutdown.")] + private partial void LogDeregisterFailedDuringShutdown(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, + Message = "Instance changed event handler invoked with new instance {NewInstance} and previous instance {PreviousInstance}.")] + private partial void LogInstanceChangedEvent(InstanceInfo newInstance, InstanceInfo previousInstance); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to handle {EventName} event.")] + private partial void LogFailedToHandleEvent(Exception exception, string eventName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registering {Application}/{Instance}.")] + private partial void LogRegistering(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Register {Application}/{Instance} succeeded.")] + private partial void LogRegistrationSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deregistering {Application}/{Instance}.")] + private partial void LogDeregistering(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Deregister {Application}/{Instance} succeeded.")] + private partial void LogDeregistrationSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending heartbeat for {Application}/{Instance}.")] + private partial void LogSendingHeartbeat(string application, string instance); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Heartbeat for {Application}/{Instance} succeeded.")] + private partial void LogHeartbeatSucceeded(string application, string instance); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Eureka heartbeat failed. This could happen if Eureka was offline during app startup. Attempting to (re)register now.")] + private partial void LogHeartbeatFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending request to fetch applications.")] + private partial void LogFetchingApplications(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Full registry fetch succeeded with {Count} applications.")] + private partial void LogFullRegistryFetchSucceeded(int count); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Sending request to fetch applications delta.")] + private partial void LogFetchingApplicationsDelta(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch registry delta. Trying full fetch.")] + private partial void LogFailedToFetchDelta(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registry delta fetched, updating local cache.")] + private partial void LogRegistryDeltaFetched(); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Discarding fetched registry delta due to hash code mismatch between local {HashLocal} and remote {HashRemote}. Trying full fetch.")] + private partial void LogDeltaHashCodeMismatch(string hashLocal, string? hashRemote); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registry delta fetch succeeded with {Count} changes.")] + private partial void LogDeltaFetchSucceeded(int count); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Skipping health check handler in starting state.")] + private partial void LogSkippingHealthCheck(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Health check handler returned status {Status}.")] + private partial void LogHealthCheckStatus(InstanceStatus status); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Changing instance status from {LocalStatus} to {RemoteStatus}.")] + private partial void LogChangingInstanceStatus(InstanceStatus? localStatus, InstanceStatus remoteStatus); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to determine health status.")] + private partial void LogFailedToDetermineHealthStatus(Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Remote instance status {RemoteStatus} differs from local status {LocalStatus}.")] + private partial void LogRemoteStatusDiffers(InstanceStatus remoteStatus, InstanceStatus localStatus); + + [LoggerMessage(Level = LogLevel.Error, Message = "Periodic renew failed.")] + private partial void LogPeriodicRenewFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Periodic fetch of applications failed.")] + private partial void LogPeriodicFetchFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning {Count} service instances for '{ServiceId}': {ServiceInstances}.")] + private partial void LogReturningServiceInstances(int count, string serviceId, string serviceInstances); } diff --git a/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs b/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs index a40b5757cf..38de08bce5 100644 --- a/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs +++ b/src/Discovery/src/Eureka/EurekaServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ namespace Steeltoe.Discovery.Eureka; public static class EurekaServiceCollectionExtensions { private const string SpringDiscoveryEnabled = "spring:cloud:discovery:enabled"; + private const string ResolvingHttpDelegatingHandlerName = "Microsoft.Extensions.ServiceDiscovery.Http.ResolvingHttpDelegatingHandler"; /// /// Configures to use for service discovery. @@ -34,7 +35,7 @@ public static IServiceCollection AddEurekaDiscoveryClient(this IServiceCollectio { ArgumentNullException.ThrowIfNull(services); - if (services.All(descriptor => descriptor.SafeGetImplementationType() != typeof(EurekaDiscoveryClient))) + if (!IsRegistered(services)) { ConfigureEurekaServices(services); AddEurekaServices(services); @@ -43,6 +44,11 @@ public static IServiceCollection AddEurekaDiscoveryClient(this IServiceCollectio return services; } + private static bool IsRegistered(IServiceCollection services) + { + return services.Any(descriptor => descriptor.SafeGetImplementationType() == typeof(EurekaDiscoveryClient)); + } + private static void ConfigureEurekaServices(IServiceCollection services) { services.AddApplicationInstanceInfo(); @@ -75,19 +81,19 @@ private static void ConfigureEurekaClientOptions(IServiceCollection services) private static void ConfigureEurekaInstanceOptions(IServiceCollection services) { + DynamicPortAssignmentHostedService.Wire(services); + services.AddOptions().BindConfiguration(EurekaInstanceOptions.ConfigurationPrefix); services.AddOptions().BindConfiguration(InetOptions.ConfigurationPrefix); services.AddSingleton, PostConfigureEurekaInstanceOptions>(); - - DynamicPortAssignmentHostedService.Wire(services); } private static void AddEurekaServices(IServiceCollection services) { services.TryAddSingleton(TimeProvider.System); - services.AddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.AddSingleton(); + services.TryAddSingleton(); + services.AddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); @@ -102,13 +108,13 @@ private static void AddEurekaServices(IServiceCollection services) private static void AddEurekaClient(IServiceCollection services) { services.TryAddSingleton(); - services.TryAddSingleton>(); + services.AddSingleton>(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.AddSingleton(); services.ConfigureCertificateOptions("Eureka"); IHttpClientBuilder eurekaHttpClientBuilder = services.AddHttpClient("Eureka"); - eurekaHttpClientBuilder.ConfigureAdditionalHttpMessageHandlers((defaultHandlers, _) => RemoveDiscoveryHttpDelegatingHandler(defaultHandlers)); + eurekaHttpClientBuilder.ConfigureAdditionalHttpMessageHandlers((defaultHandlers, _) => RemoveDiscoveryHttpHandlers(defaultHandlers)); eurekaHttpClientBuilder.ConfigurePrimaryHttpMessageHandler(serviceProvider => { @@ -122,7 +128,7 @@ private static void AddEurekaClient(IServiceCollection services) }); IHttpClientBuilder eurekaTokenHttpClientBuilder = services.AddHttpClient("AccessTokenForEureka"); - eurekaTokenHttpClientBuilder.ConfigureAdditionalHttpMessageHandlers((defaultHandlers, _) => RemoveDiscoveryHttpDelegatingHandler(defaultHandlers)); + eurekaTokenHttpClientBuilder.ConfigureAdditionalHttpMessageHandlers((defaultHandlers, _) => RemoveDiscoveryHttpHandlers(defaultHandlers)); eurekaTokenHttpClientBuilder.ConfigurePrimaryHttpMessageHandler(serviceProvider => { @@ -139,12 +145,19 @@ private static void AddEurekaClient(IServiceCollection services) services.AddSingleton(); } - private static void RemoveDiscoveryHttpDelegatingHandler(ICollection defaultHandlers) + private static void RemoveDiscoveryHttpHandlers(ICollection defaultHandlers) { + // Prevent infinite recursion: The inner HttClient used by EurekaDiscoveryClient must not use service discovery. + DelegatingHandler[] discoveryHandlers = defaultHandlers.Where(handler => { Type handlerType = handler.GetType(); + if (handlerType.FullName == ResolvingHttpDelegatingHandlerName) + { + return true; + } + if (handlerType.IsConstructedGenericType) { Type handlerOpenType = handlerType.GetGenericTypeDefinition(); @@ -160,7 +173,6 @@ private static void RemoveDiscoveryHttpDelegatingHandler(ICollection internal sealed class EurekaServiceInstance : IServiceInstance { + /// public string ServiceId { get; } + + /// + public string InstanceId { get; } + + /// public string Host { get; } + + /// public int Port { get; } + + /// public bool IsSecure { get; } + + /// public Uri Uri { get; } + + /// + public Uri? NonSecureUri { get; } + + /// + public Uri? SecureUri { get; } + + /// public IReadOnlyDictionary Metadata { get; } public EurekaServiceInstance(InstanceInfo instance) @@ -24,25 +44,25 @@ public EurekaServiceInstance(InstanceInfo instance) ArgumentNullException.ThrowIfNull(instance); ServiceId = instance.AppName; + InstanceId = instance.InstanceId; Host = instance.HostName; - Port = GetPort(instance); - IsSecure = instance.IsSecurePortEnabled; - Uri = new Uri($"{(IsSecure ? "https" : "http")}://{Host}:{Port}"); Metadata = instance.Metadata; - } - private static int GetPort(InstanceInfo instance) - { - if (instance.IsSecurePortEnabled) + if (instance is { IsNonSecurePortEnabled: true, NonSecurePort: > 0 }) { - return instance.SecurePort; +#pragma warning disable S5332 // Using clear-text protocols is security-sensitive + NonSecureUri = new Uri($"http://{Host}:{instance.NonSecurePort}"); +#pragma warning restore S5332 // Using clear-text protocols is security-sensitive + Port = instance.NonSecurePort; } - if (instance.IsNonSecurePortEnabled) + if (instance is { IsSecurePortEnabled: true, SecurePort: > 0 }) { - return instance.NonSecurePort; + SecureUri = new Uri($"https://{Host}:{instance.SecurePort}"); + Port = instance.SecurePort; } - return 0; + IsSecure = instance.IsSecurePortEnabled; + Uri = new Uri($"{(IsSecure ? "https" : "http")}://{Host}:{Port}"); } } diff --git a/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs b/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs index fc1c4b7d7a..d1dfaeae0a 100644 --- a/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs +++ b/src/Discovery/src/Eureka/EurekaServiceUriStateManager.cs @@ -6,18 +6,25 @@ using Microsoft.Extensions.Options; using Steeltoe.Discovery.Eureka.Configuration; using Steeltoe.Discovery.Eureka.Transport; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Discovery.Eureka; /// /// Keeps track of working and broken Eureka service URIs that are configured, with stickiness to the last working server. /// -public sealed class EurekaServiceUriStateManager +public sealed partial class EurekaServiceUriStateManager { private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; - private readonly object _lockObject = new(); + private readonly LockPrimitive _lockObject = new(); private readonly HashSet _failedServiceUris = []; private Uri? _lastWorkingServiceUri; @@ -55,7 +62,7 @@ private Uri[] GetAvailableServiceUris() if (_failedServiceUris.Count > 0 && _failedServiceUris.Count >= threshold) { - _logger.LogDebug("Clearing quarantined list of size {Count}.", _failedServiceUris.Count); + LogClearingQuarantinedList(_failedServiceUris.Count); _failedServiceUris.Clear(); } @@ -127,6 +134,9 @@ internal void MarkFailingServiceUri(Uri serviceUri) } } + [LoggerMessage(Level = LogLevel.Debug, Message = "Clearing quarantined list of size {Count}.")] + private partial void LogClearingQuarantinedList(int count); + /// /// Provides a method to sequentially try all available Eureka servers. /// diff --git a/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs b/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs index 3ef1f30b96..b33208d520 100644 --- a/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs +++ b/src/Discovery/src/Eureka/PostConfigureEurekaInstanceOptions.cs @@ -140,7 +140,7 @@ private void SetPorts(EurekaInstanceOptions options) options.IsSecurePortEnabled = true; } - if (options.NonSecurePort == null && options.SecurePort == null) + if (options.ShouldSetPortsFromListenAddresses) { var optionsLogger = _serviceProvider.GetRequiredService>(); ICollection addresses = _configuration.GetListenAddresses(); diff --git a/src/Discovery/src/Eureka/PublicAPI.Shipped.txt b/src/Discovery/src/Eureka/PublicAPI.Shipped.txt index 3741b7ac79..0ba6ee432a 100644 --- a/src/Discovery/src/Eureka/PublicAPI.Shipped.txt +++ b/src/Discovery/src/Eureka/PublicAPI.Shipped.txt @@ -75,6 +75,7 @@ Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.Status.get -> Steeltoe.Discovery. Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.Status.init -> void Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.StatusPageUrl.get -> string? Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.StatusPageUrl.init -> void +Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.ToServiceInstance() -> Steeltoe.Common.Discovery.IServiceInstance! Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.VipAddress.get -> string? Steeltoe.Discovery.Eureka.AppInfo.InstanceInfo.VipAddress.init -> void Steeltoe.Discovery.Eureka.AppInfo.InstanceStatus @@ -183,6 +184,8 @@ Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.StatusPageUrl.get Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.StatusPageUrl.set -> void Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.StatusPageUrlPath.get -> string? Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.StatusPageUrlPath.set -> void +Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.get -> bool +Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.set -> void Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseNetworkInterfaces.get -> bool Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseNetworkInterfaces.set -> void Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.VipAddress.get -> string? @@ -227,6 +230,7 @@ Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.EurekaDiscoveryClient(Steeltoe.D Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.GetInstancesAsync(string! serviceId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.GetLocalServiceInstance() -> Steeltoe.Common.Discovery.IServiceInstance! Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.GetServiceIdsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.InstancesFetched -> System.EventHandler? Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.ShutdownAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Steeltoe.Discovery.Eureka.EurekaServiceCollectionExtensions Steeltoe.Discovery.Eureka.EurekaServiceUriStateManager diff --git a/src/Discovery/src/Eureka/Steeltoe.Discovery.Eureka.csproj b/src/Discovery/src/Eureka/Steeltoe.Discovery.Eureka.csproj index 2afc81bb51..726a65c3a4 100644 --- a/src/Discovery/src/Eureka/Steeltoe.Discovery.Eureka.csproj +++ b/src/Discovery/src/Eureka/Steeltoe.Discovery.Eureka.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Client for service discovery and registration with Netflix Eureka. service-discovery;service-registry;Netflix-Eureka;eureka;tanzu;spring-cloud;Spring;Cloud;Netflix true diff --git a/src/Discovery/src/Eureka/Transport/DebugJsonSerializerContext.cs b/src/Discovery/src/Eureka/Transport/DebugJsonSerializerContext.cs new file mode 100644 index 0000000000..07fabaacc2 --- /dev/null +++ b/src/Discovery/src/Eureka/Transport/DebugJsonSerializerContext.cs @@ -0,0 +1,12 @@ +// 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.Text.Json.Serialization; +using Steeltoe.Discovery.Eureka.AppInfo; + +namespace Steeltoe.Discovery.Eureka.Transport; + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(ApplicationInfoCollection))] +internal sealed partial class DebugJsonSerializerContext : JsonSerializerContext; diff --git a/src/Discovery/src/Eureka/Transport/DebugSerializerOptions.cs b/src/Discovery/src/Eureka/Transport/DebugSerializerOptions.cs deleted file mode 100644 index 4a75651bb1..0000000000 --- a/src/Discovery/src/Eureka/Transport/DebugSerializerOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Steeltoe.Discovery.Eureka.Transport; - -internal static class DebugSerializerOptions -{ - public static JsonSerializerOptions Instance { get; } = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }; -} diff --git a/src/Discovery/src/Eureka/Transport/EurekaJsonSerializerContext.cs b/src/Discovery/src/Eureka/Transport/EurekaJsonSerializerContext.cs new file mode 100644 index 0000000000..861981f501 --- /dev/null +++ b/src/Discovery/src/Eureka/Transport/EurekaJsonSerializerContext.cs @@ -0,0 +1,14 @@ +// 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.Text.Json.Serialization; + +namespace Steeltoe.Discovery.Eureka.Transport; + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(JsonApplicationsRoot))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(JsonInstanceInfoRoot))] +[JsonSerializable(typeof(List))] +internal sealed partial class EurekaJsonSerializerContext : JsonSerializerContext; diff --git a/src/Discovery/src/Eureka/Transport/JsonApplicationConverter.cs b/src/Discovery/src/Eureka/Transport/JsonApplicationConverter.cs index 906aaa80f1..7129f7d517 100644 --- a/src/Discovery/src/Eureka/Transport/JsonApplicationConverter.cs +++ b/src/Discovery/src/Eureka/Transport/JsonApplicationConverter.cs @@ -13,10 +13,10 @@ internal sealed class JsonApplicationConverter : JsonConverter>(ref reader, options)!; + return JsonSerializer.Deserialize(ref reader, EurekaJsonSerializerContext.Default.ListJsonApplication)!; } - var application = JsonSerializer.Deserialize(ref reader, options); + JsonApplication? application = JsonSerializer.Deserialize(ref reader, EurekaJsonSerializerContext.Default.JsonApplication); return application != null ? [application] : []; } diff --git a/src/Discovery/src/Eureka/Transport/JsonInstanceInfoConverter.cs b/src/Discovery/src/Eureka/Transport/JsonInstanceInfoConverter.cs index 2b751a374c..56f2f21ed7 100644 --- a/src/Discovery/src/Eureka/Transport/JsonInstanceInfoConverter.cs +++ b/src/Discovery/src/Eureka/Transport/JsonInstanceInfoConverter.cs @@ -13,10 +13,10 @@ internal sealed class JsonInstanceInfoConverter : JsonConverter>(ref reader, options)!; + return JsonSerializer.Deserialize(ref reader, EurekaJsonSerializerContext.Default.ListJsonInstanceInfo)!; } - var instanceInfo = JsonSerializer.Deserialize(ref reader, options); + JsonInstanceInfo? instanceInfo = JsonSerializer.Deserialize(ref reader, EurekaJsonSerializerContext.Default.JsonInstanceInfo); return instanceInfo != null ? [instanceInfo] : []; } diff --git a/src/Discovery/src/HttpClients/ConfigurationSchema.json b/src/Discovery/src/HttpClients/ConfigurationSchema.json new file mode 100644 index 0000000000..7543664787 --- /dev/null +++ b/src/Discovery/src/HttpClients/ConfigurationSchema.json @@ -0,0 +1,17 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Discovery": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Discovery.HttpClients": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs b/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs index 82c6477d3b..7903741d95 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/RandomLoadBalancer.cs @@ -5,13 +5,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Common.Discovery; +using Steeltoe.Common.Extensions; namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Returns random service instances. /// -public sealed class RandomLoadBalancer : ILoadBalancer +public sealed partial class RandomLoadBalancer : ILoadBalancer { private readonly ServiceInstancesResolver _serviceInstancesResolver; private readonly ILogger _logger; @@ -40,13 +41,13 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT ArgumentNullException.ThrowIfNull(requestUri); string serviceName = requestUri.Host; - _logger.LogTrace("Resolving service instance for '{ServiceName}'.", serviceName); + LogResolvingServiceInstance(serviceName); IList availableServiceInstances = await _serviceInstancesResolver.ResolveInstancesAsync(serviceName, cancellationToken); if (availableServiceInstances.Count == 0) { - _logger.LogWarning("No service instances are available for '{ServiceName}'.", serviceName); + LogNoServiceInstances(serviceName); return requestUri; } @@ -54,7 +55,7 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT int index = Random.Shared.Next(availableServiceInstances.Count); IServiceInstance serviceInstance = availableServiceInstances[index]; - _logger.LogDebug("Resolved '{ServiceName}' to '{ServiceInstance}'.", serviceName, serviceInstance.Uri); + LogServiceInstanceResolved(serviceName, serviceInstance.Uri); return new Uri(serviceInstance.Uri, requestUri.PathAndQuery); } @@ -64,4 +65,13 @@ public Task UpdateStatisticsAsync(Uri requestUri, Uri serviceInstanceUri, TimeSp cancellationToken.ThrowIfCancellationRequested(); return Task.CompletedTask; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Resolving service instance for '{ServiceName}'.")] + private partial void LogResolvingServiceInstance(string serviceName); + + [LoggerMessage(Level = LogLevel.Warning, Message = "No service instances are available for '{ServiceName}'.")] + private partial void LogNoServiceInstances(string serviceName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved '{ServiceName}' to '{ServiceInstance}'.")] + private partial void LogServiceInstanceResolved(string serviceName, MaskedUri serviceInstance); } diff --git a/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs b/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs index e0a913d367..9801280713 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/RoundRobinLoadBalancer.cs @@ -7,13 +7,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Common.Discovery; +using Steeltoe.Common.Extensions; namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Returns service instances in round-robin fashion, optionally using distributed caching for determining the next instance. /// -public sealed class RoundRobinLoadBalancer : ILoadBalancer +public sealed partial class RoundRobinLoadBalancer : ILoadBalancer { private const string CacheKeyPrefix = "Steeltoe-LoadBalancerIndex-"; private readonly ServiceInstancesResolver _serviceInstancesResolver; @@ -69,20 +70,20 @@ public async Task ResolveServiceInstanceAsync(Uri requestUri, CancellationT ArgumentNullException.ThrowIfNull(requestUri); string serviceName = requestUri.Host; - _logger.LogTrace("Resolving service instance for '{ServiceName}'.", serviceName); + LogResolvingServiceInstance(serviceName); IList availableServiceInstances = await _serviceInstancesResolver.ResolveInstancesAsync(serviceName, cancellationToken); if (availableServiceInstances.Count == 0) { - _logger.LogWarning("No service instances are available for '{ServiceName}'.", serviceName); + LogNoServiceInstances(serviceName); return requestUri; } int instanceIndex = await GetNextInstanceIndexAsync(serviceName, availableServiceInstances.Count, cancellationToken); IServiceInstance serviceInstance = availableServiceInstances[instanceIndex]; - _logger.LogDebug("Resolved '{ServiceName}' to '{ServiceInstance}'.", serviceName, serviceInstance.Uri); + LogServiceInstanceResolved(serviceName, serviceInstance.Uri); return new Uri(serviceInstance.Uri, requestUri.PathAndQuery); } @@ -128,4 +129,13 @@ public Task UpdateStatisticsAsync(Uri requestUri, Uri serviceInstanceUri, TimeSp cancellationToken.ThrowIfCancellationRequested(); return Task.CompletedTask; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Resolving service instance for '{ServiceName}'.")] + private partial void LogResolvingServiceInstance(string serviceName); + + [LoggerMessage(Level = LogLevel.Warning, Message = "No service instances are available for '{ServiceName}'.")] + private partial void LogNoServiceInstances(string serviceName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved '{ServiceName}' to '{ServiceInstance}'.")] + private partial void LogServiceInstanceResolved(string serviceName, MaskedUri serviceInstance); } diff --git a/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesJsonSerializerContext.cs b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesJsonSerializerContext.cs new file mode 100644 index 0000000000..3193aebbc5 --- /dev/null +++ b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesJsonSerializerContext.cs @@ -0,0 +1,11 @@ +// 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.Text.Json.Serialization; + +namespace Steeltoe.Discovery.HttpClients.LoadBalancers; + +[JsonSourceGenerationOptions] +[JsonSerializable(typeof(ServiceInstancesResolver.JsonSerializableServiceInstance[]))] +internal sealed partial class ServiceInstancesJsonSerializerContext : JsonSerializerContext; diff --git a/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs index 8f354d8da2..79a97ddc46 100644 --- a/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs +++ b/src/Discovery/src/HttpClients/LoadBalancers/ServiceInstancesResolver.cs @@ -14,7 +14,7 @@ namespace Steeltoe.Discovery.HttpClients.LoadBalancers; /// /// Queries all discovery clients for service instances, optionally caching the results using . /// -public sealed class ServiceInstancesResolver +public sealed partial class ServiceInstancesResolver { private readonly IDiscoveryClient[] _discoveryClients; private readonly IDistributedCache? _distributedCache; @@ -66,7 +66,7 @@ public ServiceInstancesResolver(IEnumerable discoveryClients, if (_discoveryClients.Length == 0) { - _logger.LogWarning("No discovery clients are registered."); + LogNoDiscoveryClients(); } } @@ -83,7 +83,8 @@ public async Task> ResolveInstancesAsync(string serviceI if (instancesFromCache != null) { - _logger.LogDebug("Returning {Count} instances from cache.", instancesFromCache.Count); + instancesFromCache = RemoveDuplicatesByUri(instancesFromCache); + LogReturningInstancesFromCache(instancesFromCache.Count); return instancesFromCache; } } @@ -99,10 +100,12 @@ public async Task> ResolveInstancesAsync(string serviceI } catch (Exception exception) { - _logger.LogError(exception, "Failed to get instances from {DiscoveryClient}.", discoveryClient.GetType()); + LogFailedToGetInstances(exception, discoveryClient.GetType()); } } + instances = RemoveDuplicatesByUri(instances); + if (_distributedCache != null) { byte[] cacheValue = ToCacheValue(instances); @@ -112,11 +115,28 @@ public async Task> ResolveInstancesAsync(string serviceI return instances; } + private static List RemoveDuplicatesByUri(List instances) + { + var seenUris = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (IServiceInstance instance in instances) + { + if (seenUris.Add(instance.Uri.AbsoluteUri)) + { + result.Add(instance); + } + } + + return result; + } + private static List? FromCacheValue(byte[]? cacheValue) { if (cacheValue is { Length: > 0 }) { - var serializableInstances = JsonSerializer.Deserialize>(cacheValue); + JsonSerializableServiceInstance[]? serializableInstances = + JsonSerializer.Deserialize(cacheValue, ServiceInstancesJsonSerializerContext.Default.JsonSerializableServiceInstanceArray); if (serializableInstances != null) { @@ -127,21 +147,33 @@ public async Task> ResolveInstancesAsync(string serviceI return null; } - private static byte[] ToCacheValue(IEnumerable instances) + private static byte[] ToCacheValue(List instances) { JsonSerializableServiceInstance[] serializableInstances = instances.Select(JsonSerializableServiceInstance.CopyFrom).ToArray(); - return JsonSerializer.SerializeToUtf8Bytes(serializableInstances); + return JsonSerializer.SerializeToUtf8Bytes(serializableInstances, ServiceInstancesJsonSerializerContext.Default.JsonSerializableServiceInstanceArray); } - private sealed class JsonSerializableServiceInstance : IServiceInstance + [LoggerMessage(Level = LogLevel.Warning, Message = "No discovery clients are registered.")] + private partial void LogNoDiscoveryClients(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning {Count} instances from cache.")] + private partial void LogReturningInstancesFromCache(int count); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get instances from {DiscoveryClient}.")] + private partial void LogFailedToGetInstances(Exception exception, Type discoveryClient); + + internal sealed class JsonSerializableServiceInstance : IServiceInstance { // Trust that deserialized instances meet the IServiceInstance contract, so suppress nullability warnings. public string ServiceId { get; set; } = null!; + public string InstanceId { get; set; } = null!; public string Host { get; set; } = null!; public int Port { get; set; } public bool IsSecure { get; set; } public Uri Uri { get; set; } = null!; + public Uri? NonSecureUri { get; set; } + public Uri? SecureUri { get; set; } public IReadOnlyDictionary Metadata { get; set; } = null!; public static JsonSerializableServiceInstance CopyFrom(IServiceInstance instance) @@ -151,10 +183,13 @@ public static JsonSerializableServiceInstance CopyFrom(IServiceInstance instance return new JsonSerializableServiceInstance { ServiceId = instance.ServiceId, + InstanceId = instance.InstanceId, Host = instance.Host, Port = instance.Port, IsSecure = instance.IsSecure, Uri = instance.Uri, + NonSecureUri = instance.NonSecureUri, + SecureUri = instance.SecureUri, Metadata = instance.Metadata }; } diff --git a/src/Discovery/src/HttpClients/Properties/AssemblyInfo.cs b/src/Discovery/src/HttpClients/Properties/AssemblyInfo.cs index 5bed50d08c..61c147559d 100644 --- a/src/Discovery/src/HttpClients/Properties/AssemblyInfo.cs +++ b/src/Discovery/src/HttpClients/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.Discovery", "Steeltoe.Discovery.HttpClients")] [assembly: InternalsVisibleTo("Steeltoe.Discovery.HttpClients.Test")] diff --git a/src/Discovery/src/HttpClients/Steeltoe.Discovery.HttpClients.csproj b/src/Discovery/src/HttpClients/Steeltoe.Discovery.HttpClients.csproj index 2987e9dacd..27d2549b4b 100644 --- a/src/Discovery/src/HttpClients/Steeltoe.Discovery.HttpClients.csproj +++ b/src/Discovery/src/HttpClients/Steeltoe.Discovery.HttpClients.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Enables HTTP clients to load-balance over discovered service instances. service-discovery;service-registry;load-balancing;round-robin;httpclient;httpclientfactory true diff --git a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs index 10b2856e2f..f83e5b555c 100644 --- a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs +++ b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs @@ -2,12 +2,13 @@ // 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 FluentAssertions.Extensions; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Steeltoe.Common.Discovery; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Discovery.Configuration.Test; @@ -119,13 +120,11 @@ public async Task AddConfigurationDiscoveryClient_AddsClientWithOptions() } """; - 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); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); @@ -179,4 +178,100 @@ public async Task RegistersHostedService() serviceProvider.GetServices().OfType().Should().ContainSingle(); } + + [Fact] + public void Does_not_register_multiple_times() + { + var services = new ServiceCollection(); + services.AddConfigurationDiscoveryClient(); + int beforeServiceCount = services.Count; + + services.AddConfigurationDiscoveryClient(); + + services.Count.Should().Be(beforeServiceCount); + } + + [Fact] + public async Task InstancesFetched_event_is_raised_after_configuration_change() + { + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Discovery": { + "Services": [ + { + "ServiceId": "serviceA", + "host": "instanceA1", + "port": 443, + "isSecure": true + }, + { + "ServiceId": "serviceA", + "host": "instanceA2", + "port": 443, + "isSecure": true + }, + { + "ServiceId": "serviceB", + "host": "instanceB1", + "port": 443, + "isSecure": true + } + ] + } + } + """); + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); + builder.Services.AddConfigurationDiscoveryClient(); + await using WebApplication webApplication = builder.Build(); + + ConfigurationDiscoveryClient discoveryClient = webApplication.Services.GetServices().OfType().Single(); + DiscoveryInstancesFetchedEventArgs? eventArgs = null; + int eventCount = 0; + + discoveryClient.InstancesFetched += (_, args) => + { + eventArgs = args; + Interlocked.Increment(ref eventCount); + }; + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Discovery": { + "Services": [ + { + "ServiceId": "serviceA", + "host": "instanceA1", + "port": 443, + "isSecure": true + }, + { + "ServiceId": "serviceB", + "host": "instanceB1", + "port": 443, + "isSecure": true + }, + { + "ServiceId": "serviceB", + "host": "instanceB2", + "port": 443, + "isSecure": true + } + ] + } + } + """); + + fileProvider.NotifyChanged(); + + SpinWait.SpinUntil(() => eventCount == 1, 5.Seconds()).Should().BeTrue(); + + eventArgs.Should().NotBeNull(); + eventArgs.InstancesByServiceId.Should().HaveCount(2); + eventArgs.InstancesByServiceId.Should().ContainKey("ServiceA").WhoseValue.Should().HaveCount(1); + eventArgs.InstancesByServiceId.Should().ContainKey("ServiceB").WhoseValue.Should().HaveCount(2); + } } diff --git a/src/Discovery/test/Configuration.Test/Steeltoe.Discovery.Configuration.Test.csproj b/src/Discovery/test/Configuration.Test/Steeltoe.Discovery.Configuration.Test.csproj index a1d257dfa7..7d56666bcb 100644 --- a/src/Discovery/test/Configuration.Test/Steeltoe.Discovery.Configuration.Test.csproj +++ b/src/Discovery/test/Configuration.Test/Steeltoe.Discovery.Configuration.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Discovery/test/Consul.Test/Discovery/ConsulDiscoveryClientTest.cs b/src/Discovery/test/Consul.Test/Discovery/ConsulDiscoveryClientTest.cs index 88caae557d..6cc5cb66e2 100644 --- a/src/Discovery/test/Consul.Test/Discovery/ConsulDiscoveryClientTest.cs +++ b/src/Discovery/test/Consul.Test/Discovery/ConsulDiscoveryClientTest.cs @@ -32,6 +32,7 @@ public async Task AddInstancesToListAsync_AddsExpected() Service = new AgentService { Service = "ServiceId", + ID = "Instance1", Address = "foo.bar.com", Port = 1234, Meta = new Dictionary @@ -46,6 +47,7 @@ public async Task AddInstancesToListAsync_AddsExpected() Service = new AgentService { Service = "ServiceId", + ID = "Instance2", Address = "foo1.bar1.com", Port = 5678, Meta = new Dictionary @@ -79,21 +81,27 @@ await discoveryClient.AddInstancesToListAsync(serviceInstances, "ServiceId", Que serviceInstances[0].Host.Should().Be("foo.bar.com"); serviceInstances[0].ServiceId.Should().Be("ServiceId"); + serviceInstances[0].InstanceId.Should().Be("Instance1"); serviceInstances[0].IsSecure.Should().BeTrue(); serviceInstances[0].Port.Should().Be(1234); serviceInstances[0].Metadata.Should().HaveCount(2); serviceInstances[0].Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); serviceInstances[0].Metadata.Should().ContainKey("secure").WhoseValue.Should().Be("true"); serviceInstances[0].Uri.Should().Be(new Uri("https://foo.bar.com:1234")); + serviceInstances[0].NonSecureUri.Should().BeNull(); + serviceInstances[0].SecureUri.Should().Be(serviceInstances[0].Uri); serviceInstances[1].Host.Should().Be("foo1.bar1.com"); serviceInstances[1].ServiceId.Should().Be("ServiceId"); + serviceInstances[1].InstanceId.Should().Be("Instance2"); serviceInstances[1].IsSecure.Should().BeFalse(); serviceInstances[1].Port.Should().Be(5678); serviceInstances[1].Metadata.Should().HaveCount(2); serviceInstances[1].Metadata.Should().ContainKey("bar").WhoseValue.Should().Be("foo"); serviceInstances[1].Metadata.Should().ContainKey("secure").WhoseValue.Should().Be("false"); serviceInstances[1].Uri.Should().Be(new Uri("http://foo1.bar1.com:5678")); + serviceInstances[1].NonSecureUri.Should().Be(serviceInstances[1].Uri); + serviceInstances[1].SecureUri.Should().BeNull(); } [Fact] @@ -169,6 +177,7 @@ public async Task GetAllInstances_ReturnsExpected() Service = new AgentService { Service = "ServiceId", + ID = "Instance1", Address = "foo.bar.com", Port = 1234, Meta = new Dictionary @@ -183,6 +192,7 @@ public async Task GetAllInstances_ReturnsExpected() Service = new AgentService { Service = "ServiceId", + ID = "Instance2", Address = "foo1.bar1.com", Port = 5678, Meta = new Dictionary @@ -215,20 +225,26 @@ public async Task GetAllInstances_ReturnsExpected() serviceInstances[0].Host.Should().Be("foo.bar.com"); serviceInstances[0].ServiceId.Should().Be("ServiceId"); + serviceInstances[0].InstanceId.Should().Be("Instance1"); serviceInstances[0].IsSecure.Should().BeTrue(); serviceInstances[0].Port.Should().Be(1234); serviceInstances[0].Metadata.Should().HaveCount(2); serviceInstances[0].Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); serviceInstances[0].Metadata.Should().ContainKey("secure").WhoseValue.Should().Be("true"); serviceInstances[0].Uri.Should().Be(new Uri("https://foo.bar.com:1234")); + serviceInstances[0].NonSecureUri.Should().BeNull(); + serviceInstances[0].SecureUri.Should().Be(serviceInstances[0].Uri); serviceInstances[1].Host.Should().Be("foo1.bar1.com"); serviceInstances[1].ServiceId.Should().Be("ServiceId"); + serviceInstances[1].InstanceId.Should().Be("Instance2"); serviceInstances[1].IsSecure.Should().BeFalse(); serviceInstances[1].Port.Should().Be(5678); serviceInstances[1].Metadata.Should().HaveCount(2); serviceInstances[1].Metadata.Should().ContainKey("bar").WhoseValue.Should().Be("foo"); serviceInstances[1].Metadata.Should().ContainKey("secure").WhoseValue.Should().Be("false"); serviceInstances[1].Uri.Should().Be(new Uri("http://foo1.bar1.com:5678")); + serviceInstances[1].NonSecureUri.Should().Be(serviceInstances[1].Uri); + serviceInstances[1].SecureUri.Should().BeNull(); } } diff --git a/src/Discovery/test/Consul.Test/Discovery/ConsulServiceInstanceTest.cs b/src/Discovery/test/Consul.Test/Discovery/ConsulServiceInstanceTest.cs index a9426cd7a2..a104511460 100644 --- a/src/Discovery/test/Consul.Test/Discovery/ConsulServiceInstanceTest.cs +++ b/src/Discovery/test/Consul.Test/Discovery/ConsulServiceInstanceTest.cs @@ -16,6 +16,7 @@ public void Constructor_Initializes() Service = new AgentService { Service = "ServiceId", + ID = "Instance1", Address = "foo.bar.com", Port = 1234, Tags = @@ -35,6 +36,7 @@ public void Constructor_Initializes() serviceInstance.Host.Should().Be("foo.bar.com"); serviceInstance.ServiceId.Should().Be("ServiceId"); + serviceInstance.InstanceId.Should().Be("Instance1"); serviceInstance.IsSecure.Should().BeTrue(); serviceInstance.Port.Should().Be(1234); serviceInstance.Tags.Should().HaveCount(2); @@ -44,5 +46,27 @@ public void Constructor_Initializes() serviceInstance.Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); serviceInstance.Metadata.Should().ContainKey("secure").WhoseValue.Should().Be("true"); serviceInstance.Uri.Should().Be(new Uri("https://foo.bar.com:1234")); + serviceInstance.NonSecureUri.Should().BeNull(); + serviceInstance.SecureUri.Should().Be(serviceInstance.Uri); + } + + [Fact] + public void Constructor_accepts_null_tags_and_meta() + { + var healthService = new ServiceEntry + { + Service = new AgentService + { + Service = "ServiceId", + ID = "Instance1", + Address = "foo.bar.com", + Port = 1234 + } + }; + + var serviceInstance = new ConsulServiceInstance(healthService); + + serviceInstance.Tags.Should().BeEmpty(); + serviceInstance.Metadata.Should().BeEmpty(); } } diff --git a/src/Discovery/test/Consul.Test/Discovery/PostConfigureConsulDiscoveryOptionsTest.cs b/src/Discovery/test/Consul.Test/Discovery/PostConfigureConsulDiscoveryOptionsTest.cs index aaf7cf0f20..8f468dc17f 100644 --- a/src/Discovery/test/Consul.Test/Discovery/PostConfigureConsulDiscoveryOptionsTest.cs +++ b/src/Discovery/test/Consul.Test/Discovery/PostConfigureConsulDiscoveryOptionsTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; @@ -41,7 +42,8 @@ public void Constructor_InitializesDefaults() options.InstanceZone.Should().BeNull(); options.PreferIPAddress.Should().BeFalse(); options.QueryPassing.Should().BeTrue(); - options.Scheme.Should().Be("http"); + options.Scheme.Should().BeNull(); + options.EffectiveScheme.Should().Be("http"); options.ServiceName.Should().BeNull(); options.Tags.Should().BeEmpty(); options.Metadata.Should().BeEmpty(); @@ -106,8 +108,7 @@ public async Task CanUseNetworkInterfaces() inetUtilsMock.Verify(n => n.FindFirstNonLoopbackHostInfo(), Times.Once); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task CanUseNetworkInterfacesWithoutReverseDnsOnIP() { var appSettings = new Dictionary @@ -136,7 +137,7 @@ public async Task CanUseNetworkInterfacesWithoutReverseDnsOnIP() noSlowReverseDnsQuery.Stop(); options.HostName.Should().NotBeNull(); - noSlowReverseDnsQuery.ElapsedMilliseconds.Should().BeInRange(0, 1500); // testing with an actual reverse dns query results in around 5000 ms + noSlowReverseDnsQuery.ElapsedMilliseconds.Should().BeInRange(0, 2000); // testing with an actual reverse dns query results in around 5000 ms } [Fact] diff --git a/src/Discovery/test/Consul.Test/Discovery/ThisServiceInstanceTest.cs b/src/Discovery/test/Consul.Test/Discovery/ThisServiceInstanceTest.cs index 1b4fa260cd..1ae187bd1e 100644 --- a/src/Discovery/test/Consul.Test/Discovery/ThisServiceInstanceTest.cs +++ b/src/Discovery/test/Consul.Test/Discovery/ThisServiceInstanceTest.cs @@ -32,10 +32,13 @@ public void Constructor_Initializes() instance.Host.Should().Be("test.foo.bar"); instance.ServiceId.Should().Be("foobar"); + instance.InstanceId.Should().Be("ID"); instance.IsSecure.Should().BeFalse(); instance.Port.Should().Be(1234); instance.Metadata.Should().ContainSingle(); instance.Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); instance.Uri.Should().Be(new Uri("http://test.foo.bar:1234")); + instance.NonSecureUri.Should().Be(instance.Uri); + instance.SecureUri.Should().BeNull(); } } diff --git a/src/Discovery/test/Consul.Test/Registry/ConsulRegistrationTest.cs b/src/Discovery/test/Consul.Test/Registry/ConsulRegistrationTest.cs index ef8854a771..fcc2a27912 100644 --- a/src/Discovery/test/Consul.Test/Registry/ConsulRegistrationTest.cs +++ b/src/Discovery/test/Consul.Test/Registry/ConsulRegistrationTest.cs @@ -46,6 +46,8 @@ public void Constructor_SetsProperties() registration.Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); registration.IsSecure.Should().BeFalse(); registration.Uri.Should().Be(new Uri("http://address:1234")); + registration.NonSecureUri.Should().Be(registration.Uri); + registration.SecureUri.Should().BeNull(); } [Fact] @@ -161,7 +163,7 @@ public void CreateCheck_ReturnsExpected() options.Heartbeat = null; const int port = 1234; result = ConsulRegistration.CreateCheck(port, options); - var uri = new Uri($"{options.Scheme}://{options.HostName}:{port}{options.HealthCheckPath}"); + var uri = new Uri($"{options.EffectiveScheme}://{options.HostName}:{port}{options.HealthCheckPath}"); result.HTTP.Should().Be(uri.ToString()); result.Interval.Should().Be(DateTimeConversions.ToTimeSpan(options.HealthCheckInterval!)); @@ -177,17 +179,20 @@ public void CreateRegistration_ReturnsExpected() { ["spring:application:name"] = "foobar", ["consul:discovery:hostName"] = "some-host", - ["consul:discovery:port"] = "1100" + ["consul:discovery:port"] = "1100", + ["consul:discovery:scheme"] = "https" }; ConsulRegistration registration = TestRegistrationFactory.Create(appSettings); registration.InstanceId.Should().StartWith("foobar-"); - registration.IsSecure.Should().BeFalse(); + registration.IsSecure.Should().BeTrue(); registration.ServiceId.Should().Be("foobar"); registration.Host.Should().Be("some-host"); registration.Port.Should().Be(1100); - registration.Uri.Should().Be(new Uri("http://some-host:1100")); + registration.Uri.Should().Be(new Uri("https://some-host:1100")); + registration.SecureUri.Should().Be(registration.Uri); + registration.NonSecureUri.Should().BeNull(); registration.InnerRegistration.Should().NotBeNull(); registration.InnerRegistration.Address.Should().Be("some-host"); diff --git a/src/Discovery/test/Consul.Test/Steeltoe.Discovery.Consul.Test.csproj b/src/Discovery/test/Consul.Test/Steeltoe.Discovery.Consul.Test.csproj index 0c13f5fb1c..9d04c2d612 100644 --- a/src/Discovery/test/Consul.Test/Steeltoe.Discovery.Consul.Test.csproj +++ b/src/Discovery/test/Consul.Test/Steeltoe.Discovery.Consul.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Discovery/test/Eureka.Test/AppInfo/ApplicationInfoCollectionTest.cs b/src/Discovery/test/Eureka.Test/AppInfo/ApplicationInfoCollectionTest.cs index 78a3ee441d..5dca00ae23 100644 --- a/src/Discovery/test/Eureka.Test/AppInfo/ApplicationInfoCollectionTest.cs +++ b/src/Discovery/test/Eureka.Test/AppInfo/ApplicationInfoCollectionTest.cs @@ -2,8 +2,12 @@ // 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.Collections.ObjectModel; +using FluentAssertions.Extensions; using Steeltoe.Discovery.Eureka.AppInfo; +using Steeltoe.Discovery.Eureka.Configuration; using Steeltoe.Discovery.Eureka.Transport; +using Steeltoe.Discovery.Eureka.Util; namespace Steeltoe.Discovery.Eureka.Test.AppInfo; @@ -64,8 +68,8 @@ public void Add_ExpandsTo_ApplicationMap() apps.GetInstancesByVipAddress("vip1a").Should().ContainSingle(); apps.GetInstancesByVipAddress("vip1b").Should().ContainSingle(); - apps.GetInstancesBySecureVipAddress("svip2a").Should().ContainSingle(); - apps.GetInstancesBySecureVipAddress("svip2b").Should().ContainSingle(); + apps.GetInstancesByVipAddress("svip2a").Should().ContainSingle(); + apps.GetInstancesByVipAddress("svip2b").Should().ContainSingle(); } [Fact] @@ -117,13 +121,11 @@ public void Add_AddsTo_VirtualHostInstanceMaps() app2 ]; - apps.VipInstanceMap.Should().HaveCount(2); + apps.VipInstanceMap.Should().HaveCount(4); apps.VipInstanceMap.Should().ContainKey("vapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); apps.VipInstanceMap.Should().ContainKey("vapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - - apps.SecureVipInstanceMap.Should().HaveCount(2); - apps.SecureVipInstanceMap.Should().ContainKey("svapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - apps.SecureVipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); + apps.VipInstanceMap.Should().ContainKey("svapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); + apps.VipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); } [Fact] @@ -159,26 +161,20 @@ public void RemoveInstanceFromVip_UpdatesApp_RemovesFromVirtualHostInstanceMaps( ]) ]); - apps.VipInstanceMap.Should().HaveCount(2); + apps.VipInstanceMap.Should().HaveCount(4); apps.VipInstanceMap.Should().ContainKey("vapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); apps.VipInstanceMap.Should().ContainKey("vapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); + apps.VipInstanceMap.Should().ContainKey("svapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); + apps.VipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - apps.SecureVipInstanceMap.Should().HaveCount(2); - apps.SecureVipInstanceMap.Should().ContainKey("svapp1".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - apps.SecureVipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - - apps.RemoveInstanceFromVip(new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build()); - apps.RemoveInstanceFromVip(new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build()); + apps.RemoveFromVipInstanceMap(new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build()); + apps.RemoveFromVipInstanceMap(new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build()); - apps.VipInstanceMap.Should().ContainSingle(); - apps.VipInstanceMap.Should().NotContainKey("vapp1".ToUpperInvariant()); - apps.VipInstanceMap.TryGetValue("vapp1".ToUpperInvariant(), out _).Should().BeFalse(); + apps.VipInstanceMap.Should().HaveCount(4); + apps.VipInstanceMap.Should().ContainKey("vapp1".ToUpperInvariant()).WhoseValue.Should().BeEmpty(); apps.VipInstanceMap.Should().ContainKey("vapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); - - apps.SecureVipInstanceMap.Should().ContainSingle(); - apps.SecureVipInstanceMap.Should().NotContainKey("svapp1".ToUpperInvariant()); - apps.SecureVipInstanceMap.TryGetValue("svapp1".ToUpperInvariant(), out _).Should().BeFalse(); - apps.SecureVipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); + apps.VipInstanceMap.Should().ContainKey("svapp1".ToUpperInvariant()).WhoseValue.Should().BeEmpty(); + apps.VipInstanceMap.Should().ContainKey("svapp2".ToUpperInvariant()).WhoseValue.Should().HaveCount(2); } [Fact] @@ -206,16 +202,22 @@ public void GetRegisteredApplication_ReturnsExpected() } [Fact] - public void GetInstancesBySecureVipAddress_ReturnsExpected() + public void GetInstancesByVipAddress_ReturnsExpected() { + InstanceInfo instance11 = new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build(); + InstanceInfo instance12 = new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build(); + var app1 = new ApplicationInfo("app1", [ - new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build(), - new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build() + instance11, + instance12 ]); + InstanceInfo instance21 = new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build(); + InstanceInfo instance22 = new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build(); + var app2 = new ApplicationInfo("app2", [ - new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build(), - new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build() + instance21, + instance22 ]); var apps = new ApplicationInfoCollection([ @@ -223,56 +225,31 @@ public void GetInstancesBySecureVipAddress_ReturnsExpected() app2 ]); - List result = apps.GetInstancesBySecureVipAddress("svapp1"); + ReadOnlyCollection secureInstances1 = apps.GetInstancesByVipAddress("svapp1"); - result.Should().HaveCount(2); - result.Should().Contain(app1.GetInstance("id1")!); - result.Should().Contain(app1.GetInstance("id2")!); + secureInstances1.Should().HaveCount(2); + secureInstances1.Should().Contain(instance11); + secureInstances1.Should().Contain(instance12); - result = apps.GetInstancesBySecureVipAddress("svapp2"); + ReadOnlyCollection secureInstances2 = apps.GetInstancesByVipAddress("svapp2"); - result.Should().HaveCount(2); - result.Should().Contain(app2.GetInstance("id1")!); - result.Should().Contain(app2.GetInstance("id2")!); + secureInstances2.Should().HaveCount(2); + secureInstances2.Should().Contain(instance21); + secureInstances2.Should().Contain(instance22); - result = apps.GetInstancesBySecureVipAddress("foobar"); + ReadOnlyCollection nonSecureInstances1 = apps.GetInstancesByVipAddress("vapp1"); - result.Should().BeEmpty(); - } + nonSecureInstances1.Should().HaveCount(2); + nonSecureInstances1.Should().Contain(instance11); + nonSecureInstances1.Should().Contain(instance12); - [Fact] - public void GetInstancesByVipAddress_ReturnsExpected() - { - var app1 = new ApplicationInfo("app1", [ - new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build(), - new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp1").WithSecureVipAddress("svapp1").Build() - ]); + ReadOnlyCollection nonSecureInstances2 = apps.GetInstancesByVipAddress("vapp2"); - var app2 = new ApplicationInfo("app2", [ - new InstanceInfoBuilder().WithId("id1").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build(), - new InstanceInfoBuilder().WithId("id2").WithVipAddress("vapp2").WithSecureVipAddress("svapp2").Build() - ]); + nonSecureInstances2.Should().HaveCount(2); + nonSecureInstances2.Should().Contain(instance21); + nonSecureInstances2.Should().Contain(instance22); - var apps = new ApplicationInfoCollection([ - app1, - app2 - ]); - - List result = apps.GetInstancesByVipAddress("vapp1"); - - result.Should().HaveCount(2); - result.Should().Contain(app1.GetInstance("id1")!); - result.Should().Contain(app1.GetInstance("id2")!); - - result = apps.GetInstancesByVipAddress("vapp2"); - - result.Should().HaveCount(2); - result.Should().Contain(app2.GetInstance("id1")!); - result.Should().Contain(app2.GetInstance("id2")!); - - result = apps.GetInstancesByVipAddress("foobar"); - - result.Should().BeEmpty(); + apps.GetInstancesByVipAddress("foobar").Should().BeEmpty(); } [Fact] @@ -310,7 +287,7 @@ public void UpdateFromDelta_EmptyDelta_NoChange() registered.Name.Should().Be("app2"); registered.Instances.Should().HaveCount(2); - List result = apps.GetInstancesByVipAddress("vapp1"); + ReadOnlyCollection result = apps.GetInstancesByVipAddress("vapp1"); result.Should().HaveCount(2); result.Should().Contain(app1.GetInstance("id1")!); @@ -375,7 +352,7 @@ public void UpdateFromDelta_AddNewAppNewInstance_UpdatesCorrectly() registered.Name.Should().Be("app3"); registered.Instances.Should().ContainSingle(); - List result = apps.GetInstancesByVipAddress("vapp1"); + ReadOnlyCollection result = apps.GetInstancesByVipAddress("vapp1"); result.Should().HaveCount(2); result.Should().Contain(app1.GetInstance("id1")!); @@ -438,7 +415,7 @@ public void UpdateFromDelta_ExistingAppWithAddNewInstance_UpdatesCorrectly() registered.Name.Should().Be("app2"); registered.Instances.Should().HaveCount(3); - List result = apps.GetInstancesByVipAddress("vapp1"); + ReadOnlyCollection result = apps.GetInstancesByVipAddress("vapp1"); result.Should().HaveCount(2); result.Should().Contain(app1.GetInstance("id1")!); @@ -501,7 +478,7 @@ public void UpdateFromDelta_ExistingAppWithModifyInstance_UpdatesCorrectly() registered.Instances.Should().HaveCount(2); registered.Instances.Should().AllSatisfy(instance => instance.Status.Should().Be(InstanceStatus.Up)); - List result = apps.GetInstancesByVipAddress("vapp1"); + ReadOnlyCollection result = apps.GetInstancesByVipAddress("vapp1"); result.Should().HaveCount(2); result.Should().Contain(app1.GetInstance("id1")!); @@ -562,7 +539,7 @@ public void UpdateFromDelta_ExistingAppWithRemovedInstance_UpdatesCorrectly() registered.Name.Should().Be("app2"); registered.Instances.Should().ContainSingle().Which.Status.Should().Be(InstanceStatus.Up); - List result = apps.GetInstancesByVipAddress("vapp1"); + ReadOnlyCollection result = apps.GetInstancesByVipAddress("vapp1"); result.Should().HaveCount(2); result.Should().Contain(app1.GetInstance("id1")!); @@ -823,4 +800,127 @@ public void FromJsonApplications_WithMissingInstanceId() app.Name.Should().Be("myApp"); app.Instances.Should().BeEmpty(); } + + [Fact] + public void ToString_ReturnsExpected() + { + var apps = new ApplicationInfoCollection([ + new ApplicationInfo("ServiceA", [ + new InstanceInfo("full-instance-001", "ServiceA", "prod-server-01.example.com", "10.20.30.40", new DataCenterInfo + { + Name = DataCenterName.Amazon + }, TimeProvider.System) + { + AppGroupName = "ServiceGroup", + Status = InstanceStatus.Up, + OverriddenStatus = InstanceStatus.OutOfService, + VipAddress = "service-a-vip", + SecureVipAddress = "service-a-secure-vip", + NonSecurePort = 8080, + IsNonSecurePortEnabled = true, + SecurePort = 8443, + IsSecurePortEnabled = true, + HomePageUrl = "http://prod-server-01.example.com:8080/", + StatusPageUrl = "http://prod-server-01.example.com:8080/actuator/info", + HealthCheckUrl = "http://prod-server-01.example.com:8080/actuator/health", + SecureHealthCheckUrl = "https://prod-server-01.example.com:8443/actuator/health", + LeaseInfo = LeaseInfo.FromJson(new JsonLeaseInfo + { + RenewalIntervalInSeconds = 30, + DurationInSeconds = 90, + RegistrationTimestamp = DateTimeConversions.ToJavaMilliseconds(15.June(2024).At(14, 30, 55, 123).AsUtc()), + LastRenewalTimestamp = DateTimeConversions.ToJavaMilliseconds(18.June(2024).At(23, 1, 27, 789).AsUtc()), + EvictionTimestamp = DateTimeConversions.ToJavaMilliseconds(19.June(2024).At(1, 1, 27, 789).AsUtc()), + ServiceUpTimestamp = DateTimeConversions.ToJavaMilliseconds(15.June(2024).At(14, 31, 2, 456).AsUtc()) + }), + ActionType = ActionType.Added, + Metadata = new Dictionary + { + ["datacenter"] = "us-east-1", + ["availability-zone"] = "us-east-1a", + ["version"] = "3.2.1", + ["environment"] = "production", + ["deployment"] = "blue", + ["team"] = "platform" + } + }, + new InstanceInfo("minimal-instance-001", "ServiceA", "prod-server-02.local", "10.20.30.41", new DataCenterInfo + { + Name = DataCenterName.Netflix + }, TimeProvider.System) + ]), + new ApplicationInfo("EmptyService") + ]); + + apps.ToString().Should().Be(""" + [ + { + "Name": "EmptyService", + "Instances": [] + }, + { + "Name": "ServiceA", + "Instances": [ + { + "InstanceId": "full-instance-001", + "AppName": "ServiceA", + "AppGroupName": "ServiceGroup", + "HostName": "prod-server-01.example.com", + "IPAddress": "10.20.30.40", + "DataCenterInfo": { + "Name": "Amazon" + }, + "VipAddress": "service-a-vip", + "SecureVipAddress": "service-a-secure-vip", + "NonSecurePort": 8080, + "IsNonSecurePortEnabled": true, + "SecurePort": 8443, + "IsSecurePortEnabled": true, + "Status": "UP", + "OverriddenStatus": "OUT_OF_SERVICE", + "EffectiveStatus": "OUT_OF_SERVICE", + "HomePageUrl": "http://prod-server-01.example.com:8080/", + "StatusPageUrl": "http://prod-server-01.example.com:8080/actuator/info", + "HealthCheckUrl": "http://prod-server-01.example.com:8080/actuator/health", + "SecureHealthCheckUrl": "https://prod-server-01.example.com:8443/actuator/health", + "LeaseInfo": { + "RenewalInterval": "00:00:30", + "Duration": "00:01:30", + "RegistrationTimeUtc": "2024-06-15T14:30:55.123Z", + "LastRenewalTimeUtc": "2024-06-18T23:01:27.789Z", + "EvictionTimeUtc": "2024-06-19T01:01:27.789Z", + "ServiceUpTimeUtc": "2024-06-15T14:31:02.456Z" + }, + "Metadata": { + "datacenter": "us-east-1", + "availability-zone": "us-east-1a", + "version": "3.2.1", + "environment": "production", + "deployment": "blue", + "team": "platform" + }, + "ActionType": "ADDED", + "IsDirty": false + }, + { + "InstanceId": "minimal-instance-001", + "AppName": "ServiceA", + "HostName": "prod-server-02.local", + "IPAddress": "10.20.30.41", + "DataCenterInfo": { + "Name": "Netflix" + }, + "NonSecurePort": 0, + "IsNonSecurePortEnabled": false, + "SecurePort": 0, + "IsSecurePortEnabled": false, + "EffectiveStatus": "UNKNOWN", + "Metadata": {}, + "IsDirty": false + } + ] + } + ] + """); + } } diff --git a/src/Discovery/test/Eureka.Test/AppInfo/DataCenterInfoTest.cs b/src/Discovery/test/Eureka.Test/AppInfo/DataCenterInfoTest.cs index 9814925256..66580400c9 100644 --- a/src/Discovery/test/Eureka.Test/AppInfo/DataCenterInfoTest.cs +++ b/src/Discovery/test/Eureka.Test/AppInfo/DataCenterInfoTest.cs @@ -36,23 +36,28 @@ public void ToJson_Correct() json.ClassName.Should().Be("com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"); } - [Fact] - public void FromJson_Correct() + [Theory] + [InlineData("Netflix")] + [InlineData("Amazon")] + [InlineData("MyOwn")] + public void FromJson_Correct(string name) { var jsonInfo = new JsonDataCenterInfo { ClassName = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - Name = "MyOwn" + Name = name }; + var expected = Enum.Parse(name); + DataCenterInfo? result = DataCenterInfo.FromJson(jsonInfo); result.Should().NotBeNull(); - result.Name.Should().Be(DataCenterName.MyOwn); + result.Name.Should().Be(expected); } [Fact] - public void FromJson_Throws_Invalid() + public void FromJson_ReturnsNull_WhenInvalid() { var jsonInfo = new JsonDataCenterInfo { @@ -60,8 +65,8 @@ public void FromJson_Throws_Invalid() Name = "FooBar" }; - Action action = () => DataCenterInfo.FromJson(jsonInfo); + DataCenterInfo? result = DataCenterInfo.FromJson(jsonInfo); - action.Should().ThrowExactly().WithMessage("Unsupported datacenter name*"); + result.Should().BeNull(); } } diff --git a/src/Discovery/test/Eureka.Test/CloudFoundryTest.cs b/src/Discovery/test/Eureka.Test/CloudFoundryTest.cs index 9a86f5a7a4..d0b5129b8b 100644 --- a/src/Discovery/test/Eureka.Test/CloudFoundryTest.cs +++ b/src/Discovery/test/Eureka.Test/CloudFoundryTest.cs @@ -75,7 +75,7 @@ public async Task NoVCAPEnvVariables_ConfiguresEurekaDiscovery_Correctly() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddJsonStream(stream); builder.AddCloudFoundryConfiguration(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Services.AddEurekaDiscoveryClient(); await using WebApplication app = builder.Build(); @@ -269,7 +269,7 @@ public async Task WithVCAPEnvVariables_HostName_ConfiguresEurekaDiscovery_Correc WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddJsonStream(stream); builder.AddCloudFoundryConfiguration(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Services.AddEurekaDiscoveryClient(); await using WebApplication app = builder.Build(); @@ -464,7 +464,7 @@ public async Task WithVCAPEnvVariables_Route_ConfiguresEurekaDiscovery_Correctly WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddJsonStream(stream); builder.AddCloudFoundryConfiguration(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Services.AddEurekaDiscoveryClient(); await using WebApplication app = builder.Build(); @@ -661,7 +661,7 @@ public async Task WithVCAPEnvVariables_AppName_Overrides_VCAPBinding() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddJsonStream(stream); builder.AddCloudFoundryConfiguration(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Services.AddEurekaDiscoveryClient(); await using WebApplication app = builder.Build(); @@ -760,7 +760,7 @@ public async Task WithVCAPEnvVariables_ButNoUri_DoesNotThrow() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.AddCloudFoundryConfiguration(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Services.AddEurekaDiscoveryClient(); await using WebApplication app = builder.Build(); diff --git a/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs b/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs index 3b0265f868..b3adc4687e 100644 --- a/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs +++ b/src/Discovery/test/Eureka.Test/DynamicPortAssignmentTest.cs @@ -2,17 +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.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Steeltoe.Common.TestResources; +using Steeltoe.Discovery.Eureka.Configuration; namespace Steeltoe.Discovery.Eureka.Test; public sealed class DynamicPortAssignmentTest { - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task Applies_dynamically_assigned_ports_after_startup() { var appSettings = new Dictionary @@ -38,4 +40,147 @@ public async Task Applies_dynamically_assigned_ports_after_startup() infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); infoManager.Instance.SecurePort.Should().BePositive(); } + + [Fact] + public async Task Applies_dynamically_assigned_ports_when_kestrel_overrides_urls_config() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["urls"] = "http://*:5000" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.Configuration.AddInMemoryCollection(appSettings); + + builder.WebHost.ConfigureKestrel(options => + { + options.ListenAnyIP(0); + }); + + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.NonSecurePort.Should().NotBe(5000); + infoManager.Instance.NonSecurePort.Should().BePositive(); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_explicitly_configured_secure_port() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:SecurePort"] = "443", + ["Eureka:Instance:SecurePortEnabled"] = "true" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.SecurePort.Should().Be(443); + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_explicitly_configured_non_secure_port() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:Port"] = "80", + ["Eureka:Instance:NonSecurePortEnabled"] = "true" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.NonSecurePort.Should().Be(80); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_override_ports_configured_via_code() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + builder.Services.Configure(options => + { + options.SecurePort = 8443; + options.IsSecurePortEnabled = true; + }); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsSecurePortEnabled.Should().BeTrue(); + infoManager.Instance.SecurePort.Should().Be(8443); + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + } + + [Fact] + public async Task Does_not_apply_dynamic_ports_when_UseAspNetCoreUrls_is_false() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Instance:UseAspNetCoreUrls"] = "false" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); + builder.WebHost.UseSetting("urls", "http://*:0"); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + await using WebApplication app = builder.Build(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var infoManager = app.Services.GetRequiredService(); + + infoManager.Instance.IsNonSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.NonSecurePort.Should().Be(0); + infoManager.Instance.IsSecurePortEnabled.Should().BeFalse(); + infoManager.Instance.SecurePort.Should().Be(0); + } } diff --git a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs index 8194b53020..dabd6d4039 100644 --- a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.Configuration; namespace Steeltoe.Discovery.Eureka.Test; @@ -98,14 +97,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - 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); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection clientSection = configuration.GetSection(EurekaClientOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/Eureka.Test/EurekaDiscoveryClientTest.cs b/src/Discovery/test/Eureka.Test/EurekaDiscoveryClientTest.cs index fc45d1dce7..590e979a1c 100644 --- a/src/Discovery/test/Eureka.Test/EurekaDiscoveryClientTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaDiscoveryClientTest.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.Globalization; using System.Net; using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; @@ -88,7 +89,7 @@ public sealed class EurekaDiscoveryClientTest "instance": [ { "instanceId": "localhost:foo", - "hostName": "localhost", + "hostName": "modified-host", "app": "FOO", "ipAddr": "192.168.56.1", "status": "UP", @@ -167,7 +168,10 @@ public async Task Constructor_Initializes_Correctly() thisService.Metadata.Should().BeEmpty(); thisService.Port.Should().Be(5000); thisService.ServiceId.Should().Be("DEMO"); + thisService.InstanceId.Should().Be($"{instanceOptions.HostName}:demo:5000"); thisService.Uri.Should().Be(new Uri($"http://{instanceOptions.HostName}:5000")); + thisService.NonSecureUri.Should().Be(thisService.Uri); + thisService.SecureUri.Should().BeNull(); } [Fact] @@ -414,7 +418,64 @@ public async Task UnRegisterAsync_Succeeds_WhenOKStatusReturned() } [Fact] - public async Task GetInstancesByVipAddress_ReturnsExpected() + public async Task ShutdownAsync_Unregisters_WhenRegistered() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "true", + ["Eureka:Instance:AppName"] = "FOO", + ["Eureka:Instance:InstanceId"] = "localhost:foo" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Post, "http://localhost:8761/eureka/apps/FOO").Respond(HttpStatusCode.OK); + handler.Mock.Expect(HttpMethod.Delete, "http://localhost:8761/eureka/apps/FOO/localhost%3Afoo").Respond(HttpStatusCode.OK); + + await using WebApplication webApplication = builder.Build(); + webApplication.Services.GetRequiredService().Using(handler); + + var discoveryClient = webApplication.Services.GetRequiredService(); + + await discoveryClient.ShutdownAsync(TestContext.Current.CancellationToken); + + handler.Mock.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task ShutdownAsync_DoesNotUnregister_WhenNotRegistered() + { + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "true", + ["Eureka:Instance:AppName"] = "FOO", + ["Eureka:Instance:InstanceId"] = "localhost:foo" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Post, "http://localhost:8761/eureka/apps/FOO").Respond(HttpStatusCode.NotFound); + + await using WebApplication webApplication = builder.Build(); + webApplication.Services.GetRequiredService().Using(handler); + + var discoveryClient = webApplication.Services.GetRequiredService(); + + await discoveryClient.ShutdownAsync(TestContext.Current.CancellationToken); + + handler.Mock.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task GetInstancesAsync_ReturnsExpected() { var appSettings = new Dictionary { @@ -432,13 +493,13 @@ public async Task GetInstancesByVipAddress_ReturnsExpected() discoveryClient.Applications = new ApplicationInfoCollection([ new ApplicationInfo("app1", [ - new InstanceInfo("id1", "app1", "localhost", "192.168.56.1", new DataCenterInfo(), TimeProvider.System) + new InstanceInfo("id11", "app1", "localhost", "192.168.56.1", new DataCenterInfo(), TimeProvider.System) { VipAddress = "vapp1", SecureVipAddress = "svapp1", Status = InstanceStatus.Down }, - new InstanceInfo("id2", "app1", "localhost", "192.168.56.1", new DataCenterInfo(), TimeProvider.System) + new InstanceInfo("id12", "app1", "localhost", "192.168.56.1", new DataCenterInfo(), TimeProvider.System) { VipAddress = "vapp1", SecureVipAddress = "svapp1", @@ -461,18 +522,18 @@ public async Task GetInstancesByVipAddress_ReturnsExpected() ]) ]); - IReadOnlyList result = discoveryClient.GetInstancesByVipAddress("vapp1", false); + IList result = await discoveryClient.GetInstancesAsync("vapp1", TestContext.Current.CancellationToken); result.Should().HaveCount(2); - result.Should().ContainSingle(info => info.InstanceId == "id1"); - result.Should().ContainSingle(info => info.InstanceId == "id2"); + result.Should().ContainSingle(info => info.InstanceId == "id11"); + result.Should().ContainSingle(info => info.InstanceId == "id12"); - result = discoveryClient.GetInstancesByVipAddress("boohoo", false); + result = await discoveryClient.GetInstancesAsync("boohoo", TestContext.Current.CancellationToken); result.Should().BeEmpty(); discoveryClient.Applications.ReturnUpInstancesOnly = true; - result = discoveryClient.GetInstancesByVipAddress("vapp1", false); + result = await discoveryClient.GetInstancesAsync("vapp1", TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -632,7 +693,7 @@ public async Task Can_manipulate_request_headers() } [Fact] - public async Task ApplicationEventsFireOnChangeDuringFetch() + public async Task ApplicationEventsFireAfterFetch() { var appSettings = new Dictionary { @@ -652,15 +713,125 @@ public async Task ApplicationEventsFireOnChangeDuringFetch() webApplication.Services.GetRequiredService().Using(handler); var discoveryClient = webApplication.Services.GetRequiredService(); - int eventCount = 0; + ApplicationsFetchedEventArgs? applicationsEventArgs = null; + int applicationsEventCount = 0; + DiscoveryInstancesFetchedEventArgs? instancesEventArgs = null; + int instancesEventCount = 0; - discoveryClient.ApplicationsFetched += (_, _) => eventCount++; + discoveryClient.ApplicationsFetched += (_, args) => + { + applicationsEventArgs = args; + Interlocked.Increment(ref applicationsEventCount); + }; + + discoveryClient.InstancesFetched += (_, args) => + { + instancesEventArgs = args; + Interlocked.Increment(ref instancesEventCount); + }; await discoveryClient.FetchRegistryAsync(true, TestContext.Current.CancellationToken); - SpinWait.SpinUntil(() => eventCount == 1, 5.Seconds()).Should().BeTrue(); + SpinWait.SpinUntil(() => applicationsEventCount == 1 && instancesEventCount == 1, 5.Seconds()).Should().BeTrue(); + + applicationsEventArgs.Should().NotBeNull(); + InstanceInfo oldInstanceFromAppEvent = applicationsEventArgs.Applications.Should().ContainSingle().Which.Instances.Should().ContainSingle().Which; + oldInstanceFromAppEvent.ActionType.Should().Be(ActionType.Added); + + instancesEventArgs.Should().NotBeNull(); + IServiceInstance oldInstanceFromEvent = instancesEventArgs.InstancesByServiceId.Should().ContainKey("foo").WhoseValue.Should().ContainSingle().Which; + oldInstanceFromEvent.Uri.ToString().Should().Be("http://localhost:8080/"); + + IList oldInstancesFromGet = await discoveryClient.GetInstancesAsync("foo", TestContext.Current.CancellationToken); + oldInstancesFromGet.Should().ContainSingle().Which.Uri.Should().Be(oldInstanceFromEvent.Uri); await discoveryClient.FetchRegistryAsync(false, TestContext.Current.CancellationToken); - SpinWait.SpinUntil(() => eventCount == 2, 5.Seconds()).Should().BeTrue(); + SpinWait.SpinUntil(() => applicationsEventCount == 2 && instancesEventCount == 2, 5.Seconds()).Should().BeTrue(); + + InstanceInfo newInstanceFromAppEvent = applicationsEventArgs.Applications.Should().ContainSingle().Which.Instances.Should().ContainSingle().Which; + newInstanceFromAppEvent.ActionType.Should().Be(ActionType.Modified); + + IServiceInstance newInstanceFromEvent = instancesEventArgs.InstancesByServiceId.Should().ContainKey("foo").WhoseValue.Should().ContainSingle().Which; + newInstanceFromEvent.Uri.ToString().Should().Be("http://modified-host:8080/"); + + IList newInstancesFromGet = await discoveryClient.GetInstancesAsync("foo", TestContext.Current.CancellationToken); + newInstancesFromGet.Should().ContainSingle().Which.Uri.Should().Be(newInstanceFromEvent.Uri); + + handler.Mock.VerifyNoOutstandingExpectation(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InstancesFetched_returns_same_data_as_GetInstancesAsync(bool filterOnlyUpInstances) + { + const string registryJson = """ + { + "applications": { + "application": [ + { + "name": "ignored", + "instance": [ + { + "instanceId": "id1", + "hostName": "h1", + "app": "app1", + "ipAddr": "10.0.0.1", + "status": "UP", + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "vipAddress": "vapp1" + }, + { + "instanceId": "id2", + "hostName": "h2", + "app": "app1", + "ipAddr": "10.0.0.2", + "status": "DOWN", + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "vipAddress": "vapp1" + } + ] + } + ] + } + } + """; + + var appSettings = new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false", + ["Eureka:Client:ShouldFilterOnlyUpInstances"] = filterOnlyUpInstances.ToString(CultureInfo.InvariantCulture) + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddEurekaDiscoveryClient(); + + var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, "http://localhost:8761/eureka/apps").Respond("application/json", registryJson); + + await using WebApplication webApplication = builder.Build(); + webApplication.Services.GetRequiredService().Using(handler); + + var discoveryClient = webApplication.Services.GetRequiredService(); + DiscoveryInstancesFetchedEventArgs? eventArgs = null; + discoveryClient.InstancesFetched += (_, args) => eventArgs = args; + + await discoveryClient.FetchRegistryAsync(true, TestContext.Current.CancellationToken); + SpinWait.SpinUntil(() => eventArgs != null, 5.Seconds()).Should().BeTrue(); + + eventArgs.Should().NotBeNull(); + + IList instancesFromGet = await discoveryClient.GetInstancesAsync("vapp1", TestContext.Current.CancellationToken); + IReadOnlyList instancesFromEvent = eventArgs.InstancesByServiceId.Should().ContainKey("vapp1").WhoseValue; + + instancesFromEvent.Should().BeEquivalentTo(instancesFromGet); handler.Mock.VerifyNoOutstandingExpectation(); } diff --git a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs index d5a1a9ee6f..445262fbb4 100644 --- a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.AppInfo; using Steeltoe.Discovery.Eureka.Configuration; @@ -43,6 +42,7 @@ public void Constructor_Initializes_Defaults() instanceOptions.SecureHealthCheckUrl.Should().BeNull(); instanceOptions.AutoScalingGroupName.Should().BeNull(); instanceOptions.DataCenterInfo.Name.Should().Be(DataCenterName.MyOwn); + instanceOptions.UseAspNetCoreUrls.Should().BeTrue(); instanceOptions.UseNetworkInterfaces.Should().BeFalse(); } @@ -103,14 +103,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - 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); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection instanceSection = configuration.GetSection(EurekaInstanceOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs index d8c1453e14..745c012afb 100644 --- a/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -55,14 +56,13 @@ public async Task AddEurekaDiscoveryClient_UsesManagementOptions() options.Value.StatusPageUrlPath.Should().Be("/actuator/info"); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AddEurekaDiscoveryClient_UsesServerTimeout() { var appSettings = new Dictionary { ["Eureka:Client:EurekaServer:ConnectTimeoutSeconds"] = "1", - ["Eureka:Client:EurekaServer:RetryCount"] = "1" + ["Eureka:Client:EurekaServer:RetryCount"] = "0" }; IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); diff --git a/src/Discovery/test/Eureka.Test/EurekaServiceInstanceTest.cs b/src/Discovery/test/Eureka.Test/EurekaServiceInstanceTest.cs index ce1bb62cd0..78d30feba8 100644 --- a/src/Discovery/test/Eureka.Test/EurekaServiceInstanceTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaServiceInstanceTest.cs @@ -27,11 +27,14 @@ public void InstanceWithBothPorts() var serviceInstance = new EurekaServiceInstance(instance); serviceInstance.ServiceId.Should().Be(instance.AppName); + serviceInstance.InstanceId.Should().Be("id"); serviceInstance.Host.Should().Be(instance.HostName); serviceInstance.Port.Should().Be(instance.SecurePort); serviceInstance.IsSecure.Should().BeTrue(); serviceInstance.Metadata.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); serviceInstance.Uri.Should().Be("https://host:9999/"); + serviceInstance.NonSecureUri.Should().Be("http://host:8888/"); + serviceInstance.SecureUri.Should().Be("https://host:9999/"); } [Fact] @@ -50,6 +53,8 @@ public void InstanceWithSecurePort() serviceInstance.Port.Should().Be(instance.SecurePort); serviceInstance.IsSecure.Should().BeTrue(); serviceInstance.Uri.Should().Be("https://host:9999/"); + serviceInstance.NonSecureUri.Should().BeNull(); + serviceInstance.SecureUri.Should().Be("https://host:9999/"); } [Fact] @@ -68,6 +73,8 @@ public void InstanceWithNonSecurePort() serviceInstance.Port.Should().Be(instance.NonSecurePort); serviceInstance.IsSecure.Should().BeFalse(); serviceInstance.Uri.Should().Be("http://host:8888/"); + serviceInstance.NonSecureUri.Should().Be("http://host:8888/"); + serviceInstance.SecureUri.Should().BeNull(); } [Fact] @@ -86,5 +93,7 @@ public void InstanceWithoutPort() serviceInstance.Port.Should().Be(0); serviceInstance.IsSecure.Should().BeFalse(); serviceInstance.Uri.Should().Be("http://host:0/"); + serviceInstance.NonSecureUri.Should().BeNull(); + serviceInstance.SecureUri.Should().BeNull(); } } diff --git a/src/Discovery/test/Eureka.Test/PostConfigureEurekaInstanceOptionsTest.cs b/src/Discovery/test/Eureka.Test/PostConfigureEurekaInstanceOptionsTest.cs index 9eebea8b55..2b554d1457 100644 --- a/src/Discovery/test/Eureka.Test/PostConfigureEurekaInstanceOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/PostConfigureEurekaInstanceOptionsTest.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Globalization; using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -217,8 +218,7 @@ public async Task Can_use_network_interfaces() instanceOptions.IPAddress.Should().Be("254.254.254.254"); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task Can_use_network_interfaces_without_reverse_DNS_on_IP() { var appSettings = new Dictionary @@ -329,7 +329,7 @@ public async Task Adds_random_number_to_instance_ID_when_ports_are_zero() EurekaInstanceOptions instanceOptions = optionsMonitor.CurrentValue; instanceOptions.InstanceId.Should().NotBeNull(); - string[] parts = instanceOptions.InstanceId!.Split(':'); + string[] parts = instanceOptions.InstanceId.Split(':'); parts.Should().HaveCount(3); int.TryParse(parts[2], CultureInfo.InvariantCulture, out int number).Should().BeTrue(); diff --git a/src/Discovery/test/Eureka.Test/Steeltoe.Discovery.Eureka.Test.csproj b/src/Discovery/test/Eureka.Test/Steeltoe.Discovery.Eureka.Test.csproj index f79a91c068..28d96ac0da 100644 --- a/src/Discovery/test/Eureka.Test/Steeltoe.Discovery.Eureka.Test.csproj +++ b/src/Discovery/test/Eureka.Test/Steeltoe.Discovery.Eureka.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs b/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs index 74d424020c..0b6ece9106 100644 --- a/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs @@ -4,7 +4,7 @@ using System.Net; using System.Text; -using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RichardSzalay.MockHttp; @@ -13,6 +13,8 @@ using Steeltoe.Discovery.Eureka.Configuration; using Steeltoe.Discovery.Eureka.Transport; +// ReSharper disable AccessToDisposedClosure + namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class EurekaClientTest @@ -128,10 +130,39 @@ public sealed class EurekaClientTest } """; + private static readonly string ExpectedJsonRequestBody = """ + { + "instance": { + "instanceId": "some", + "app": "FOOBAR", + "ipAddr": "127.0.0.1", + "port": { + "@enabled": "true", + "$": 8080 + }, + "securePort": { + "@enabled": "false", + "$": 9090 + }, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "localhost", + "overriddenstatus": "UNKNOWN", + "metadata": { + "@class": "java.util.Collections$EmptyMap" + }, + "lastUpdatedTimestamp": "1708427732823", + "lastDirtyTimestamp": "1708427732823" + } + } + """.ReplaceLineEndings(string.Empty).Replace(" ", string.Empty, StringComparison.Ordinal); + [Fact] public async Task RegisterAsync_ThrowsOnUnreachableServer() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -161,15 +192,50 @@ public async Task RegisterAsync_ThrowsOnUnreachableServer() IList logMessages = capturingLoggerProvider.GetAll(); logMessages.Should().BeEquivalentTo( - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"WARN {typeof(EurekaClient).FullName}: Failed to execute HTTP POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' in attempt 1."); + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"WARN {typeof(EurekaClient)}: Failed to execute HTTP POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' in attempt 1."); + } + + [Fact] + public async Task RegisterAsync_ThrowsOnUnreachableAccessTokenServer() + { + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + + var services = new ServiceCollection(); + services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); + services.AddOptions().Configure(options => options.AccessTokenUri = "http://host-that-does-not-exist.net:9999/"); + services.AddSingleton(new TestHttpClientFactory()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(TimeProvider.System); + + await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var client = serviceProvider.GetRequiredService(); + + var instance = new InstanceInfo("some", "FOOBAR", "localhost", "127.0.0.1", new DataCenterInfo(), TimeProvider.System) + { + NonSecurePort = 8080, + IsNonSecurePortEnabled = true, + SecurePort = 9090, + IsSecurePortEnabled = false, + LastUpdatedTimeUtc = new DateTime(638_440_245_328_236_418, DateTimeKind.Utc), + LastDirtyTimeUtc = new DateTime(638_440_245_328_236_418, DateTimeKind.Utc) + }; + + Func asyncAction = async () => await client.RegisterAsync(instance, TestContext.Current.CancellationToken); + + await asyncAction.Should().ThrowExactlyAsync().WithMessage("Failed to execute request on all known Eureka servers."); + + IList logMessages = capturingLoggerProvider.GetAll(); + + logMessages.Should().BeEquivalentTo( + $"WARN {typeof(EurekaClient)}: Failed to fetch access token from 'http://host-that-does-not-exist.net:9999/' in attempt 1."); } [Fact] public async Task RegisterAsync_ThrowsOnErrorResponse() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -208,17 +274,16 @@ public async Task RegisterAsync_ThrowsOnErrorResponse() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"DBUG {typeof(EurekaClient).FullName}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient).FullName}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: Sorry!" + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: 'Sorry!'." ], options => options.WithStrictOrdering()); } [Fact] public async Task RegisterAsync_ThrowsOnRetryLimitReached() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -254,10 +319,9 @@ public async Task RegisterAsync_ThrowsOnRetryLimitReached() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"DBUG {typeof(EurekaClient).FullName}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient).FullName}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: " + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 404 in attempt 1.", + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' failed with status 404: ''." ], options => options.WithStrictOrdering()); } @@ -266,7 +330,7 @@ public async Task RegisterAsync_LogsWarningOnCloudWithLocalhost() { using var scope = new EnvironmentVariableScope("VCAP_APPLICATION", "{}"); - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -293,15 +357,15 @@ public async Task RegisterAsync_LogsWarningOnCloudWithLocalhost() IList logMessages = capturingLoggerProvider.GetAll(); logMessages.Should().Contain( - $"WARN {typeof(EurekaClient).FullName}: Registering with hostname 'localhost' in containerized or cloud environments may not be valid. Please configure Eureka:Instance:HostName with a non-localhost address."); + $"WARN {typeof(EurekaClient)}: Registering with hostname 'localhost' in containerized or cloud environments may not be valid. Please configure Eureka:Instance:HostName with a non-localhost address."); } [Fact] public async Task RegisterAsync_SendsRequestToServer() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); - using JsonDocument requestDocument = JsonDocument.Parse(""" + string jsonRequest = JsonNode.Parse(""" { "instance": { "instanceId": "some", @@ -328,9 +392,7 @@ public async Task RegisterAsync_SendsRequestToServer() "lastDirtyTimestamp": "1708427732823" } } - """); - - string jsonRequest = JsonSerializer.Serialize(requestDocument); + """)!.ToJsonString(); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -364,16 +426,15 @@ public async Task RegisterAsync_SendsRequestToServer() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"DBUG {typeof(EurekaClient).FullName}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 204 in attempt 1." + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://localhost:8761/eureka/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://localhost:8761/eureka/apps/FOOBAR' returned status 204 in attempt 1." ], options => options.WithStrictOrdering()); } [Fact] public async Task RegisterAsync_TriesSecondServerIfFirstOneFails() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -408,13 +469,11 @@ public async Task RegisterAsync_TriesSecondServerIfFirstOneFails() logMessages.Should().BeEquivalentTo( [ - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://server1:8761/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"DBUG {typeof(EurekaClient).FullName}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' returned status 404 in attempt 1.", - $"INFO {typeof(EurekaClient).FullName}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' failed with status 404: ", - $"DBUG {typeof(EurekaClient).FullName}: Sending POST request to 'http://server2:8761/apps/FOOBAR' with body: " + - """{"instance":{"instanceId":"some","app":"FOOBAR","ipAddr":"127.0.0.1","port":{"@enabled":"true","$":8080},"securePort":{"@enabled":"false","$":9090},"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"localhost","overriddenstatus":"UNKNOWN","metadata":{"@class":"java.util.Collections$EmptyMap"},"lastUpdatedTimestamp":"1708427732823","lastDirtyTimestamp":"1708427732823"}}.""", - $"DBUG {typeof(EurekaClient).FullName}: HTTP POST request to 'http://server2:8761/apps/FOOBAR' returned status 204 in attempt 2." + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server1:8761/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' returned status 404 in attempt 1.", + $"INFO {typeof(EurekaClient)}: HTTP POST request to 'http://server1:8761/apps/FOOBAR' failed with status 404: ''.", + $"DBUG {typeof(EurekaClient)}: Sending POST request to 'http://server2:8761/apps/FOOBAR' with body: '{ExpectedJsonRequestBody}'.", + $"DBUG {typeof(EurekaClient)}: HTTP POST request to 'http://server2:8761/apps/FOOBAR' returned status 204 in attempt 2." ], options => options.WithStrictOrdering()); } @@ -606,7 +665,7 @@ public async Task GetApplicationsAsync_ThrowsOnBrokenJsonResponse() { const string jsonResponse = """{"applications": {"""; - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); @@ -630,11 +689,10 @@ public async Task GetApplicationsAsync_ThrowsOnBrokenJsonResponse() IList logMessages = capturingLoggerProvider.GetAll(); - logMessages.Should().BeEquivalentTo( - [ - $"DBUG {typeof(EurekaClient).FullName}: Sending GET request to 'http://localhost:8761/eureka/apps' without request body.", - $"DBUG {typeof(EurekaClient).FullName}: HTTP GET request to 'http://localhost:8761/eureka/apps' returned status 200 in attempt 1.", - $"DBUG {typeof(EurekaClient).FullName}: Failed to deserialize HTTP response from GET 'http://localhost:8761/eureka/apps'." + logMessages.Should().BeEquivalentTo([ + $"DBUG {typeof(EurekaClient)}: Sending GET request to 'http://localhost:8761/eureka/apps' without request body.", + $"DBUG {typeof(EurekaClient)}: HTTP GET request to 'http://localhost:8761/eureka/apps' returned status 200 in attempt 1.", + $"DBUG {typeof(EurekaClient)}: Failed to deserialize HTTP response from GET 'http://localhost:8761/eureka/apps'." ], options => options.WithStrictOrdering()); } @@ -713,7 +771,8 @@ public async Task GetByVipAsync_SendsRequestToServer() [Fact] public async Task Redacts_HTTP_headers() { - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("System.Net.Http.HttpClient", StringComparison.Ordinal)); + using var capturingLoggerProvider = + new CapturingLoggerProvider(category => category.StartsWith("System.Net.Http.HttpClient", StringComparison.Ordinal)); var services = new ServiceCollection(); services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationRootTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationRootTest.cs deleted file mode 100644 index 5df09733ae..0000000000 --- a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationRootTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -// 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.Text.Json; -using Steeltoe.Discovery.Eureka.Transport; - -namespace Steeltoe.Discovery.Eureka.Test.Transport; - -public sealed class JsonApplicationRootTest -{ - [Fact] - public void Deserialize_GoodJson() - { - const string json = """ - { - "application": { - "name": "FOO", - "instance": [ - { - "instanceId": "localhost:foo", - "hostName": "localhost", - "app": "FOO", - "ipAddr": "192.168.56.1", - "status": "UP", - "overriddenStatus": "UNKNOWN", - "port": { - "$": 8080, - "@enabled": "true" - }, - "securePort": { - "$": 443, - "@enabled": "false" - }, - "countryId": 1, - "dataCenterInfo": { - "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - "name": "MyOwn" - }, - "leaseInfo": { - "renewalIntervalInSecs": 30, - "durationInSecs": 90, - "registrationTimestamp": 1458152330783, - "lastRenewalTimestamp": 1458243422342, - "evictionTimestamp": 0, - "serviceUpTimestamp": 1458152330783 - }, - "metadata": { - "@class": "java.util.Collections$EmptyMap" - }, - "homePageUrl": "http://localhost:8080/", - "statusPageUrl": "http://localhost:8080/info", - "healthCheckUrl": "http://localhost:8080/health", - "vipAddress": "foo", - "isCoordinatingDiscoveryServer": "false", - "lastUpdatedTimestamp": "1458152330783", - "lastDirtyTimestamp": "1458152330696", - "actionType": "ADDED" - } - ] - } - } - """; - - var result = JsonSerializer.Deserialize(json); - - result.Should().NotBeNull(); - result.Application.Should().NotBeNull(); - result.Application.Name.Should().Be("FOO"); - result.Application.Instances.Should().ContainSingle(); - } -} diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationTest.cs index 2bebc8ce36..e528bb2e64 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationTest.cs @@ -10,62 +10,48 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonApplicationTest { [Fact] - public void Deserialize_GoodJson() + public void Deserialize_InstanceArray() { const string json = """ { "name": "FOO", "instance": [ { - "instanceId": "localhost:foo", - "hostName": "localhost", - "app": "FOO", - "ipAddr": "192.168.56.1", - "status": "UP", - "overriddenStatus": "UNKNOWN", - "port": { - "$": 8080, - "@enabled": "true" - }, - "securePort": { - "$": 443, - "@enabled": "false" - }, - "countryId": 1, - "dataCenterInfo": { - "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - "name": "MyOwn" - }, - "leaseInfo": { - "renewalIntervalInSecs": 30, - "durationInSecs": 90, - "registrationTimestamp": 1457714988223, - "lastRenewalTimestamp": 1457716158319, - "evictionTimestamp": 0, - "serviceUpTimestamp": 1457714988223 - }, - "metadata": { - "@class": "java.util.Collections$EmptyMap" - }, - "homePageUrl": "http://localhost:8080/", - "statusPageUrl": "http://localhost:8080/info", - "healthCheckUrl": "http://localhost:8080/health", - "vipAddress": "foo", - "isCoordinatingDiscoveryServer": "false", - "lastUpdatedTimestamp": "1457714988223", - "lastDirtyTimestamp": "1457714988172", - "actionType": "ADDED" + "instanceId": "localhost:foo" } ] } """; - var result = JsonSerializer.Deserialize(json); + JsonApplication? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonApplication); result.Should().NotBeNull(); result.Name.Should().Be("FOO"); - result.Instances.Should().ContainSingle(); - // Rest is validated by JsonInstanceInfoTest + JsonInstanceInfo? instance = result.Instances.Should().ContainSingle().Subject; + instance.Should().NotBeNull(); + instance.InstanceId.Should().Be("localhost:foo"); + } + + [Fact] + public void Deserialize_InstanceSingleElement() + { + const string json = """ + { + "name": "FOO", + "instance": { + "instanceId": "localhost:foo" + } + } + """; + + JsonApplication? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonApplication); + + result.Should().NotBeNull(); + result.Name.Should().Be("FOO"); + + JsonInstanceInfo? instance = result.Instances.Should().ContainSingle().Subject; + instance.Should().NotBeNull(); + instance.InstanceId.Should().Be("localhost:foo"); } } diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsRootTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsRootTest.cs index fcbd465e8d..5e3c9a94b1 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsRootTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsRootTest.cs @@ -10,69 +10,17 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonApplicationsRootTest { [Fact] - public void Deserialize_GoodJson() + public void Deserialize() { const string json = """ { - "applications": { - "versions__delta": "1", - "apps__hashcode": "UP_1_", - "application": [ - { - "name": "FOO", - "instance": [ - { - "instanceId": "localhost:foo", - "hostName": "localhost", - "app": "FOO", - "ipAddr": "192.168.56.1", - "status": "UP", - "overriddenStatus": "UNKNOWN", - "port": { - "$": 8080, - "@enabled": "true" - }, - "securePort": { - "$": 443, - "@enabled": "false" - }, - "countryId": 1, - "dataCenterInfo": { - "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - "name": "MyOwn" - }, - "leaseInfo": { - "renewalIntervalInSecs": 30, - "durationInSecs": 90, - "registrationTimestamp": 1457714988223, - "lastRenewalTimestamp": 1457716158319, - "evictionTimestamp": 0, - "serviceUpTimestamp": 1457714988223 - }, - "metadata": { - "@class": "java.util.Collections$EmptyMap" - }, - "homePageUrl": "http://localhost:8080/", - "statusPageUrl": "http://localhost:8080/info", - "healthCheckUrl": "http://localhost:8080/health", - "vipAddress": "foo", - "isCoordinatingDiscoveryServer": "false", - "lastUpdatedTimestamp": "1457714988223", - "lastDirtyTimestamp": "1457714988172", - "actionType": "ADDED" - } - ] - } - ] - } + "applications": {} } """; - var result = JsonSerializer.Deserialize(json); + JsonApplicationsRoot? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonApplicationsRoot); result.Should().NotBeNull(); result.Applications.Should().NotBeNull(); - - // Rest is validated by JsonApplicationsTest } } diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsTest.cs index 60f22290c5..84696bdf91 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonApplicationsTest.cs @@ -10,7 +10,31 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonApplicationsTest { [Fact] - public void Deserialize_GoodJson() + public void Deserialize_ApplicationSingleElement() + { + const string json = """ + { + "versions__delta": "1", + "apps__hashcode": "UP_1_", + "application": { + "name": "FOO" + } + } + """; + + JsonApplications? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonApplications); + + result.Should().NotBeNull(); + result.VersionDelta.Should().Be(1); + result.AppsHashCode.Should().Be("UP_1_"); + + JsonApplication? app = result.Applications.Should().ContainSingle().Subject; + app.Should().NotBeNull(); + app.Name.Should().Be("FOO"); + } + + [Fact] + public void Deserialize_ApplicationArray() { const string json = """ { @@ -18,61 +42,20 @@ public void Deserialize_GoodJson() "apps__hashcode": "UP_1_", "application": [ { - "name": "FOO", - "instance": [ - { - "instanceId": "localhost:foo", - "hostName": "localhost", - "app": "FOO", - "ipAddr": "192.168.56.1", - "status": "UP", - "overriddenStatus": "UNKNOWN", - "port": { - "$": 8080, - "@enabled": "true" - }, - "securePort": { - "$": 443, - "@enabled": "false" - }, - "countryId": 1, - "dataCenterInfo": { - "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - "name": "MyOwn" - }, - "leaseInfo": { - "renewalIntervalInSecs": 30, - "durationInSecs": 90, - "registrationTimestamp": 1457714988223, - "lastRenewalTimestamp": 1457716158319, - "evictionTimestamp": 0, - "serviceUpTimestamp": 1457714988223 - }, - "metadata": { - "@class": "java.util.Collections$EmptyMap" - }, - "homePageUrl": "http://localhost:8080/", - "statusPageUrl": "http://localhost:8080/info", - "healthCheckUrl": "http://localhost:8080/health", - "vipAddress": "foo", - "isCoordinatingDiscoveryServer": "false", - "lastUpdatedTimestamp": "1457714988223", - "lastDirtyTimestamp": "1457714988172", - "actionType": "ADDED" - } - ] + "name": "FOO" } ] } """; - var result = JsonSerializer.Deserialize(json); + JsonApplications? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonApplications); result.Should().NotBeNull(); - result.AppsHashCode.Should().Be("UP_1_"); result.VersionDelta.Should().Be(1); - result.Applications.Should().ContainSingle(); + result.AppsHashCode.Should().Be("UP_1_"); - // Rest is validated by JsonApplicationTest + JsonApplication? app = result.Applications.Should().ContainSingle().Subject; + app.Should().NotBeNull(); + app.Name.Should().Be("FOO"); } } diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoRootTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoRootTest.cs index 6eb99be928..83903f2149 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoRootTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoRootTest.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; -using Steeltoe.Discovery.Eureka.AppInfo; +using Steeltoe.Common.TestResources; using Steeltoe.Discovery.Eureka.Transport; namespace Steeltoe.Discovery.Eureka.Test.Transport; @@ -11,68 +11,34 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonInstanceInfoRootTest { [Fact] - public void Deserialize_GoodJson() + public void Serialize() + { + var root = new JsonInstanceInfoRoot + { + Instance = new JsonInstanceInfo() + }; + + string result = JsonSerializer.Serialize(root, EurekaJsonSerializerContext.Default.JsonInstanceInfoRoot); + + result.Should().BeJson(""" + { + "instance": {} + } + """); + } + + [Fact] + public void Deserialize() { const string json = """ { - "instance": { - "instanceId": "DESKTOP-GNQ5SUT", - "app": "FOOBAR", - "appGroupName": null, - "ipAddr": "192.168.0.147", - "sid": "na", - "port": { - "@enabled": true, - "$": 80 - }, - "securePort": { - "@enabled": false, - "$": 443 - }, - "homePageUrl": "http://DESKTOP-GNQ5SUT:80/", - "statusPageUrl": "http://DESKTOP-GNQ5SUT:80/Status", - "healthCheckUrl": "http://DESKTOP-GNQ5SUT:80/health-check", - "secureHealthCheckUrl": null, - "vipAddress": "DESKTOP-GNQ5SUT:80", - "secureVipAddress": "DESKTOP-GNQ5SUT:443", - "countryId": 1, - "dataCenterInfo": { - "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", - "name": "MyOwn" - }, - "hostName": "DESKTOP-GNQ5SUT", - "status": "UP", - "overriddenStatus": "UNKNOWN", - "leaseInfo": { - "renewalIntervalInSecs": 30, - "durationInSecs": 90, - "registrationTimestamp": 0, - "lastRenewalTimestamp": 0, - "renewalTimestamp": 0, - "evictionTimestamp": 0, - "serviceUpTimestamp": 0 - }, - "isCoordinatingDiscoveryServer": false, - "metadata": { - "@class": "java.util.Collections$EmptyMap", - "metadata": null - }, - "lastUpdatedTimestamp": 1458116137663, - "lastDirtyTimestamp": 1458116137663, - "actionType": "ADDED", - "asgName": null - } + "instance": {} } """; - var result = JsonSerializer.Deserialize(json); + JsonInstanceInfoRoot? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonInstanceInfoRoot); result.Should().NotBeNull(); result.Instance.Should().NotBeNull(); - - // Random check some values - result.Instance.ActionType.Should().Be(ActionType.Added); - result.Instance.HealthCheckUrl.Should().Be("http://DESKTOP-GNQ5SUT:80/health-check"); - result.Instance.AppName.Should().Be("FOOBAR"); } } diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoTest.cs index eebfffe565..7c842dae07 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonInstanceInfoTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; +using Steeltoe.Common.TestResources; using Steeltoe.Discovery.Eureka.AppInfo; using Steeltoe.Discovery.Eureka.Transport; @@ -11,7 +12,105 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonInstanceInfoTest { [Fact] - public void Deserialize_GoodJson() + public void Serialize() + { + var instanceInfo = new JsonInstanceInfo + { + InstanceId = "localhost:foo", + HostName = "localhost", + AppName = "FOO", + IPAddress = "192.168.56.1", + Status = InstanceStatus.Up, + OverriddenStatus = InstanceStatus.OutOfService, + OverriddenStatusLegacy = InstanceStatus.Down, + Port = new JsonPortWrapper + { + Enabled = true, + Port = 8080 + }, + SecurePort = new JsonPortWrapper + { + Enabled = false, + Port = 443 + }, + CountryId = 1, + DataCenterInfo = new JsonDataCenterInfo + { + ClassName = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + Name = "MyOwn" + }, + LeaseInfo = new JsonLeaseInfo + { + RenewalIntervalInSeconds = 30, + DurationInSeconds = 90, + RegistrationTimestamp = 1_457_714_988_223, + LastRenewalTimestamp = 1_457_716_158_319, + EvictionTimestamp = 1_457_715_134_123, + ServiceUpTimestamp = 1_457_714_988_223 + }, + Metadata = new Dictionary + { + ["@class"] = "java.util.Collections$EmptyMap" + }, + HomePageUrl = "http://localhost:8080/", + StatusPageUrl = "http://localhost:8080/info", + HealthCheckUrl = "http://localhost:8080/health", + VipAddress = "foo", + IsCoordinatingDiscoveryServer = false, + LastUpdatedTimestamp = 1_457_714_988_223, + LastDirtyTimestamp = 1_457_714_988_172, + ActionType = ActionType.Added + }; + + string result = JsonSerializer.Serialize(instanceInfo, EurekaJsonSerializerContext.Default.JsonInstanceInfo); + + result.Should().BeJson(""" + { + "instanceId": "localhost:foo", + "app": "FOO", + "ipAddr": "192.168.56.1", + "port": { + "@enabled": "true", + "$": 8080 + }, + "securePort": { + "@enabled": "false", + "$": 443 + }, + "homePageUrl": "http://localhost:8080/", + "statusPageUrl": "http://localhost:8080/info", + "healthCheckUrl": "http://localhost:8080/health", + "vipAddress": "foo", + "countryId": 1, + "dataCenterInfo": { + "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name": "MyOwn" + }, + "hostName": "localhost", + "status": "UP", + "overriddenStatus": "OUT_OF_SERVICE", + "overriddenstatus": "DOWN", + "leaseInfo": { + "renewalIntervalInSecs": 30, + "durationInSecs": 90, + "registrationTimestamp": "1457714988223", + "lastRenewalTimestamp": "1457716158319", + "evictionTimestamp": "1457715134123", + "serviceUpTimestamp": "1457714988223" + }, + "isCoordinatingDiscoveryServer": "false", + "metadata": { + "@class": "java.util.Collections$EmptyMap" + }, + "lastUpdatedTimestamp": "1457714988223", + "lastDirtyTimestamp": "1457714988172", + "actionType": "ADDED" + } + """); + } + + [Fact] + public void Deserialize() { const string json = """ { @@ -40,7 +139,7 @@ public void Deserialize_GoodJson() "durationInSecs": 90, "registrationTimestamp": 1457714988223, "lastRenewalTimestamp": 1457716158319, - "evictionTimestamp": 0, + "evictionTimestamp": 1457715134123, "serviceUpTimestamp": 1457714988223 }, "metadata": { @@ -57,7 +156,7 @@ public void Deserialize_GoodJson() } """; - var result = JsonSerializer.Deserialize(json); + JsonInstanceInfo? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonInstanceInfo); result.Should().NotBeNull(); result.InstanceId.Should().Be("localhost:foo"); @@ -82,7 +181,7 @@ public void Deserialize_GoodJson() result.LeaseInfo.DurationInSeconds.Should().Be(90); result.LeaseInfo.RegistrationTimestamp.Should().Be(1_457_714_988_223); result.LeaseInfo.LastRenewalTimestamp.Should().Be(1_457_716_158_319); - result.LeaseInfo.EvictionTimestamp.Should().Be(0); + result.LeaseInfo.EvictionTimestamp.Should().Be(1_457_715_134_123); result.LeaseInfo.ServiceUpTimestamp.Should().Be(1_457_714_988_223); result.Metadata.Should().ContainSingle(); result.Metadata.Should().ContainKey("@class").WhoseValue.Should().Be("java.util.Collections$EmptyMap"); diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonLeaseTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonLeaseTest.cs index c8366ad635..689312b069 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonLeaseTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonLeaseTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; +using Steeltoe.Common.TestResources; using Steeltoe.Discovery.Eureka.Transport; namespace Steeltoe.Discovery.Eureka.Test.Transport; @@ -10,7 +11,34 @@ namespace Steeltoe.Discovery.Eureka.Test.Transport; public sealed class JsonLeaseTest { [Fact] - public void Deserialize_GoodJson() + public void Serialize() + { + var leaseInfo = new JsonLeaseInfo + { + RenewalIntervalInSeconds = 30, + DurationInSeconds = 90, + RegistrationTimestamp = 1_457_714_988_223, + LastRenewalTimestamp = 1_457_716_158_319, + EvictionTimestamp = 1_457_715_134_123, + ServiceUpTimestamp = 1_457_714_988_223 + }; + + string result = JsonSerializer.Serialize(leaseInfo, EurekaJsonSerializerContext.Default.JsonLeaseInfo); + + result.Should().BeJson(""" + { + "renewalIntervalInSecs": 30, + "durationInSecs": 90, + "registrationTimestamp": "1457714988223", + "lastRenewalTimestamp": "1457716158319", + "evictionTimestamp": "1457715134123", + "serviceUpTimestamp": "1457714988223" + } + """); + } + + [Fact] + public void Deserialize() { const string json = """ { @@ -18,19 +46,19 @@ public void Deserialize_GoodJson() "durationInSecs": 90, "registrationTimestamp": 1457714988223, "lastRenewalTimestamp": 1457716158319, - "evictionTimestamp": 0, + "evictionTimestamp": 1457715134123, "serviceUpTimestamp": 1457714988223 } """; - var leaseInfo = JsonSerializer.Deserialize(json); + JsonLeaseInfo? result = JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonLeaseInfo); - leaseInfo.Should().NotBeNull(); - leaseInfo.RenewalIntervalInSeconds.Should().Be(30); - leaseInfo.DurationInSeconds.Should().Be(90); - leaseInfo.RegistrationTimestamp.Should().Be(1_457_714_988_223); - leaseInfo.LastRenewalTimestamp.Should().Be(1_457_716_158_319); - leaseInfo.EvictionTimestamp.Should().Be(0); - leaseInfo.ServiceUpTimestamp.Should().Be(1_457_714_988_223); + result.Should().NotBeNull(); + result.RenewalIntervalInSeconds.Should().Be(30); + result.DurationInSeconds.Should().Be(90); + result.RegistrationTimestamp.Should().Be(1_457_714_988_223); + result.LastRenewalTimestamp.Should().Be(1_457_716_158_319); + result.EvictionTimestamp.Should().Be(1_457_715_134_123); + result.ServiceUpTimestamp.Should().Be(1_457_714_988_223); } } diff --git a/src/Discovery/test/Eureka.Test/Transport/JsonSerializationTest.cs b/src/Discovery/test/Eureka.Test/Transport/JsonSerializationTest.cs index 34bc67c396..ae7d02c9b8 100644 --- a/src/Discovery/test/Eureka.Test/Transport/JsonSerializationTest.cs +++ b/src/Discovery/test/Eureka.Test/Transport/JsonSerializationTest.cs @@ -22,7 +22,7 @@ public void Deserialize_BadJson_Throws() """; #pragma warning restore JSON001 // Invalid JSON pattern - Action action = () => JsonSerializer.Deserialize(json); + Action action = () => JsonSerializer.Deserialize(json, EurekaJsonSerializerContext.Default.JsonInstanceInfo); action.Should().ThrowExactly(); } diff --git a/src/Discovery/test/HttpClients.Test/DiscoveryWebApplicationBuilderExtensionsTest.cs b/src/Discovery/test/HttpClients.Test/DiscoveryWebApplicationBuilderExtensionsTest.cs index cbbe90e4e0..358958b738 100644 --- a/src/Discovery/test/HttpClients.Test/DiscoveryWebApplicationBuilderExtensionsTest.cs +++ b/src/Discovery/test/HttpClients.Test/DiscoveryWebApplicationBuilderExtensionsTest.cs @@ -2,12 +2,16 @@ // 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 FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Http; +using RichardSzalay.MockHttp; using Steeltoe.Common.Discovery; +using Steeltoe.Common.Http.HttpClientPooling; using Steeltoe.Common.TestResources; using Steeltoe.Discovery.Consul; using Steeltoe.Discovery.Eureka; @@ -58,8 +62,7 @@ public async Task AddConsulDiscoveryClient_WebApplicationBuilder_AddsServiceDisc host.Services.GetServices().OfType().Should().ContainSingle(); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AddEurekaDiscoveryClient_WorksWithGlobalServiceDiscovery() { WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); @@ -77,4 +80,34 @@ public async Task AddEurekaDiscoveryClient_WorksWithGlobalServiceDiscovery() await action.Should().NotThrowAsync(); } + + [Fact] + public async Task AddEurekaDiscoveryClient_WorksWithAspireGlobalServiceDiscovery() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Eureka:Client:ShouldFetchRegistry"] = "false", + ["Eureka:Client:ShouldRegisterWithEureka"] = "false" + }); + + builder.Services.AddEurekaDiscoveryClient(); + builder.Services.AddTransient(); + builder.Services.ConfigureHttpClientDefaults(action => action.AddHttpMessageHandler()); + + var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, "http://localhost:8761/eureka/apps").Respond("application/json", "{}"); + + await using WebApplication host = builder.Build(); + + host.Services.GetRequiredService().Using(handler); + + var discoveryClient = host.Services.GetRequiredService(); + Func action = async () => await discoveryClient.FetchRegistryAsync(true, TestContext.Current.CancellationToken); + + await action.Should().NotThrowAsync(); + + handler.Mock.VerifyNoOutstandingExpectation(); + } } diff --git a/src/Discovery/test/HttpClients.Test/LoadBalancers/RoundRobinLoadBalancerTest.cs b/src/Discovery/test/HttpClients.Test/LoadBalancers/RoundRobinLoadBalancerTest.cs index 0d7a33fbf7..c84ead2d37 100644 --- a/src/Discovery/test/HttpClients.Test/LoadBalancers/RoundRobinLoadBalancerTest.cs +++ b/src/Discovery/test/HttpClients.Test/LoadBalancers/RoundRobinLoadBalancerTest.cs @@ -112,6 +112,67 @@ public async Task ResolveServiceInstanceAsync_FindsService_ReturnsURI() result.Should().Be(new Uri("https://foundit:5555/test/bar/foo?test=1&test2=2")); } + [Fact] + public async Task ResolveServiceInstanceAsync_RemovesDuplicates_CaseInsensitive() + { + var client = new TestDiscoveryClient([ + new TestServiceInstance(new Uri("HTTPS://CASE-HOST:1234/")), + new TestServiceInstance(new Uri("https://case-host:1234/")) + ], "svc"); + + var resolver = new ServiceInstancesResolver([client], NullLogger.Instance); + var loadBalancer = new RoundRobinLoadBalancer(resolver, null, null, NullLogger.Instance); + + Uri first = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + Uri second = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + + first.Should().Be(second); + } + + [Fact] + public async Task ResolveServiceInstanceAsync_RemovesDuplicates_CaseInsensitive_MultipleDiscoveryClients() + { + var targetUri = new Uri("https://merged:1/"); + var clientA = new TestDiscoveryClient([new TestServiceInstance(targetUri)], "svc"); + var clientB = new TestDiscoveryClient([new TestServiceInstance(targetUri)], "svc"); + + var resolver = new ServiceInstancesResolver([ + clientA, + clientB + ], NullLogger.Instance); + + var loadBalancer = new RoundRobinLoadBalancer(resolver, null, null, NullLogger.Instance); + + Uri first = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + Uri second = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + + first.Should().Be(second); + } + + [Fact] + public async Task ResolveServiceInstanceAsync_RemovesDuplicates_Rotates() + { + var shared = new Uri("https://shared:100/"); + var other = new Uri("https://other:100/"); + + var client = new TestDiscoveryClient([ + new TestServiceInstance(shared), + new TestServiceInstance(shared), + new TestServiceInstance(other) + ], "svc"); + + var resolver = new ServiceInstancesResolver([client], NullLogger.Instance); + var loadBalancer = new RoundRobinLoadBalancer(resolver, null, null, NullLogger.Instance); + + Uri first = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + Uri second = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + Uri third = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://svc/api"), TestContext.Current.CancellationToken); + + first.Should().Be(new Uri(shared, "api")); + second.Should().Be(new Uri(other, "api")); + third.Should().Be(first); + } + [Fact] public async Task ResolveServiceInstanceAsync_SkipsOverThrowingDiscoveryClients() { @@ -134,15 +195,28 @@ public async Task ResolveServiceInstanceAsync_SkipsOverThrowingDiscoveryClients( [Fact] public async Task ResolveServiceInstanceAsync_CachesInstances() { - ConfigurationDiscoveryOptions options = CreateTestServiceInstances(); + var options = new ConfigurationDiscoveryOptions + { + Services = + { + new ConfigurationServiceInstance + { + ServiceId = "fruit-service", + Host = "before-reload", + Port = 8000, + IsSecure = true + } + } + }; + TestOptionsMonitor optionsMonitor = TestOptionsMonitor.Create(options); var client = new ConfigurationDiscoveryClient(optionsMonitor); IDistributedCache distributedCache = GetCache(); var resolver = new ServiceInstancesResolver([client], distributedCache, null, NullLogger.Instance); var loadBalancer = new RoundRobinLoadBalancer(resolver, null, null, NullLogger.Instance); - Uri fruitUri = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://fruit-service/api"), TestContext.Current.CancellationToken); - fruitUri.Should().Be("https://fruit-ball:8000/api"); + Uri first = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://fruit-service/api"), TestContext.Current.CancellationToken); + first.Should().Be("https://before-reload:8000/api"); optionsMonitor.Change(new ConfigurationDiscoveryOptions { @@ -151,15 +225,15 @@ public async Task ResolveServiceInstanceAsync_CachesInstances() new ConfigurationServiceInstance { ServiceId = "fruit-service", - Host = "CHANGED", + Host = "after-reload", Port = 8000, IsSecure = true } } }); - fruitUri = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://fruit-service/api"), TestContext.Current.CancellationToken); - fruitUri.Should().Be("https://fruit-ball:8000/api"); + Uri second = await loadBalancer.ResolveServiceInstanceAsync(new Uri("https://fruit-service/api"), TestContext.Current.CancellationToken); + second.Should().Be(first); } private static ConfigurationDiscoveryOptions CreateTestServiceInstances() @@ -241,44 +315,55 @@ private static IDistributedCache GetCache() private sealed class TestServiceInstance(Uri uri) : IServiceInstance { public string ServiceId => throw new NotImplementedException(); + public string InstanceId => throw new NotImplementedException(); public string Host => throw new NotImplementedException(); public int Port => throw new NotImplementedException(); public bool IsSecure => throw new NotImplementedException(); public Uri Uri { get; } = uri; + public Uri NonSecureUri => throw new NotImplementedException(); + public Uri SecureUri => throw new NotImplementedException(); public IReadOnlyDictionary Metadata => throw new NotImplementedException(); } - private sealed class TestDiscoveryClient(IServiceInstance? instance = null) : IDiscoveryClient + private sealed class TestDiscoveryClient(IList instances, string? serviceId = null) : IDiscoveryClient { - private readonly IServiceInstance? _instance = instance; + private readonly IList _instances = instances; + private readonly string? _serviceId = serviceId; public string Description => throw new NotImplementedException(); - public Task> GetServiceIdsAsync(CancellationToken cancellationToken) +#pragma warning disable CS0067 // The event is never used + public event EventHandler? InstancesFetched; +#pragma warning restore CS0067 // The event is never used + + public TestDiscoveryClient() + : this([]) { - throw new NotImplementedException(); } - public Task> GetInstancesAsync(string serviceId, CancellationToken cancellationToken) + public TestDiscoveryClient(IServiceInstance instance, string? serviceId = null) + : this([instance], serviceId) { - IList instances = []; + } - if (_instance != null) - { - instances.Add(_instance); - } + public Task> GetServiceIdsAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - return Task.FromResult(instances); + public Task> GetInstancesAsync(string serviceId, CancellationToken cancellationToken) + { + return Task.FromResult(_serviceId == null || serviceId == _serviceId ? _instances : []); } - public IServiceInstance GetLocalServiceInstance() + public IServiceInstance? GetLocalServiceInstance() { - throw new NotImplementedException(); + return null; } public Task ShutdownAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); + return Task.CompletedTask; } } @@ -286,6 +371,10 @@ private sealed class ThrowingDiscoveryClient : 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 IServiceInstance GetLocalServiceInstance() { throw new InvalidOperationException(); diff --git a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs index 98d8f09d57..fe62959c7c 100644 --- a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs +++ b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs @@ -15,7 +15,6 @@ using Steeltoe.Common.HealthChecks; using Steeltoe.Common.Http.HttpClientPooling; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; @@ -57,14 +56,11 @@ public async Task WithEurekaConfiguration_AddsDiscoveryClient() } """; - 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); IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); @@ -235,7 +231,7 @@ public async Task SingleEurekaVCAP_AddsEurekaDiscoveryClient() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(appSettings); builder.AddCloudFoundry(); - builder.AddCloudFoundryServiceBindings(); + builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); IConfiguration configuration = builder.Build(); IServiceCollection services = new ServiceCollection(); @@ -332,7 +328,7 @@ public async Task MultipleEurekaVCAPs_AddsEurekaDiscoveryClientForFirstEntry() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(appSettings); builder.AddCloudFoundry(); - builder.AddCloudFoundryServiceBindings(); + builder.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); IConfiguration configuration = builder.Build(); IServiceCollection services = new ServiceCollection(); @@ -424,17 +420,20 @@ public void MultipleEurekaVCAPs_LogsWarning() using var appScope = new EnvironmentVariableScope("VCAP_APPLICATION", env1); using var servicesScope = new EnvironmentVariableScope("VCAP_SERVICES", env2); - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); using var loggerFactory = new LoggerFactory([capturingLoggerProvider]); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddCloudFoundryServiceBindings(_ => false, new EnvironmentServiceBindingsReader(), loggerFactory); + + configurationBuilder.AddCloudFoundryServiceBindings(_ => false, new EnvironmentServiceBindingsReader(), CloudFoundryServiceBrokerTypes.Eureka, + loggerFactory); + _ = configurationBuilder.Build(); IList logMessages = capturingLoggerProvider.GetAll(); logMessages.Should().BeEquivalentTo( - $"WARN {typeof(EurekaCloudFoundryPostProcessor).FullName}: Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES."); + $"WARN {typeof(EurekaCloudFoundryPostProcessor)}: Multiple Eureka service bindings found, which is not supported. Using the first binding from VCAP_SERVICES."); } [Fact] @@ -540,7 +539,7 @@ public async Task EurekaWithAccessTokenUri_SendsAuthTokenRequestFirst() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddCloudFoundry(); - builder.Configuration.AddCloudFoundryServiceBindings(); + builder.Configuration.AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Eureka); builder.Configuration.AddInMemoryCollection(appSettings); builder.Services.AddEurekaDiscoveryClient(); @@ -656,14 +655,11 @@ public async Task WithConsulConfiguration_AddsDiscoveryClient() } """; - 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); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); @@ -829,10 +825,12 @@ public async Task WithAppConfiguration_AddsAndWorks() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - IConfiguration configuration = new ConfigurationBuilder().SetBasePath(Path.GetDirectoryName(path)!).AddJsonFile(Path.GetFileName(path)).Build(); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); services.AddOptions(); @@ -873,4 +871,20 @@ public async Task WithMultipleClients_AddsDiscoveryClients() serviceProvider.GetServices().OfType().Should().ContainSingle(); serviceProvider.GetServices().OfType().Should().BeEmpty(); } + + [Fact] + public void WithMultipleClients_DotNotRegisterMultipleTimes() + { + var services = new ServiceCollection(); + services.AddConfigurationDiscoveryClient(); + services.AddConsulDiscoveryClient(); + services.AddEurekaDiscoveryClient(); + int beforeServiceCount = services.Count; + + services.AddConfigurationDiscoveryClient(); + services.AddConsulDiscoveryClient(); + services.AddEurekaDiscoveryClient(); + + services.Count.Should().Be(beforeServiceCount); + } } diff --git a/src/Discovery/test/HttpClients.Test/ResolvingHttpDelegatingHandler.cs b/src/Discovery/test/HttpClients.Test/ResolvingHttpDelegatingHandler.cs new file mode 100644 index 0000000000..98516a5842 --- /dev/null +++ b/src/Discovery/test/HttpClients.Test/ResolvingHttpDelegatingHandler.cs @@ -0,0 +1,16 @@ +// 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. + +#pragma warning disable IDE0130 // Namespace does not match folder structure +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.ServiceDiscovery.Http; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + throw new InvalidOperationException("This handler should have been removed from the HttpClient pipeline."); + } +} diff --git a/src/Discovery/test/HttpClients.Test/Steeltoe.Discovery.HttpClients.Test.csproj b/src/Discovery/test/HttpClients.Test/Steeltoe.Discovery.HttpClients.Test.csproj index 9a42b744de..e9458c6127 100644 --- a/src/Discovery/test/HttpClients.Test/Steeltoe.Discovery.HttpClients.Test.csproj +++ b/src/Discovery/test/HttpClients.Test/Steeltoe.Discovery.HttpClients.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Logging/src/Abstractions/ConfigurationSchema.json b/src/Logging/src/Abstractions/ConfigurationSchema.json new file mode 100644 index 0000000000..4a14939b1c --- /dev/null +++ b/src/Logging/src/Abstractions/ConfigurationSchema.json @@ -0,0 +1,17 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Logging": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Logging.Abstractions": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Logging/src/Abstractions/DynamicLoggerProvider.cs b/src/Logging/src/Abstractions/DynamicLoggerProvider.cs index 5bab89d486..41118d530a 100644 --- a/src/Logging/src/Abstractions/DynamicLoggerProvider.cs +++ b/src/Logging/src/Abstractions/DynamicLoggerProvider.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Logging; /// -/// Provides access to categories and their minimum log levels and enables to decorate log messages. +/// Provides access to categories and their minimum log levels and enables decorating log messages. /// public abstract class DynamicLoggerProvider : IDynamicLoggerProvider { diff --git a/src/Logging/src/Abstractions/Properties/AssemblyInfo.cs b/src/Logging/src/Abstractions/Properties/AssemblyInfo.cs index e74dba04fc..39195e6162 100644 --- a/src/Logging/src/Abstractions/Properties/AssemblyInfo.cs +++ b/src/Logging/src/Abstractions/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.Logging", "Steeltoe.Logging.Abstractions")] [assembly: InternalsVisibleTo("Steeltoe.Logging.DynamicSerilog.Test")] diff --git a/src/Logging/src/Abstractions/PublicAPI.Shipped.txt b/src/Logging/src/Abstractions/PublicAPI.Shipped.txt index 265845c2f6..df3b837d54 100644 --- a/src/Logging/src/Abstractions/PublicAPI.Shipped.txt +++ b/src/Logging/src/Abstractions/PublicAPI.Shipped.txt @@ -34,5 +34,4 @@ Steeltoe.Logging.MessageProcessingLogger.MessageProcessingLogger(Microsoft.Exten Steeltoe.Logging.MessageProcessingLogger.MessageProcessors.get -> System.Collections.Generic.IReadOnlyCollection! virtual Steeltoe.Logging.DynamicLoggerProvider.CreateMessageProcessingLogger(string! categoryName) -> Steeltoe.Logging.MessageProcessingLogger! virtual Steeltoe.Logging.DynamicLoggerProvider.Dispose(bool disposing) -> void -virtual Steeltoe.Logging.LoggerFilter.Invoke(Microsoft.Extensions.Logging.LogLevel level) -> bool virtual Steeltoe.Logging.MessageProcessingLogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func! formatter) -> void diff --git a/src/Logging/src/Abstractions/Steeltoe.Logging.Abstractions.csproj b/src/Logging/src/Abstractions/Steeltoe.Logging.Abstractions.csproj index ac8b3aa41c..df179a4e84 100644 --- a/src/Logging/src/Abstractions/Steeltoe.Logging.Abstractions.csproj +++ b/src/Logging/src/Abstractions/Steeltoe.Logging.Abstractions.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Steeltoe.Logging Abstractions for managing minimum logging levels at runtime. abstractions;logging;dynamic-logging;log-management diff --git a/src/Logging/src/DynamicConsole/Steeltoe.Logging.DynamicConsole.csproj b/src/Logging/src/DynamicConsole/Steeltoe.Logging.DynamicConsole.csproj index 1d56db4af0..fb04b352d7 100644 --- a/src/Logging/src/DynamicConsole/Steeltoe.Logging.DynamicConsole.csproj +++ b/src/Logging/src/DynamicConsole/Steeltoe.Logging.DynamicConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Manage minimum logging levels at runtime using ConsoleLogger. logging;dynamic-logging;console;log-management;monitoring true diff --git a/src/Logging/src/DynamicSerilog/DynamicSerilogLoggerProvider.cs b/src/Logging/src/DynamicSerilog/DynamicSerilogLoggerProvider.cs index 899746dd3d..3751d64dbb 100644 --- a/src/Logging/src/DynamicSerilog/DynamicSerilogLoggerProvider.cs +++ b/src/Logging/src/DynamicSerilog/DynamicSerilogLoggerProvider.cs @@ -7,6 +7,13 @@ using Serilog.Core; using Serilog.Events; using Serilog.Extensions.Logging; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Logging.DynamicSerilog; @@ -15,7 +22,7 @@ namespace Steeltoe.Logging.DynamicSerilog; /// public sealed class DynamicSerilogLoggerProvider : DynamicLoggerProvider { - private static readonly object LoggerLock = new(); + private static readonly LockPrimitive LoggerLock = new(); private static Logger? _serilogLogger; private readonly IDisposable? _optionsChangeListener; diff --git a/src/Logging/src/DynamicSerilog/SerilogLoggingBuilderExtensions.cs b/src/Logging/src/DynamicSerilog/SerilogLoggingBuilderExtensions.cs index 1569425b83..c01090d0ca 100644 --- a/src/Logging/src/DynamicSerilog/SerilogLoggingBuilderExtensions.cs +++ b/src/Logging/src/DynamicSerilog/SerilogLoggingBuilderExtensions.cs @@ -35,7 +35,7 @@ public static ILoggingBuilder AddDynamicSerilog(this ILoggingBuilder builder) /// The to configure. /// /// - /// Enables to configure Serilog from code instead of configuration. + /// Enables configuring Serilog from code instead of configuration. /// /// /// The incoming so that additional calls can be chained. @@ -69,7 +69,7 @@ public static ILoggingBuilder AddDynamicSerilog(this ILoggingBuilder builder, bo /// The to configure. /// /// - /// Enables to configure Serilog from code instead of configuration. + /// Enables configuring Serilog from code instead of configuration. /// /// /// When set to true, does not remove existing logger providers. @@ -104,7 +104,7 @@ public static ILoggingBuilder AddDynamicSerilog(this ILoggingBuilder builder, Lo private static bool IsSerilogDynamicLoggerProviderAlreadyRegistered(ILoggingBuilder builder) { return builder.Services.Any(descriptor => - descriptor.ServiceType == typeof(ILoggerProvider) && descriptor.SafeGetImplementationType() == typeof(DynamicSerilogLoggerProvider)); + descriptor.SafeGetImplementationType() == typeof(DynamicSerilogLoggerProvider) && descriptor.ServiceType == typeof(ILoggerProvider)); } private static void AssertNoDynamicLoggerProviderRegistered(ILoggingBuilder builder) diff --git a/src/Logging/src/DynamicSerilog/SerilogOptions.cs b/src/Logging/src/DynamicSerilog/SerilogOptions.cs index 949e4ff716..871e3a9a63 100644 --- a/src/Logging/src/DynamicSerilog/SerilogOptions.cs +++ b/src/Logging/src/DynamicSerilog/SerilogOptions.cs @@ -23,7 +23,7 @@ public sealed class SerilogOptions public MinimumLevel? MinimumLevel { get; set; } /// - /// Enables to bind from configuration. + /// Enables binding from configuration. /// /// /// The configuration to bind from. @@ -56,7 +56,7 @@ internal void SetSerilogOptions(IConfiguration configuration) } /// - /// Enables to configure programmatically. + /// Enables configuring programmatically. /// /// /// The instance to obtain settings from. diff --git a/src/Logging/src/DynamicSerilog/Steeltoe.Logging.DynamicSerilog.csproj b/src/Logging/src/DynamicSerilog/Steeltoe.Logging.DynamicSerilog.csproj index 791445379b..07804c6a7c 100644 --- a/src/Logging/src/DynamicSerilog/Steeltoe.Logging.DynamicSerilog.csproj +++ b/src/Logging/src/DynamicSerilog/Steeltoe.Logging.DynamicSerilog.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Manage minimum logging levels at runtime using Serilog. logging;dynamic-logging;serilog;log-management;monitoring true diff --git a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs index 5c17d14573..dce86c3f4e 100644 --- a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs +++ b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Logging; using Steeltoe.Common.TestResources; +#pragma warning disable CA1873 // Avoid potentially expensive logging + namespace Steeltoe.Logging.DynamicConsole.Test; public sealed class DynamicConsoleLoggerProviderTest : IDisposable @@ -398,19 +400,17 @@ public async Task AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Warning" + } + } } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); await testContext.Parent.AssertMinLevelAsync(LogLevel.Warning); @@ -424,16 +424,16 @@ public async Task AppliesChangedConfiguration() await testContext.Self.AssertMinLevelAsync(LogLevel.Error, LogLevel.Warning); await testContext.Child.AssertMinLevelAsync(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Trace", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Trace", + "A.B.C": "Debug" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); testContext.Refresh(); @@ -489,7 +489,6 @@ public async Task CanUseJsonFormatterWithScopes() "Category": "Fully.Qualified.Type", "Message": "Processing of { RequestUrl = https://www.example.com, UserAgent = Steeltoe } started.", "State": { - "Message": "Processing of { RequestUrl = https://www.example.com, UserAgent = Steeltoe } started.", "@IncomingRequest": "{ RequestUrl = https://www.example.com, UserAgent = Steeltoe }", "{OriginalFormat}": "Processing of {@IncomingRequest} started." }, @@ -503,7 +502,7 @@ public async Task CanUseJsonFormatterWithScopes() ] } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] diff --git a/src/Logging/test/DynamicConsole.Test/Steeltoe.Logging.DynamicConsole.Test.csproj b/src/Logging/test/DynamicConsole.Test/Steeltoe.Logging.DynamicConsole.Test.csproj index 3a3430b390..ec171ac8c0 100644 --- a/src/Logging/test/DynamicConsole.Test/Steeltoe.Logging.DynamicConsole.Test.csproj +++ b/src/Logging/test/DynamicConsole.Test/Steeltoe.Logging.DynamicConsole.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs index 637c93dffe..41545a488f 100644 --- a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs +++ b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs @@ -8,6 +8,8 @@ using Serilog.Context; using Steeltoe.Common.TestResources; +#pragma warning disable CA1873 // Avoid potentially expensive logging + namespace Steeltoe.Logging.DynamicSerilog.Test; public sealed class DynamicSerilogLoggerProviderTest : IDisposable @@ -196,24 +198,22 @@ public void AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Warning" + } + }, + "WriteTo": { + "Name": "Console" + } } - }, - "WriteTo": { - "Name": "Console" } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); testContext.Parent.AssertMinLevel(LogLevel.Warning); @@ -227,19 +227,19 @@ public void AppliesChangedConfiguration() testContext.Self.AssertMinLevel(LogLevel.Error, LogLevel.Warning); testContext.Child.AssertMinLevel(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Verbose", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Verbose", + "A.B.C": "Debug" + } + }, + "WriteTo": "Console" } - }, - "WriteTo": "Console" - } - } - """); + } + """); fileProvider.NotifyChanged(); testContext.Refresh(); @@ -285,7 +285,7 @@ public void CanUseScopes() [INF] Fully.Qualified.Type: {InnerScopeKey="InnerScopeValue", Scope=["OuterScope", "InnerScope=InnerScopeValue"]} TestInfo - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -329,7 +329,7 @@ public void CanUseSerilogEnrichers() [INF] Fully.Qualified.Type: {A=1} Carries property A = 1, again - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -358,7 +358,7 @@ public void CanUseSerilogDestructuring() logOutput.Should().Be(""" [INF] Fully.Qualified.Type: Processing of {"RequestUrl": "https://www.example.com", "UserAgent": "Steeltoe"} started. - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -389,7 +389,7 @@ public void CallsIntoMessageProcessors() [INF] {SourceContext="Test", Scope=["TwoOne"]} Three - """); + """, IgnoreLineEndingsComparer.Instance); } private static IDynamicLoggerProvider CreateLoggerProvider(Action? configure = null) diff --git a/src/Logging/test/DynamicSerilog.Test/HostBuilderTest.cs b/src/Logging/test/DynamicSerilog.Test/HostBuilderTest.cs index d1804c2e89..151b001ca2 100644 --- a/src/Logging/test/DynamicSerilog.Test/HostBuilderTest.cs +++ b/src/Logging/test/DynamicSerilog.Test/HostBuilderTest.cs @@ -91,13 +91,13 @@ public async Task CanPreserveDefaultConsoleLoggerProvider() logOutput.Should().Contain("SERILOG [INF] TestInfo"); logOutput.Should().Contain("SERILOG [ERR] TestError"); - logOutput.Should().Contain($""" - info: {typeof(HostBuilderTest).FullName}[0] + logOutput.Should().ContainLines($""" + info: {typeof(HostBuilderTest)}[0] TestInfo """); - logOutput.Should().Contain($""" - fail: {typeof(HostBuilderTest).FullName}[0] + logOutput.Should().ContainLines($""" + fail: {typeof(HostBuilderTest)}[0] TestError """); } @@ -133,7 +133,7 @@ public async Task CanConfigureSerilogWithoutLevelsConfiguration() [INF] TestInfo [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -167,7 +167,7 @@ public async Task CanConfigureSerilogFromConfigurationWithDefaultLevel() logOutput.Should().Be(""" [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -201,7 +201,7 @@ public async Task CanConfigureSerilogFromConfigurationWithShortKeyForDefaultLeve logOutput.Should().Be(""" [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -236,7 +236,7 @@ public async Task CanConfigureSerilogFromConfigurationWithOnlyOverrides() logOutput.Should().Be(""" [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -273,7 +273,7 @@ public async Task CanConfigureSerilogFromCodeWithDefaultLevel() logOutput.Should().Be(""" [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -311,7 +311,7 @@ public async Task CanConfigureSerilogFromCodeWithOnlyOverrides() logOutput.Should().Be(""" [ERR] TestError - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -376,7 +376,7 @@ public async Task DynamicLevelChangeDoesNotAffectUsageOfNativeSerilogApi() Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); Log.Logger.Information("TestInfoBefore"); - var loggerProvider = host.Services.GetRequiredService(); + using var loggerProvider = host.Services.GetRequiredService(); loggerProvider.SetLogLevel(string.Empty, LogLevel.Critical); Log.Logger.Information("TestInfoAfter"); diff --git a/src/Logging/test/DynamicSerilog.Test/Steeltoe.Logging.DynamicSerilog.Test.csproj b/src/Logging/test/DynamicSerilog.Test/Steeltoe.Logging.DynamicSerilog.Test.csproj index 9120fbd62a..0620235176 100644 --- a/src/Logging/test/DynamicSerilog.Test/Steeltoe.Logging.DynamicSerilog.Test.csproj +++ b/src/Logging/test/DynamicSerilog.Test/Steeltoe.Logging.DynamicSerilog.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Management/src/Abstractions/Configuration/EndpointOptions.cs b/src/Management/src/Abstractions/Configuration/EndpointOptions.cs index 5a70dd8e64..44908329e4 100644 --- a/src/Management/src/Abstractions/Configuration/EndpointOptions.cs +++ b/src/Management/src/Abstractions/Configuration/EndpointOptions.cs @@ -6,8 +6,6 @@ namespace Steeltoe.Management.Configuration; public abstract class EndpointOptions { - private string? _path; - /// /// Gets or sets a value indicating whether this endpoint is enabled. /// @@ -23,14 +21,14 @@ public abstract class EndpointOptions /// public virtual string? Path { - get => _path ?? Id; - set => _path = value; + get => field ?? Id; + set; } /// /// Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Restricted. /// - public EndpointPermissions RequiredPermissions { get; set; } = EndpointPermissions.Restricted; + public virtual EndpointPermissions RequiredPermissions { get; set; } = EndpointPermissions.Restricted; /// /// Gets the list of HTTP verbs that are allowed for this endpoint. diff --git a/src/Management/src/Abstractions/Configuration/EndpointPermissions.cs b/src/Management/src/Abstractions/Configuration/EndpointPermissions.cs index ce61c2964e..3e46238110 100644 --- a/src/Management/src/Abstractions/Configuration/EndpointPermissions.cs +++ b/src/Management/src/Abstractions/Configuration/EndpointPermissions.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Management.Configuration; public enum EndpointPermissions { /// - /// Indicates no permission constraints. + /// Indicates no permissions. /// None, diff --git a/src/Management/src/Abstractions/ConfigurationSchema.json b/src/Management/src/Abstractions/ConfigurationSchema.json new file mode 100644 index 0000000000..20ca549eec --- /dev/null +++ b/src/Management/src/Abstractions/ConfigurationSchema.json @@ -0,0 +1,17 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Management": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Management.Abstractions": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Management/src/Abstractions/Properties/AssemblyInfo.cs b/src/Management/src/Abstractions/Properties/AssemblyInfo.cs index e92dcc05ab..2aa6de9a8f 100644 --- a/src/Management/src/Abstractions/Properties/AssemblyInfo.cs +++ b/src/Management/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.Management", "Steeltoe.Management.Abstractions")] [assembly: InternalsVisibleTo("Steeltoe.Management.Endpoint")] [assembly: InternalsVisibleTo("Steeltoe.Management.Endpoint.Test")] diff --git a/src/Management/src/Abstractions/PublicAPI.Shipped.txt b/src/Management/src/Abstractions/PublicAPI.Shipped.txt index 733f764436..2d7c294d61 100644 --- a/src/Management/src/Abstractions/PublicAPI.Shipped.txt +++ b/src/Management/src/Abstractions/PublicAPI.Shipped.txt @@ -2,8 +2,6 @@ Steeltoe.Management.Configuration.EndpointOptions Steeltoe.Management.Configuration.EndpointOptions.AllowedVerbs.get -> System.Collections.Generic.IList! Steeltoe.Management.Configuration.EndpointOptions.EndpointOptions() -> void -Steeltoe.Management.Configuration.EndpointOptions.RequiredPermissions.get -> Steeltoe.Management.Configuration.EndpointPermissions -Steeltoe.Management.Configuration.EndpointOptions.RequiredPermissions.set -> void Steeltoe.Management.Configuration.EndpointPermissions Steeltoe.Management.Configuration.EndpointPermissions.Full = 2 -> Steeltoe.Management.Configuration.EndpointPermissions Steeltoe.Management.Configuration.EndpointPermissions.None = 0 -> Steeltoe.Management.Configuration.EndpointPermissions @@ -15,4 +13,6 @@ virtual Steeltoe.Management.Configuration.EndpointOptions.Id.get -> string? virtual Steeltoe.Management.Configuration.EndpointOptions.Id.set -> void virtual Steeltoe.Management.Configuration.EndpointOptions.Path.get -> string? virtual Steeltoe.Management.Configuration.EndpointOptions.Path.set -> void +virtual Steeltoe.Management.Configuration.EndpointOptions.RequiredPermissions.get -> Steeltoe.Management.Configuration.EndpointPermissions +virtual Steeltoe.Management.Configuration.EndpointOptions.RequiredPermissions.set -> void virtual Steeltoe.Management.Configuration.EndpointOptions.RequiresExactMatch() -> bool diff --git a/src/Management/src/Abstractions/Steeltoe.Management.Abstractions.csproj b/src/Management/src/Abstractions/Steeltoe.Management.Abstractions.csproj index 620ad8a35b..8f01a65466 100644 --- a/src/Management/src/Abstractions/Steeltoe.Management.Abstractions.csproj +++ b/src/Management/src/Abstractions/Steeltoe.Management.Abstractions.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Steeltoe.Management Abstractions for application management and monitoring. abstractions;actuator diff --git a/src/Management/src/Endpoint/ActuatorMapper.cs b/src/Management/src/Endpoint/ActuatorMapper.cs index 6f65dabf8c..12dc2242ed 100644 --- a/src/Management/src/Endpoint/ActuatorMapper.cs +++ b/src/Management/src/Endpoint/ActuatorMapper.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Management.Endpoint; -internal abstract class ActuatorMapper +internal abstract partial class ActuatorMapper { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly ILogger _logger; @@ -47,9 +47,8 @@ protected ActuatorMapper(IEnumerable middlewares, IOptionsM { if (managementOptions is { IsCloudFoundryEnabled: true, HasCloudFoundrySecurity: false }) { - _logger.LogWarning( - $"Actuators at the {ConfigureManagementOptions.DefaultCloudFoundryPath} endpoint are disabled because the Cloud Foundry security middleware is not active. " + - $"Call {nameof(EndpointApplicationBuilderExtensions.UseCloudFoundrySecurity)}() from your custom middleware pipeline to enable them."); + LogCloudFoundryActuatorsDisabled(ConfigureManagementOptions.DefaultCloudFoundryPath, + nameof(EndpointApplicationBuilderExtensions.UseCloudFoundrySecurity)); } foreach (IEndpointMiddleware middleware in _middlewares.Where(middleware => middleware is not HypermediaEndpointMiddleware)) @@ -62,7 +61,15 @@ protected ActuatorMapper(IEnumerable middlewares, IOptionsM protected void LogErrorForDuplicateRoute(string routePattern, IEndpointMiddleware existingMiddleware, IEndpointMiddleware duplicateMiddleware) { - _logger.LogError("Skipping over duplicate route '{Route}' from {DuplicateMiddlewareType}, which was already added by {ExistingMiddlewareType}", - routePattern, duplicateMiddleware.GetType(), existingMiddleware.GetType()); + LogSkippingDuplicateRoute(routePattern, duplicateMiddleware.GetType(), existingMiddleware.GetType()); } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Actuators at the {CloudFoundryPath} endpoint are disabled because the Cloud Foundry security middleware is not active. " + + "Call {MethodName}() from your custom middleware pipeline to enable them.")] + private partial void LogCloudFoundryActuatorsDisabled(string cloudFoundryPath, string methodName); + + [LoggerMessage(Level = LogLevel.Error, + Message = "Skipping over duplicate route '{Route}' from {DuplicateMiddlewareType}, which was already added by {ExistingMiddlewareType}.")] + private partial void LogSkippingDuplicateRoute(string route, Type duplicateMiddlewareType, Type existingMiddlewareType); } diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointHandler.cs index 49e9dfda00..cf4b71401a 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointHandler.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 Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common; @@ -19,6 +20,7 @@ internal sealed class CloudFoundryEndpointHandler : ICloudFoundryEndpointHandler { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly IOptionsMonitor _endpointOptionsMonitor; + private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEndpointOptionsMonitorProvider[] _optionsMonitorProviderArray; private readonly ILogger _hypermediaServiceLogger; @@ -26,11 +28,12 @@ internal sealed class CloudFoundryEndpointHandler : ICloudFoundryEndpointHandler public CloudFoundryEndpointHandler(IOptionsMonitor managementOptionsMonitor, IOptionsMonitor endpointOptionsMonitor, IEnumerable endpointOptionsMonitorProviders, - ILoggerFactory loggerFactory) + IHttpContextAccessor httpContextAccessor, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(managementOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders); + ArgumentNullException.ThrowIfNull(httpContextAccessor); ArgumentNullException.ThrowIfNull(loggerFactory); IEndpointOptionsMonitorProvider[] optionsMonitorProviderArray = endpointOptionsMonitorProviders.ToArray(); @@ -38,6 +41,7 @@ public CloudFoundryEndpointHandler(IOptionsMonitor management _managementOptionsMonitor = managementOptionsMonitor; _endpointOptionsMonitor = endpointOptionsMonitor; + _httpContextAccessor = httpContextAccessor; _optionsMonitorProviderArray = optionsMonitorProviderArray; _hypermediaServiceLogger = loggerFactory.CreateLogger(); } @@ -46,8 +50,8 @@ public async Task InvokeAsync(string baseUrl, CancellationToken cancellat { ArgumentException.ThrowIfNullOrWhiteSpace(baseUrl); - var hypermediaService = - new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _optionsMonitorProviderArray, _hypermediaServiceLogger); + var hypermediaService = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _optionsMonitorProviderArray, _httpContextAccessor, + _hypermediaServiceLogger); Links result = hypermediaService.Invoke(new Uri(baseUrl)); return await Task.FromResult(result); diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs index d265774afb..f37fa3df42 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs @@ -28,8 +28,8 @@ internal sealed class CloudFoundryEndpointMiddleware( ? headerScheme.ToString() : httpContext.Request.Scheme; - string uri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}"; - return Task.FromResult(uri); + string requestUri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.Path}"; + return Task.FromResult(requestUri); } protected override async Task InvokeEndpointHandlerAsync(string? uri, CancellationToken cancellationToken) diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryJsonSerializerContext.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryJsonSerializerContext.cs new file mode 100644 index 0000000000..91b49a4f3c --- /dev/null +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryJsonSerializerContext.cs @@ -0,0 +1,12 @@ +// 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.Text.Json.Serialization; + +namespace Steeltoe.Management.Endpoint.Actuators.CloudFoundry; + +[JsonSourceGenerationOptions] +[JsonSerializable(typeof(PermissionsProvider.PermissionsResponse))] +[JsonSerializable(typeof(SecurityResult))] +internal sealed partial class CloudFoundryJsonSerializerContext : JsonSerializerContext; diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs index 629ea0a4a1..9fedf69e56 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundrySecurityMiddleware.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.CloudFoundry; -internal sealed class CloudFoundrySecurityMiddleware +internal sealed partial class CloudFoundrySecurityMiddleware { private const string BearerTokenPrefix = "Bearer "; private readonly IOptionsMonitor _managementOptionsMonitor; @@ -53,7 +53,7 @@ public async Task InvokeAsync(HttpContext context) { ArgumentNullException.ThrowIfNull(context); - _logger.LogDebug("InvokeAsync({RequestPath})", context.Request.Path.Value); + LogEntering(context.Request.Path.Value); ManagementOptions managementOptions = _managementOptionsMonitor.CurrentValue; if (Platform.IsCloudFoundry && managementOptions.IsCloudFoundryEnabled && PermissionsProvider.IsCloudFoundryRequest(context.Request.Path)) @@ -62,8 +62,7 @@ public async Task InvokeAsync(HttpContext context) if (string.IsNullOrEmpty(endpointOptions.ApplicationId)) { - _logger.LogError( - "The Application Id could not be found. Make sure the Cloud Foundry Configuration Provider has been added to the application configuration."); + LogApplicationIdMissing(); await ReturnErrorAsync(context, new SecurityResult(HttpStatusCode.ServiceUnavailable, PermissionsProvider.Messages.ApplicationIdMissing)); return; @@ -161,7 +160,7 @@ internal async Task GetPermissionsAsync(HttpContext context) private async Task ReturnErrorAsync(HttpContext context, SecurityResult error) { - _logger.LogError("Actuator Security Error: {Code} - {Message}", error.Code, error.Message); + LogSecurityError(error.Code, error.Message); context.Response.Headers.Append("Content-Type", "application/json;charset=UTF-8"); // UseStatusCodeFromResponse was added to prevent IIS/HWC from blocking the response body on 500-level errors. @@ -172,6 +171,16 @@ private async Task ReturnErrorAsync(HttpContext context, SecurityResult error) context.Response.StatusCode = (int)error.Code; } - await JsonSerializer.SerializeAsync(context.Response.Body, error, cancellationToken: context.RequestAborted); + await JsonSerializer.SerializeAsync(context.Response.Body, error, CloudFoundryJsonSerializerContext.Default.SecurityResult, context.RequestAborted); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering Cloud Foundry Security middleware at path {RequestPath}.")] + private partial void LogEntering(string? requestPath); + + [LoggerMessage(Level = LogLevel.Error, + Message = "The Application Id could not be found. Make sure the Cloud Foundry Configuration Provider has been added to the application configuration.")] + private partial void LogApplicationIdMissing(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Actuator security error with status {Code}: '{Message}'.")] + private partial void LogSecurityError(HttpStatusCode code, string? message); } diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/EndpointServiceCollectionExtensions.cs index c376f108af..de556c0037 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/EndpointServiceCollectionExtensions.cs @@ -43,6 +43,8 @@ public static IServiceCollection AddCloudFoundryActuator(this IServiceCollection { ArgumentNullException.ThrowIfNull(services); + services.AddHttpContextAccessor(); + services.AddCoreActuatorServices(configureMiddleware); diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs index 7b9d77c571..7e2f51b131 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/PermissionsProvider.cs @@ -4,8 +4,8 @@ using System.Net; using System.Net.Http.Headers; -using System.Security; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -17,9 +17,8 @@ namespace Steeltoe.Management.Endpoint.Actuators.CloudFoundry; -internal sealed class PermissionsProvider +internal sealed partial class PermissionsProvider { - private const string ReadSensitiveDataJsonPropertyName = "read_sensitive_data"; public const string HttpClientName = "CloudFoundrySecurity"; private static readonly TimeSpan GetPermissionsTimeout = TimeSpan.FromMilliseconds(5_000); @@ -52,21 +51,20 @@ public async Task GetPermissionsAsync(string accessToken, Cancel } CloudFoundryEndpointOptions options = _optionsMonitor.CurrentValue; - string checkPermissionsUri = $"{options.Api}/v2/apps/{options.ApplicationId}/permissions"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(checkPermissionsUri, UriKind.RelativeOrAbsolute)); + var checkPermissionsUri = new Uri($"{options.Api}/v2/apps/{options.ApplicationId}/permissions", UriKind.RelativeOrAbsolute); + var request = new HttpRequestMessage(HttpMethod.Get, checkPermissionsUri); var auth = new AuthenticationHeaderValue("bearer", accessToken); request.Headers.Authorization = auth; try { - _logger.LogDebug("GetPermissionsAsync({Uri})", checkPermissionsUri); + LogGetPermissions(checkPermissionsUri); using HttpClient httpClient = CreateHttpClient(); using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (response.StatusCode != HttpStatusCode.OK) { - _logger.LogInformation("Cloud Foundry returned status: {HttpStatus} while obtaining permissions from: {PermissionsUri}", response.StatusCode, - checkPermissionsUri); + LogResponseStatus(checkPermissionsUri, response.StatusCode); if (response.StatusCode is HttpStatusCode.Forbidden) { @@ -78,8 +76,11 @@ public async Task GetPermissionsAsync(string accessToken, Cancel : new SecurityResult(HttpStatusCode.ServiceUnavailable, Messages.CloudFoundryNotReachable); } - EndpointPermissions permissions = await ParsePermissionsResponseAsync(response, cancellationToken); - return new SecurityResult(permissions); + EndpointPermissions? permissions = await ParsePermissionsResponseAsync(response, cancellationToken); + + return permissions != null + ? new SecurityResult(permissions.Value) + : new SecurityResult(HttpStatusCode.BadGateway, Messages.CloudFoundryBrokenResponse); } catch (HttpRequestException exception) { @@ -92,34 +93,40 @@ public async Task GetPermissionsAsync(string accessToken, Cancel } } - public async Task ParsePermissionsResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) + public async Task ParsePermissionsResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(response); - string json = string.Empty; - var permissions = EndpointPermissions.None; - try { - json = await response.Content.ReadAsStringAsync(cancellationToken); - - _logger.LogDebug("GetPermissionsAsync returned json: {Json}", SecurityUtilities.SanitizeInput(json)); + string json = await response.Content.ReadAsStringAsync(cancellationToken); + ExpensiveLogResponseJson(json); - var result = JsonSerializer.Deserialize>(json); + PermissionsResponse? result = JsonSerializer.Deserialize(json, CloudFoundryJsonSerializerContext.Default.PermissionsResponse); - if (result != null && result.TryGetValue(ReadSensitiveDataJsonPropertyName, out JsonElement permissionElement)) + EndpointPermissions permissions = result switch { - bool enabled = JsonSerializer.Deserialize(permissionElement.GetRawText()); - permissions = enabled ? EndpointPermissions.Full : EndpointPermissions.Restricted; - } + { ReadBasicData: true, ReadSensitiveData: true } => EndpointPermissions.Full, + { ReadBasicData: true, ReadSensitiveData: false } => EndpointPermissions.Restricted, + _ => EndpointPermissions.None + }; + + LogPermissions(permissions); + return permissions; } catch (Exception exception) when (!exception.IsCancellation()) { - throw new SecurityException($"Exception extracting permissions from json: {SecurityUtilities.SanitizeInput(json)}", exception); + return null; } + } - _logger.LogDebug("GetPermissionsAsync returning: {Permissions}", permissions); - return permissions; + private void ExpensiveLogResponseJson(string json) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + string input = SecurityUtilities.SanitizeInput(json); + LogResponseJson(input); + } } private HttpClient CreateHttpClient() @@ -129,6 +136,27 @@ private HttpClient CreateHttpClient() return httpClient; } + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching permissions from {PermissionsUri}.")] + private partial void LogGetPermissions(MaskedUri permissionsUri); + + [LoggerMessage(Level = LogLevel.Information, Message = "Cloud Foundry returned status {HttpStatus} while obtaining permissions from {PermissionsUri}.")] + private partial void LogResponseStatus(MaskedUri permissionsUri, HttpStatusCode httpStatus); + + [LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true, Message = "Permissions response returned JSON: {Json}")] + private partial void LogResponseJson(string json); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Resolved permissions to {Permissions}.")] + private partial void LogPermissions(EndpointPermissions permissions); + + internal sealed class PermissionsResponse + { + [JsonPropertyName("read_basic_data")] + public bool ReadBasicData { get; set; } + + [JsonPropertyName("read_sensitive_data")] + public bool ReadSensitiveData { get; set; } + } + internal static class Messages { public const string AccessDenied = "Access denied"; @@ -137,6 +165,7 @@ internal static class Messages public const string CloudFoundryApiMissing = "Cloud controller URL is not available"; public const string CloudFoundryNotReachable = "Cloud controller not reachable"; public const string CloudFoundryTimeout = "Cloud controller request timed out"; + public const string CloudFoundryBrokenResponse = "Failed to parse Cloud controller response"; public const string InvalidToken = "Invalid token"; } } diff --git a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsDescriptor.cs b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsDescriptor.cs index 656246f636..17f78cceb1 100644 --- a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsDescriptor.cs +++ b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsDescriptor.cs @@ -2,10 +2,15 @@ // 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.Text.Json.Serialization; + namespace Steeltoe.Management.Endpoint.Actuators.DbMigrations; public sealed class DbMigrationsDescriptor { + [JsonPropertyName("pendingMigrations")] public IList PendingMigrations { get; } = new List(); + + [JsonPropertyName("appliedMigrations")] public IList AppliedMigrations { get; } = new List(); } diff --git a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs index 073aea1ee1..8352763605 100644 --- a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.DbMigrations; -internal sealed class DbMigrationsEndpointHandler : IDbMigrationsEndpointHandler +internal sealed partial class DbMigrationsEndpointHandler : IDbMigrationsEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IServiceProvider _serviceProvider; @@ -41,7 +41,7 @@ public async Task> InvokeAsync(object if (dbContextType is null) { - _logger.LogError("The Microsoft.EntityFrameworkCore.DbContext type is unavailable. Are you missing a package reference?"); + LogDbContextTypeUnavailable(); } else { @@ -83,7 +83,7 @@ public async Task> InvokeAsync(object } catch (DbException exception) when (exception.Message.Contains("exist", StringComparison.Ordinal)) { - _logger.LogWarning(exception, "Failed to load pending/applied migrations, returning all migrations."); + LogFailedToLoadMigrations(exception); AddRange(descriptor.PendingMigrations, _scanner.GetMigrations(dbContext)); } } @@ -106,4 +106,10 @@ private static void AddRange(IList source, IEnumerable items) } } } + + [LoggerMessage(Level = LogLevel.Error, Message = "The Microsoft.EntityFrameworkCore.DbContext type is unavailable. Are you missing a package reference?")] + private partial void LogDbContextTypeUnavailable(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to load pending/applied migrations, returning all migrations.")] + private partial void LogFailedToLoadMigrations(Exception exception); } diff --git a/src/Management/src/Endpoint/Actuators/Environment/ConfigureEnvironmentEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/Environment/ConfigureEnvironmentEndpointOptions.cs index e7efadfe24..dc3a4908e3 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/ConfigureEnvironmentEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/ConfigureEnvironmentEndpointOptions.cs @@ -17,7 +17,8 @@ internal sealed class ConfigureEnvironmentEndpointOptions(IConfiguration configu "key", "token", ".*credentials.*", - "vcap_services" + "vcap_services", + ".*connectionstring.*" ]; public override void Configure(EnvironmentEndpointOptions options) diff --git a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs index 7a72857137..5dfca27f23 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Environment; -internal sealed class EnvironmentEndpointHandler : IEnvironmentEndpointHandler +internal sealed partial class EnvironmentEndpointHandler : IEnvironmentEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IConfiguration _configuration; @@ -36,7 +36,7 @@ public EnvironmentEndpointHandler(IOptionsMonitor op public Task InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Fetching property sources."); + LogFetchingPropertySources(); List activeProfiles = [_environment.EnvironmentName]; IList propertySources = GetPropertySources(); @@ -114,4 +114,7 @@ private static HashSet GetFullKeyNames(IConfigurationProvider provider, return initialKeys; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Fetching property sources.")] + private partial void LogFetchingPropertySources(); } diff --git a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointOptions.cs index 6c1b289294..6e3b82d780 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointOptions.cs @@ -10,11 +10,16 @@ public sealed class EnvironmentEndpointOptions : EndpointOptions { private Sanitizer? _sanitizer; + /// + /// Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full. + /// + public override EndpointPermissions RequiredPermissions { get; set; } = EndpointPermissions.Full; + /// /// Gets the list of keys to sanitize. A key can be a simple string that the property must end with, or a regular expression. A case-insensitive match is /// always performed. Use a single-element empty string to disable sanitization. Default value: /// /// public IList KeysToSanitize { get; } = new List(); diff --git a/src/Management/src/Endpoint/Actuators/Environment/Sanitizer.cs b/src/Management/src/Endpoint/Actuators/Environment/Sanitizer.cs index 312b61c96d..33426d3adf 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/Sanitizer.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/Sanitizer.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Environment; -internal sealed class Sanitizer +internal sealed partial class Sanitizer { private readonly char[] _regexCharacters = [ @@ -31,14 +31,31 @@ public Sanitizer(ICollection keysToSanitize) } } + [GeneratedRegex("://([^:]*?):[^@]+?@", RegexOptions.None, 1000)] + private static partial Regex UriUserInfoRegex(); + + [GeneratedRegex(@"(?^|;)(?\s*)(?password|pwd)(?\s*=\s*)(?[^;]+)", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, 1000)] + private static partial Regex PasswordPairRegex(); + public string? Sanitize(string key, string? value) { - if (value != null && _matchers.Exists(regex => regex.IsMatch(key))) + ArgumentNullException.ThrowIfNull(key); + + if (_matchers.Exists(regex => regex.IsMatch(key))) { return "******"; } - return value; + if (value == null) + { + return null; + } + + string maskedValue = PasswordPairRegex().Replace(value, "${leading}${whitespaceBeforeKey}${key}${equals}******"); + maskedValue = UriUserInfoRegex().Replace(maskedValue, "://$1:******@"); + + return maskedValue; } private bool IsRegex(string value) diff --git a/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs b/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs index 63c1cee1c4..eb48e6fcc0 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Availability/ApplicationAvailability.cs @@ -6,7 +6,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health.Availability; -public sealed class ApplicationAvailability +public sealed partial class ApplicationAvailability { public const string LivenessKey = "Liveness"; public const string ReadinessKey = "Readiness"; @@ -58,7 +58,7 @@ public void SetAvailabilityState(string availabilityType, AvailabilityState newS throw new InvalidOperationException($"{availabilityType} state can only be of type {availabilityType}State"); } - _logger.LogTrace("{StateKey} availability has been set to {NewState} by {Caller}", availabilityType, newState, caller ?? "unspecified"); + LogAvailabilityStateChanged(availabilityType, newState, caller ?? "unspecified"); _availabilityStates[availabilityType] = newState; if (availabilityType == LivenessKey) @@ -71,4 +71,7 @@ public void SetAvailabilityState(string availabilityType, AvailabilityState newS ReadinessChanged?.Invoke(this, new AvailabilityEventArgs(newState)); } } + + [LoggerMessage(Level = LogLevel.Trace, Message = "{StateKey} availability has been set to {NewState} by {Caller}.")] + private partial void LogAvailabilityStateChanged(string stateKey, AvailabilityState newState, string caller); } diff --git a/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs b/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs index 6552652017..ec7e703b6b 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Contributors/AvailabilityStateHealthContributor.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health.Contributors; -internal abstract class AvailabilityStateHealthContributor : IHealthContributor +internal abstract partial class AvailabilityStateHealthContributor : IHealthContributor { private readonly IDictionary _stateMappings; private readonly ILogger _logger; @@ -39,18 +39,18 @@ private HealthCheckResult Health() if (currentHealth == null) { - _logger.LogError("Failed to get current availability state"); + LogFailedToGetState(); health.Description = "Failed to get current availability state"; } else { - try + if (_stateMappings.TryGetValue(currentHealth, out HealthStatus status)) { - health.Status = _stateMappings[currentHealth]; + health.Status = status; } - catch (Exception exception) + else { - _logger.LogError(exception, "Failed to map current availability state"); + LogFailedToMapState(currentHealth); health.Description = "Failed to map current availability state"; } } @@ -59,4 +59,10 @@ private HealthCheckResult Health() } protected abstract AvailabilityState? GetState(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get current availability state.")] + private partial void LogFailedToGetState(); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to map availability state {State}.")] + private partial void LogFailedToMapState(AvailabilityState state); } diff --git a/src/Management/src/Endpoint/Actuators/Health/Contributors/FileSystem/NetworkShareWrapper.cs b/src/Management/src/Endpoint/Actuators/Health/Contributors/FileSystem/NetworkShareWrapper.cs index a62227d27d..c39452957d 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Contributors/FileSystem/NetworkShareWrapper.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Contributors/FileSystem/NetworkShareWrapper.cs @@ -2,7 +2,6 @@ // 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 System.Runtime.InteropServices; namespace Steeltoe.Management.Endpoint.Actuators.Health.Contributors.FileSystem; @@ -27,7 +26,6 @@ private NetworkShareWrapper(ulong freeBytesAvailable, ulong totalNumberOfBytes) : null; } - [ExcludeFromCodeCoverage(Justification = "Workaround for https://github.com/coverlet-coverage/coverlet/issues/1762")] private static partial class NativeMethods { [LibraryImport("kernel32.dll", EntryPoint = "GetDiskFreeSpaceExW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] diff --git a/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs index 1226c49cc4..02019af59d 100644 --- a/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs @@ -76,7 +76,7 @@ private static void RegisterDefaultHealthContributors(IServiceCollection service } /// - /// Adds the specified to the D/I container as a scoped service. + /// Adds the specified to the D/I container as a singleton service. /// /// /// The type of health contributor to add. @@ -98,7 +98,7 @@ public static IServiceCollection AddHealthContributor(this IServiceCollection } /// - /// Adds the specified to the D/I container as a scoped service. + /// Adds the specified to the D/I container as a singleton service. /// /// /// The to add services to. diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs index e7b3cf8770..426c39ade0 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointHandler.cs @@ -12,7 +12,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health; -internal sealed class HealthEndpointHandler : IHealthEndpointHandler +internal sealed partial class HealthEndpointHandler : IHealthEndpointHandler { private readonly IOptionsMonitor _endpointOptionsMonitor; private readonly IHealthAggregator _healthAggregator; @@ -115,8 +115,7 @@ private void CleanResponse(HealthEndpointResponse response, HealthEndpointOption if (ShouldClear(showComponents, healthRequest)) { - _logger.LogTrace("Clearing health check components. ShowComponents={ShowComponents}, HasClaim={HasClaimForHealth}.", showComponents, - healthRequest.HasClaim); + LogClearingComponents(showComponents, healthRequest.HasClaim); response.Components.Clear(); } @@ -126,8 +125,7 @@ private void CleanResponse(HealthEndpointResponse response, HealthEndpointOption if (ShouldClear(showDetails, healthRequest)) { - _logger.LogTrace("Clearing health check component details. ShowDetails={ShowDetails}, HasClaim={HasClaimForHealth}.", showDetails, - healthRequest.HasClaim); + LogClearingDetails(showDetails, healthRequest.HasClaim); foreach (HealthCheckResult component in response.Components.Values) { @@ -141,4 +139,12 @@ private static bool ShouldClear(ShowValues showValues, HealthEndpointRequest hea { return showValues == ShowValues.Never || (showValues == ShowValues.WhenAuthorized && !healthRequest.HasClaim); } + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Clearing health check components because ShowComponents is {ShowComponents} and HasClaim is {HasClaimForHealth}.")] + private partial void LogClearingComponents(ShowValues showComponents, bool hasClaimForHealth); + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Clearing health check component details because ShowDetails is {ShowDetails} and HasClaim is {HasClaimForHealth}.")] + private partial void LogClearingDetails(ShowValues showDetails, bool hasClaimForHealth); } diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs index 657e861364..7737c50b1a 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Health; -internal sealed class HealthEndpointMiddleware : EndpointMiddleware +internal sealed partial class HealthEndpointMiddleware : EndpointMiddleware { private readonly IOptionsMonitor _endpointOptionsMonitor; private readonly ILogger _logger; @@ -59,11 +59,11 @@ private string GetRequestedHealthGroup(PathString requestPath, HealthEndpointOpt if (requestComponents.Length > 0 && requestComponents[^1] != endpointOptions.Id) { - _logger.LogTrace("Found group '{HealthGroup}' in the request path.", requestComponents[^1]); + LogGroupFound(requestComponents[^1]); return requestComponents[^1]; } - _logger.LogTrace("Did not find a health group in the request path."); + LogNoGroupFound(); return string.Empty; } @@ -127,4 +127,10 @@ private static int GetStatusCode(HealthEndpointResponse response) { return response.Status is HealthStatus.Down or HealthStatus.OutOfService ? 503 : 200; } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Found group '{HealthGroup}' in the request path.")] + private partial void LogGroupFound(string healthGroup); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Did not find a health group in the request path.")] + private partial void LogNoGroupFound(); } diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointRequest.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointRequest.cs index fd1fe47c94..dc593685fd 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointRequest.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointRequest.cs @@ -2,11 +2,16 @@ // 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.Text.Json.Serialization; + namespace Steeltoe.Management.Endpoint.Actuators.Health; public sealed class HealthEndpointRequest { + [JsonPropertyName("groupName")] public string GroupName { get; } + + [JsonPropertyName("hasClaim")] public bool HasClaim { get; } public HealthEndpointRequest(string groupName, bool hasClaim) diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointResponse.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointResponse.cs index 231d84fc53..09e734c9c5 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointResponse.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointResponse.cs @@ -14,17 +14,20 @@ public sealed class HealthEndpointResponse /// /// Gets the status of the health check. /// + [JsonPropertyName("status")] [JsonConverter(typeof(SnakeCaseAllCapsEnumMemberJsonConverter))] public HealthStatus Status { get; init; } /// /// Gets a description of the health check result. /// + [JsonPropertyName("description")] public string? Description { get; init; } /// /// Gets the individual health check components, including their details. /// + [JsonPropertyName("components")] [JsonIgnoreEmptyCollection] public IDictionary Components { get; } = new Dictionary(); @@ -32,6 +35,7 @@ public sealed class HealthEndpointResponse /// Gets the list of available health groups. /// [JsonIgnoreEmptyCollection] + [JsonPropertyName("groups")] public IList Groups { get; } = new List(); /// diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs index d6e8d19cad..1c8b1a70e8 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointHandler.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumpEndpointHandler : IHeapDumpEndpointHandler +internal sealed partial class HeapDumpEndpointHandler : IHeapDumpEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IHeapDumper _heapDumper; @@ -29,8 +29,11 @@ public HeapDumpEndpointHandler(IOptionsMonitor optionsM public Task InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Invoking the heap dumper"); + LogInvokingHeapDumper(); string filePath = _heapDumper.DumpHeapToFile(cancellationToken); return Task.FromResult(filePath); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Invoking the heap dumper.")] + private partial void LogInvokingHeapDumper(); } diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs index c03d1b552e..9fe78539db 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumpEndpointMiddleware( +internal sealed partial class HeapDumpEndpointMiddleware( IHeapDumpEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { @@ -28,7 +28,7 @@ protected override async Task WriteResponseAsync(string? fileName, HttpContext h { ArgumentNullException.ThrowIfNull(httpContext); - _logger.LogDebug("Returning: {FileName}", fileName); + LogReturning(fileName); if (!File.Exists(fileName)) { @@ -51,4 +51,7 @@ protected override async Task WriteResponseAsync(string? fileName, HttpContext h File.Delete(fileName); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning heap dump file '{FileName}'.")] + private partial void LogReturning(string? fileName); } diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointOptions.cs index e31a89d13f..ce3eb07194 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointOptions.cs @@ -8,6 +8,11 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; public sealed class HeapDumpEndpointOptions : EndpointOptions { + /// + /// Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full. + /// + public override EndpointPermissions RequiredPermissions { get; set; } = EndpointPermissions.Full; + /// /// Gets or sets the type of dump to create. Default value: Full (on macOS: GCDump). /// diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs index 06bbd0c286..6e21c41e2d 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumper.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HeapDump; -internal sealed class HeapDumper : IHeapDumper +internal sealed partial class HeapDumper : IHeapDumper { private readonly IOptionsMonitor _optionsMonitor; private readonly TimeProvider _timeProvider; @@ -43,7 +43,7 @@ public string DumpHeapToFile(CancellationToken cancellationToken) _ => "full dump" }; - _logger.LogInformation("Attempting to create a {DumpType}.", dumpDescription); + LogStart(dumpDescription); try { @@ -64,7 +64,7 @@ public string DumpHeapToFile(CancellationToken cancellationToken) throw; } - _logger.LogInformation("Successfully created a {DumpType}.", dumpDescription); + LogSucceeded(dumpDescription); return outputPath; } @@ -159,7 +159,7 @@ internal void CaptureLogOutput(Func action, string dumpDescrip throw new InvalidOperationException($"Failed to create a {dumpDescription}. Captured log:{System.Environment.NewLine}{logOutput}", error); } - _logger.LogTrace("Captured log from {DumpType}:{LineBreak}{DumpLog}", dumpDescription, System.Environment.NewLine, logOutput); + LogDumpLogCaptured(dumpDescription, System.Environment.NewLine, logOutput); } private static void SafeDelete(string? outputPath) @@ -179,4 +179,13 @@ private static void SafeDelete(string? outputPath) } } } + + [LoggerMessage(Level = LogLevel.Information, Message = "Attempting to create a {DumpType}.")] + private partial void LogStart(string dumpType); + + [LoggerMessage(Level = LogLevel.Information, Message = "Successfully created a {DumpType}.")] + private partial void LogSucceeded(string dumpType); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Captured log from {DumpType}:{LineBreak}{DumpLog}")] + private partial void LogDumpLogCaptured(string dumpType, string lineBreak, string dumpLog); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs index e6cdf32e39..5a1ff13705 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticObserver.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal abstract class DiagnosticObserver : IObserver>, IDisposable +internal abstract partial class DiagnosticObserver : IObserver>, IDisposable { private readonly string _observerName; private readonly string _listenerName; @@ -38,7 +38,7 @@ protected virtual void Dispose(bool disposing) _subscription?.Dispose(); _subscription = null; - _logger.LogTrace("DiagnosticObserver {Observer} disposed", _observerName); + LogObserverDisposed(_observerName); } } @@ -54,7 +54,7 @@ public void Subscribe(DiagnosticListener listener) } _subscription = listener.Subscribe(this); - _logger.LogTrace("DiagnosticObserver {Observer} subscribed to {Listener}", _observerName, listener.Name); + LogObserverSubscribed(_observerName, listener.Name); } } @@ -76,9 +76,18 @@ public virtual void OnNext(KeyValuePair value) } catch (Exception exception) { - _logger.LogError(exception, "Failed to process event {Id}", value.Key); + LogFailedToProcessEvent(exception, value.Key); } } public abstract void ProcessEvent(string eventName, object? value); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Diagnostic observer {Observer} disposed.")] + private partial void LogObserverDisposed(string observer); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Diagnostic observer {Observer} subscribed to {Listener}.")] + private partial void LogObserverSubscribed(string observer, string listener); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to process event {Id}.")] + private partial void LogFailedToProcessEvent(Exception exception, string id); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs index 7be3cc71a7..1b92b98b3c 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsManager.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal sealed class DiagnosticsManager : IObserver, IDisposable +internal sealed partial class DiagnosticsManager : IObserver, IDisposable { private readonly ILogger _logger; private readonly List _observers; @@ -57,7 +57,7 @@ public void Start() if (_listenersSubscription != null) { - _logger.LogTrace("Subscribed to Diagnostic Listener"); + LogSubscribed(); } } } @@ -82,4 +82,7 @@ public void Dispose() _isDisposed = true; } } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Subscribed to diagnostic listener.")] + private partial void LogSubscribed(); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs index 275d92dc4d..1e834e8bfa 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/Diagnostics/DiagnosticsService.cs @@ -7,7 +7,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges.Diagnostics; -internal sealed class DiagnosticsService : IHostedService +internal sealed partial class DiagnosticsService : IHostedService { private readonly ILogger _logger; private readonly DiagnosticsManager _observerManager; @@ -23,15 +23,21 @@ public DiagnosticsService(DiagnosticsManager observerManager, ILogger TimeTaken != null ? XmlConvert.ToString(TimeTaken.Value) : null; + [JsonPropertyName("timestamp")] public DateTime Timestamp { get; } + + [JsonPropertyName("principal")] public HttpExchangePrincipal? Principal { get; } + + [JsonPropertyName("session")] public HttpExchangeSession? Session { get; } + + [JsonPropertyName("request")] public HttpExchangeRequest Request { get; } + + [JsonPropertyName("response")] public HttpExchangeResponse Response { get; } [JsonIgnore] diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs index 2e7f976c12..f8e870963a 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangePrincipal.cs @@ -2,10 +2,13 @@ // 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.Text.Json.Serialization; + namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchangePrincipal { + [JsonPropertyName("name")] public string Name { get; } public HttpExchangePrincipal(string name) diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs index ddce7c1e83..6d26d346ad 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeRequest.cs @@ -2,19 +2,36 @@ // 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.Text.Json.Serialization; using Microsoft.Extensions.Primitives; +using Steeltoe.Common.Extensions; using Steeltoe.Common.Json; namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchangeRequest { + [JsonPropertyName("method")] public string Method { get; } + + [JsonIgnore] public Uri Uri { get; } + [JsonPropertyName("uri")] + public string JsonUri + { + get + { + MaskedUri masked = Uri; + return masked.ToString(); + } + } + + [JsonPropertyName("headers")] [JsonIgnoreEmptyCollection] public IDictionary Headers { get; } + [JsonPropertyName("remoteAddress")] public string? RemoteAddress { get; } public HttpExchangeRequest(string method, Uri uri, IDictionary headers, string? remoteAddress) diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs index 63e6338a98..4ecf185834 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeResponse.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.Text.Json.Serialization; using Microsoft.Extensions.Primitives; using Steeltoe.Common.Json; @@ -9,8 +10,10 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchangeResponse { + [JsonPropertyName("status")] public int Status { get; } + [JsonPropertyName("headers")] [JsonIgnoreEmptyCollection] public IDictionary Headers { get; } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs index 33ed0af39c..b2ad567174 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangeSession.cs @@ -2,10 +2,13 @@ // 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.Text.Json.Serialization; + namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchangeSession { + [JsonPropertyName("id")] public string Id { get; } public HttpExchangeSession(string id) diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs index 1d5a423b36..3abe12cfd0 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointHandler.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -internal sealed class HttpExchangesEndpointHandler : IHttpExchangesEndpointHandler +internal sealed partial class HttpExchangesEndpointHandler : IHttpExchangesEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly HttpExchangesRepository _httpExchangesRepository; @@ -30,8 +30,11 @@ public HttpExchangesEndpointHandler(IOptionsMonitor InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Fetching Http Exchanges"); + LogFetchingHttpExchanges(); HttpExchangesResult result = _httpExchangesRepository.GetHttpExchanges(); return Task.FromResult(result); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Fetching HTTP exchanges.")] + private partial void LogFetchingHttpExchanges(); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs index 994ab4272e..5c0afd2466 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesRepository.cs @@ -6,10 +6,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Steeltoe.Common.Extensions; namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; -internal sealed class HttpExchangesRepository +internal sealed partial class HttpExchangesRepository { private const string RedactedText = "******"; @@ -34,7 +35,7 @@ private void OnRecord(HttpExchange exchange) { ArgumentNullException.ThrowIfNull(exchange); - _logger.LogDebug("Incoming exchange for {Url}.", exchange.Request.Uri); + LogIncomingExchange(exchange.Request.Uri); _queue.Enqueue(exchange); if (_queue.Count > _optionsMonitor.CurrentValue.Capacity) @@ -104,4 +105,7 @@ private static bool HeaderShouldBeRedacted(string currentHeader, HashSet { return !options.IncludeTimeTaken ? null : timeTaken; } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Incoming exchange for {Url}.")] + private partial void LogIncomingExchange(MaskedUri url); } diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs index d687f08313..32bc9a2505 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesResult.cs @@ -2,12 +2,14 @@ // 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.Text.Json.Serialization; using Steeltoe.Common; namespace Steeltoe.Management.Endpoint.Actuators.HttpExchanges; public sealed class HttpExchangesResult { + [JsonPropertyName("exchanges")] public IList Exchanges { get; } public HttpExchangesResult(IList exchanges) diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/EndpointServiceCollectionExtensions.cs index 82e10e1898..6bc092f804 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/EndpointServiceCollectionExtensions.cs @@ -39,6 +39,8 @@ public static IServiceCollection AddHypermediaActuator(this IServiceCollection s { ArgumentNullException.ThrowIfNull(services); + services.AddHttpContextAccessor(); + services.AddCoreActuatorServices(configureMiddleware); diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointHandler.cs index 9071e84130..9460d9363e 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointHandler.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 Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common; @@ -17,6 +18,7 @@ internal sealed class HypermediaEndpointHandler : IHypermediaEndpointHandler { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly IOptionsMonitor _endpointOptionsMonitor; + private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEndpointOptionsMonitorProvider[] _endpointOptionsMonitorProviderArray; private readonly ILogger _hypermediaServiceLogger; @@ -24,11 +26,12 @@ internal sealed class HypermediaEndpointHandler : IHypermediaEndpointHandler public HypermediaEndpointHandler(IOptionsMonitor managementOptionsMonitor, IOptionsMonitor endpointOptionsMonitor, IEnumerable endpointOptionsMonitorProviders, - ILoggerFactory loggerFactory) + IHttpContextAccessor httpContextAccessor, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(managementOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders); + ArgumentNullException.ThrowIfNull(httpContextAccessor); ArgumentNullException.ThrowIfNull(loggerFactory); IEndpointOptionsMonitorProvider[] endpointOptionsMonitorProviderArray = endpointOptionsMonitorProviders.ToArray(); @@ -36,6 +39,7 @@ public HypermediaEndpointHandler(IOptionsMonitor managementOp _managementOptionsMonitor = managementOptionsMonitor; _endpointOptionsMonitor = endpointOptionsMonitor; + _httpContextAccessor = httpContextAccessor; _endpointOptionsMonitorProviderArray = endpointOptionsMonitorProviderArray; _hypermediaServiceLogger = loggerFactory.CreateLogger(); } @@ -44,7 +48,9 @@ public Task InvokeAsync(string baseUrl, CancellationToken cancellationTok { ArgumentException.ThrowIfNullOrWhiteSpace(baseUrl); - var service = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _endpointOptionsMonitorProviderArray, _hypermediaServiceLogger); + var service = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _endpointOptionsMonitorProviderArray, _httpContextAccessor, + _hypermediaServiceLogger); + Links result = service.Invoke(new Uri(baseUrl)); return Task.FromResult(result); } diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs index 34a6b425cc..b4f3bd15b9 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs @@ -23,12 +23,7 @@ internal sealed class HypermediaEndpointMiddleware( ? headerScheme.ToString() : httpContext.Request.Scheme; - // request.Host automatically includes or excludes the port based on whether it is standard for the scheme - // ... except when we manually change the scheme to match the X-Forwarded-Proto - string requestUri = scheme == "https" && httpContext.Request.Host.Port == 443 - ? $"{scheme}://{httpContext.Request.Host.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}" - : $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}"; - + string requestUri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.Path}"; return Task.FromResult(requestUri); } diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs index 1ee759de23..6e1df986b2 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.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 Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common; @@ -11,42 +12,49 @@ namespace Steeltoe.Management.Endpoint.Actuators.Hypermedia; -internal sealed class HypermediaService +internal sealed partial class HypermediaService { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly EndpointOptions _endpointOptions; private readonly ICollection _endpointOptionsMonitorProviders; + private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public HypermediaService(IOptionsMonitor managementOptionsMonitor, IOptionsMonitor hypermediaEndpointOptionsMonitor, - ICollection endpointOptionsMonitorProviders, ILogger logger) + ICollection endpointOptionsMonitorProviders, IHttpContextAccessor httpContextAccessor, + ILogger logger) { ArgumentNullException.ThrowIfNull(managementOptionsMonitor); ArgumentNullException.ThrowIfNull(hypermediaEndpointOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders); + ArgumentNullException.ThrowIfNull(httpContextAccessor); ArgumentGuard.ElementsNotNull(endpointOptionsMonitorProviders); ArgumentNullException.ThrowIfNull(logger); _managementOptionsMonitor = managementOptionsMonitor; _endpointOptions = hypermediaEndpointOptionsMonitor.CurrentValue; _endpointOptionsMonitorProviders = endpointOptionsMonitorProviders; + _httpContextAccessor = httpContextAccessor; _logger = logger; } public HypermediaService(IOptionsMonitor managementOptionsMonitor, IOptionsMonitor cloudFoundryEndpointOptionsMonitor, - ICollection endpointOptionsMonitorProviders, ILogger logger) + ICollection endpointOptionsMonitorProviders, IHttpContextAccessor httpContextAccessor, + ILogger logger) { ArgumentNullException.ThrowIfNull(managementOptionsMonitor); ArgumentNullException.ThrowIfNull(cloudFoundryEndpointOptionsMonitor); ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders); + ArgumentNullException.ThrowIfNull(httpContextAccessor); ArgumentGuard.ElementsNotNull(endpointOptionsMonitorProviders); ArgumentNullException.ThrowIfNull(logger); _managementOptionsMonitor = managementOptionsMonitor; _endpointOptions = cloudFoundryEndpointOptionsMonitor.CurrentValue; _endpointOptionsMonitorProviders = endpointOptionsMonitorProviders; + _httpContextAccessor = httpContextAccessor; _logger = logger; } @@ -62,12 +70,17 @@ public Links Invoke(Uri baseUrl) return links; } - _logger.LogTrace("Processing hypermedia for {ManagementOptions}", managementOptions); + LogProcessingHypermedia(); Link? selfLink = null; bool skipExposureCheck = PermissionsProvider.IsCloudFoundryRequest(baseUrl.PathAndQuery); string? basePath = managementOptions.GetBasePath(baseUrl.AbsolutePath); + if (_httpContextAccessor.HttpContext?.Request != null) + { + basePath = $"{_httpContextAccessor.HttpContext.Request.PathBase}{basePath}"; + } + foreach (EndpointOptions endpointOptions in _endpointOptionsMonitorProviders.Select(provider => provider.Get()).OrderBy(options => options.Id)) { if (endpointOptions.Id == null || !endpointOptions.IsEnabled(managementOptions)) @@ -88,7 +101,7 @@ public Links Invoke(Uri baseUrl) { if (links.Entries.ContainsKey(endpointOptions.Id)) { - _logger.LogWarning("Duplicate endpoint with ID '{DuplicateEndpointId}' detected.", endpointOptions.Id); + LogDuplicateEndpoint(endpointOptions.Id); } else { @@ -114,7 +127,12 @@ private static Link CreateLink(Uri baseUrl, string? basePath, EndpointOptions en }; string href = builder.Uri.ToString(); - bool isTemplated = !endpointOptions.RequiresExactMatch(); - return new Link(href, isTemplated); + return new Link(href, false); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Processing hypermedia.")] + private partial void LogProcessingHypermedia(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Duplicate endpoint with ID '{DuplicateEndpointId}' detected.")] + private partial void LogDuplicateEndpoint(string? duplicateEndpointId); } diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/Link.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/Link.cs index a6cb952bbe..05fb09a975 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/Link.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/Link.cs @@ -8,6 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Hypermedia; public sealed class Link { + [JsonPropertyName("href")] public string Href { get; set; } [JsonPropertyName("templated")] diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/Links.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/Links.cs index 6a748f1c6c..777f7f5e93 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/Links.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/Links.cs @@ -14,6 +14,7 @@ public sealed class Links /// /// Gets or sets the type of links contained in this collection. /// + [JsonPropertyName("type")] public string Type { get; set; } = "steeltoe"; /// diff --git a/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs b/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs index 75c62dcfad..71e1a065de 100644 --- a/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs +++ b/src/Management/src/Endpoint/Actuators/Info/Contributors/GitInfoContributor.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Info.Contributors; -internal sealed class GitInfoContributor : ConfigurationContributor, IInfoContributor +internal sealed partial class GitInfoContributor : ConfigurationContributor, IInfoContributor { private const string GitSettingsPrefix = "git"; private const string GitPropertiesFileName = "git.properties"; @@ -78,7 +78,7 @@ public async Task ContributeAsync(InfoBuilder builder, CancellationToken cancell } else { - _logger.LogWarning("File '{Path}' does not exist.", _propertiesPath); + LogFileNotFound(_propertiesPath); } return null; @@ -99,4 +99,7 @@ protected override void AddKeyValue(IDictionary dictionary, str dictionary[key] = valueToInsert; } + + [LoggerMessage(Level = LogLevel.Warning, Message = "File '{Path}' does not exist.")] + private partial void LogFileNotFound(string path); } diff --git a/src/Management/src/Endpoint/Actuators/Info/Contributors/RuntimeInfoContributor.cs b/src/Management/src/Endpoint/Actuators/Info/Contributors/RuntimeInfoContributor.cs new file mode 100644 index 0000000000..dc940d8a8b --- /dev/null +++ b/src/Management/src/Endpoint/Actuators/Info/Contributors/RuntimeInfoContributor.cs @@ -0,0 +1,28 @@ +// 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; + +namespace Steeltoe.Management.Endpoint.Actuators.Info.Contributors; + +internal sealed class RuntimeInfoContributor : IInfoContributor +{ + public Task ContributeAsync(InfoBuilder builder, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.WithInfo("runtime", new Dictionary + { + ["runtimeName"] = RuntimeInformation.FrameworkDescription, + ["runtimeVersion"] = System.Environment.Version.ToString(), + ["runtimeIdentifier"] = RuntimeInformation.RuntimeIdentifier, + ["processArchitecture"] = RuntimeInformation.ProcessArchitecture.ToString(), + ["osArchitecture"] = RuntimeInformation.OSArchitecture.ToString(), + ["osDescription"] = RuntimeInformation.OSDescription, + ["osVersion"] = System.Environment.OSVersion.ToString() + }); + + return Task.CompletedTask; + } +} diff --git a/src/Management/src/Endpoint/Actuators/Info/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/Info/EndpointServiceCollectionExtensions.cs index dd699748b6..93bca54b2e 100644 --- a/src/Management/src/Endpoint/Actuators/Info/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/Info/EndpointServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ private static void RegisterDefaultInfoContributors(IServiceCollection services) services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); } /// diff --git a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs index 8c1a65223b..38a942e86d 100644 --- a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointHandler.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Info; -internal sealed class InfoEndpointHandler : IInfoEndpointHandler +internal sealed partial class InfoEndpointHandler : IInfoEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IInfoContributor[] _contributors; @@ -44,11 +44,13 @@ public InfoEndpointHandler(IOptionsMonitor optionsMonitor, } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Exception thrown by contributor '{ContributorTypeName}' while contributing to info endpoint.", - contributor.GetType()); + LogContributorError(exception, contributor.GetType()); } } return builder.Build(); } + + [LoggerMessage(Level = LogLevel.Warning, Message = "Exception thrown by contributor '{ContributorTypeName}' while contributing to info endpoint.")] + private partial void LogContributorError(Exception exception, Type contributorTypeName); } diff --git a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs index d0412ee6d2..5abeef3d99 100644 --- a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointHandler.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Loggers; -internal sealed class LoggersEndpointHandler : ILoggersEndpointHandler +internal sealed partial class LoggersEndpointHandler : ILoggersEndpointHandler { private const string SpringDefaultCategoryName = "Default"; @@ -48,7 +48,7 @@ public LoggersEndpointHandler(IOptionsMonitor optionsMon { ArgumentNullException.ThrowIfNull(request); - _logger.LogDebug("Invoke({Request})", SecurityUtilities.SanitizeInput(request.ToString())); + ExpensiveLogEntering(request); LoggersResponse? response; @@ -65,6 +65,15 @@ public LoggersEndpointHandler(IOptionsMonitor optionsMon return Task.FromResult(response); } + private void ExpensiveLogEntering(LoggersRequest request) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + string input = SecurityUtilities.SanitizeInput(request.ToString()); + LogEntering(input); + } + } + private LoggersResponse GetLogLevels() { ICollection loggerStates = _dynamicLoggerProvider.GetLogLevels(); @@ -72,7 +81,7 @@ private LoggersResponse GetLogLevels() foreach (DynamicLoggerState loggerState in loggerStates.OrderBy(entry => entry.CategoryName)) { - _logger.LogTrace("Adding {LoggerState}", loggerState); + LogAddingLoggerState(loggerState); string categoryName = loggerState.CategoryName.Length == 0 ? SpringDefaultCategoryName : loggerState.CategoryName; var levels = new LoggerLevels(loggerState.BackupMinLevel, loggerState.EffectiveMinLevel); @@ -89,4 +98,10 @@ private void SetLogLevel(string name, string? level) _dynamicLoggerProvider.SetLogLevel(categoryName, logLevel); } + + [LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true, Message = "Invoking loggers endpoint handler with request {Request}.")] + private partial void LogEntering(string request); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Adding state {LoggerState}.")] + private partial void LogAddingLoggerState(DynamicLoggerState loggerState); } diff --git a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs index a29774a5ae..aa8b9fc352 100644 --- a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Loggers; -internal sealed class LoggersEndpointMiddleware( +internal sealed partial class LoggersEndpointMiddleware( ILoggersEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { @@ -31,19 +31,20 @@ internal sealed class LoggersEndpointMiddleware( if (httpContext.Request.Path.StartsWithSegments(path, out PathString remaining) && remaining.HasValue) { - string loggerName = remaining.Value!.TrimStart('/'); + string loggerName = remaining.Value.TrimStart('/'); - Dictionary change = await DeserializeRequestAsync(httpContext.Request.Body, cancellationToken); + Dictionary changes = await DeserializeRequestAsync(httpContext.Request.Body, cancellationToken); - change.TryGetValue("configuredLevel", out string? level); + // Spring Boot Admin sends {} to reset the level, while Apps Manager sends {"configuredLevel":null} + _ = changes.TryGetValue("configuredLevel", out string? level); - _logger.LogDebug("Change Request: {Name}, {Level}", loggerName, level ?? "RESET"); + LogChangeRequest(loggerName, level ?? "RESET"); if (!string.IsNullOrEmpty(loggerName)) { if (!string.IsNullOrEmpty(level) && LoggerLevels.StringToLogLevel(level) == null) { - _logger.LogDebug("Invalid LogLevel specified: {Level}", level); + LogInvalidLevel(level); return null; } @@ -55,11 +56,12 @@ internal sealed class LoggersEndpointMiddleware( return new LoggersRequest(); } - private async Task> DeserializeRequestAsync(Stream stream, CancellationToken cancellationToken) + private async Task> DeserializeRequestAsync(Stream stream, CancellationToken cancellationToken) { try { - var dictionary = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken); + JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; + var dictionary = await JsonSerializer.DeserializeAsync>(stream, options, cancellationToken); if (dictionary != null) { @@ -68,7 +70,7 @@ private async Task> DeserializeRequestAsync(Stream st } catch (Exception exception) when (!exception.IsCancellation()) { - _logger.LogError(exception, "Exception deserializing loggers endpoint request."); + LogDeserializationFailed(exception); } return []; @@ -99,4 +101,13 @@ protected override async Task WriteResponseAsync(LoggersResponse? response, Http await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Received request to change logger '{Name}' to level {Level}.")] + private partial void LogChangeRequest(string name, string level); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Invalid log level {Level} specified.")] + private partial void LogInvalidLevel(string level); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to deserialize loggers endpoint request.")] + private partial void LogDeserializationFailed(Exception exception); } diff --git a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs index 47b6fe4517..3b6a909399 100644 --- a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointHandler.cs @@ -9,7 +9,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.Refresh; -internal sealed class RefreshEndpointHandler : IRefreshEndpointHandler +internal sealed partial class RefreshEndpointHandler : IRefreshEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IConfiguration _configuration; @@ -30,7 +30,7 @@ public RefreshEndpointHandler(IOptionsMonitor optionsMon public Task> InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogInformation("Refreshing Configuration"); + LogRefreshingConfiguration(); if (_configuration is not IConfigurationRoot root) { @@ -51,4 +51,7 @@ public Task> InvokeAsync(object? argument, CancellationToken cance return Task.FromResult>(keys.ToList()); } + + [LoggerMessage(Level = LogLevel.Information, Message = "Refreshing configuration.")] + private partial void LogRefreshingConfiguration(); } diff --git a/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs b/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs index 39c995b7a9..018ac42330 100644 --- a/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs +++ b/src/Management/src/Endpoint/Actuators/RouteMappings/AspNetEndpointProvider.cs @@ -28,7 +28,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.RouteMappings; /// /// Gathers endpoints in an ASP.NET Core application by combining information from various sources. /// -internal sealed class AspNetEndpointProvider +internal sealed partial class AspNetEndpointProvider { private static readonly MethodInfo? ProducesContentTypesPropertyGetter = typeof(ProducesResponseTypeAttribute).GetProperty("ContentTypes", BindingFlags.Instance | BindingFlags.NonPublic)?.GetMethod; @@ -60,7 +60,7 @@ public IList GetEndpoints(bool includeActuators) { if (!_mvcOptionsMonitor.CurrentValue.EnableEndpointRouting) { - _logger.LogWarning("Conventional routing is not supported."); + LogConventionalRoutingNotSupported(); return []; } @@ -436,4 +436,7 @@ private static IEnumerable ExtractProducedContentTypes(EndpointMetadataC yield return contentType; } } + + [LoggerMessage(Level = LogLevel.Warning, Message = "Conventional routing is not supported.")] + private partial void LogConventionalRoutingNotSupported(); } diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs index 4270877b40..80e71fdc00 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/EventPipeThreadDumper.cs @@ -18,7 +18,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.ThreadDump; /// /// Thread dumper that uses the EventPipe to acquire the call stacks of all the running threads. /// -internal sealed class EventPipeThreadDumper : IThreadDumper +internal sealed partial class EventPipeThreadDumper : IThreadDumper { private const string ThreadIdTemplate = "Thread ("; @@ -63,7 +63,7 @@ public async Task> DumpThreadsAsync(CancellationToken cancella { try { - _logger.LogInformation("Attempting to create a thread dump."); + LogStart(); var client = new DiagnosticsClient(System.Environment.ProcessId); List providers = [new("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)]; @@ -72,28 +72,28 @@ public async Task> DumpThreadsAsync(CancellationToken cancella using EventPipeSession session = client.StartEventPipeSession(providers); List threads = await GetThreadsFromEventPipeSessionAsync(session, logWriter, cancellationToken); - _logger.LogInformation("Successfully created a thread dump."); + LogSucceeded(); return threads; } finally { #pragma warning disable S1215 // "GC.Collect" should not be called - long totalMemory = GC.GetTotalMemory(true); + long memoryInBytes = GC.GetTotalMemory(true); #pragma warning restore S1215 // "GC.Collect" should not be called - _logger.LogDebug("Total memory: {Memory}.", totalMemory); + LogTotalMemory(memoryInBytes); } }, cancellationToken); } internal async Task CaptureLogOutputAsync(Func> action, CancellationToken cancellationToken) { - bool isLogEnabled = _logger.IsEnabled(LogLevel.Trace); + bool isTraceLogEnabled = _logger.IsEnabled(LogLevel.Trace); using var logStream = new MemoryStream(); Exception? error = null; TResult? result = default; - await using (TextWriter logWriter = isLogEnabled ? new StreamWriter(logStream, leaveOpen: true) : TextWriter.Null) + await using (TextWriter logWriter = isTraceLogEnabled ? new StreamWriter(logStream, leaveOpen: true) : TextWriter.Null) { try { @@ -108,7 +108,7 @@ internal async Task CaptureLogOutputAsync(Func CaptureLogOutputAsync(Func> GetThreadsFromEventPipeSessionAsync(EventPi var computer = new SampleProfilerThreadTimeComputer(eventLog, symbolReader); computer.GenerateThreadTimeStacks(stackSource); - List results = ReadStackSource(stackSource, symbolReader).ToList(); + List results = ReadStackSource(stackSource, symbolReader, logWriter).ToList(); - _logger.LogTrace("Finished thread walk."); + LogThreadWalkFinished(results.Count); return results; } finally @@ -183,9 +183,7 @@ private async Task CreateTraceFileAsync(EventPipeSession session, Cancel } catch (TimeoutException) when (!cancellationToken.IsCancellationRequested) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - _logger.LogInformation("Sufficiently large applications can cause this command to take non-trivial amounts of time."); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogPossiblySlow(); throw; } @@ -231,12 +229,14 @@ private static SymbolReader CreateSymbolReader(TextWriter logWriter) }; } - private IEnumerable ReadStackSource(MutableTraceEventStackSource stackSource, SymbolReader symbolReader) + private IEnumerable ReadStackSource(MutableTraceEventStackSource stackSource, SymbolReader symbolReader, TextWriter logWriter) { var samplesForThread = new Dictionary>(); stackSource.ForEach(sample => { + logWriter.WriteLine($"[Steeltoe] Tracking sample: {sample}"); + StackSourceCallStackIndex stackIndex = sample.StackIndex; while (!stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false).StartsWith(ThreadIdTemplate, StringComparison.Ordinal)) @@ -257,10 +257,14 @@ private IEnumerable ReadStackSource(MutableTraceEventStackSource sta } }); + logWriter.WriteLine(samplesForThread.Count == 0 + ? "[Steeltoe] WARN: No managed samples found in memory dump." + : $"[Steeltoe] Start analyzing all {samplesForThread.Count} threads."); + // For every thread recorded in our trace, use the first stack. foreach ((int threadId, List samples) in samplesForThread) { - _logger.LogDebug("Found {Stacks} stacks for thread {Thread}.", samples.Count, threadId); + logWriter.WriteLine($"[Steeltoe] Found {samples.Count} samples for thread {threadId}, analyzing the first one."); var threadInfo = new ThreadInfo { @@ -269,7 +273,13 @@ private IEnumerable ReadStackSource(MutableTraceEventStackSource sta ThreadName = $"Thread-{threadId:D5}" }; - List stackTrace = GetStackTrace(threadId, samples[0], stackSource, symbolReader).ToList(); + List stackTrace = GetStackTrace(threadId, samples[0], stackSource, symbolReader, logWriter).ToList(); + + if (logWriter != TextWriter.Null) + { + int managedCount = stackTrace.Count(frame => !frame.IsNativeMethod); + logWriter.WriteLine($"[Steeltoe] Found {managedCount} of {stackTrace.Count} frames in managed code for thread {threadId}."); + } foreach (StackTraceElement stackFrame in stackTrace) { @@ -289,10 +299,10 @@ private static int ExtractThreadId(string frameName) return int.Parse(frameName.AsSpan(ThreadIdTemplate.Length, firstIndex - ThreadIdTemplate.Length), CultureInfo.InvariantCulture); } - private IEnumerable GetStackTrace(int threadId, StackSourceSample stackSourceSample, TraceEventStackSource stackSource, - SymbolReader symbolReader) + private static IEnumerable GetStackTrace(int threadId, StackSourceSample stackSourceSample, TraceEventStackSource stackSource, + SymbolReader symbolReader, TextWriter logWriter) { - _logger.LogDebug("Processing thread with ID: {Thread}.", threadId); + logWriter.WriteLine($"[Steeltoe] Walking stack frames of thread {threadId}."); StackSourceCallStackIndex stackIndex = stackSourceSample.StackIndex; StackSourceFrameIndex frameIndex = stackSource.GetFrameIndex(stackIndex); @@ -355,6 +365,24 @@ private static void SetThreadState(ThreadInfo threadInfo) : State.Runnable; } + [LoggerMessage(Level = LogLevel.Information, Message = "Attempting to create a thread dump.")] + private partial void LogStart(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Successfully created a thread dump.")] + private partial void LogSucceeded(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Total memory is {MemoryInBytes} bytes.")] + private partial void LogTotalMemory(long memoryInBytes); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Captured log from thread dump:{LineBreak}{DumpLog}")] + private partial void LogDumpLogCaptured(string lineBreak, string? dumpLog); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Finished thread walk, found {Count} results.")] + private partial void LogThreadWalkFinished(int count); + + [LoggerMessage(Level = LogLevel.Information, Message = "Sufficiently large applications can cause this command to take non-trivial amounts of time.")] + private partial void LogPossiblySlow(); + private sealed record StackFrameSymbol(string AssemblyName, string TypeName, string MemberName, string Parameters) { public static bool TryParse(string frameName, [NotNullWhen(true)] out StackFrameSymbol? symbol) diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs index 597f545f9b..87bde9475f 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointHandler.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Actuators.ThreadDump; -internal sealed class ThreadDumpEndpointHandler : IThreadDumpEndpointHandler +internal sealed partial class ThreadDumpEndpointHandler : IThreadDumpEndpointHandler { private readonly IOptionsMonitor _optionsMonitor; private readonly IThreadDumper _threadDumper; @@ -29,7 +29,10 @@ public ThreadDumpEndpointHandler(IOptionsMonitor opti public async Task> InvokeAsync(object? argument, CancellationToken cancellationToken) { - _logger.LogTrace("Invoking ThreadDumper"); + LogInvokingThreadDumper(); return await _threadDumper.DumpThreadsAsync(cancellationToken); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Invoking thread dumper.")] + private partial void LogInvokingThreadDumper(); } diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointOptions.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointOptions.cs index 5d613033ed..e9bb5101ec 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointOptions.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointOptions.cs @@ -8,6 +8,11 @@ namespace Steeltoe.Management.Endpoint.Actuators.ThreadDump; public sealed class ThreadDumpEndpointOptions : EndpointOptions { + /// + /// Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full. + /// + public override EndpointPermissions RequiredPermissions { get; set; } = EndpointPermissions.Full; + /// /// Gets or sets the time (in milliseconds) to trace for, before automatically stopping the trace. Default value: 10. /// diff --git a/src/Management/src/Endpoint/Configuration/IEndpointOptionsMonitorProvider.cs b/src/Management/src/Endpoint/Configuration/IEndpointOptionsMonitorProvider.cs index bce0a71eb8..9f80e3b911 100644 --- a/src/Management/src/Endpoint/Configuration/IEndpointOptionsMonitorProvider.cs +++ b/src/Management/src/Endpoint/Configuration/IEndpointOptionsMonitorProvider.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.Configuration; /// -/// Enables to register multiple typed providers to enumerate all s for the various +/// Enables registering multiple typed providers to enumerate all s for the various /// types. /// internal interface IEndpointOptionsMonitorProvider diff --git a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs index cb9bcc5f86..0314b64669 100644 --- a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs +++ b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs @@ -55,7 +55,7 @@ public sealed class ManagementOptions public bool SslEnabled { get; set; } /// - /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an + /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an /// X-Use-Status-Code-From-Response HTTP header. Default value: true. /// public bool UseStatusCodeFromResponse { get; set; } = true; diff --git a/src/Management/src/Endpoint/ConfigurationSchema.json b/src/Management/src/Endpoint/ConfigurationSchema.json index 979706704a..c7941f1d98 100644 --- a/src/Management/src/Endpoint/ConfigurationSchema.json +++ b/src/Management/src/Endpoint/ConfigurationSchema.json @@ -192,7 +192,7 @@ "items": { "type": "string" }, - "description": "Gets the list of keys to sanitize. A key can be a simple string that the property must end with, or a regular expression. A case-insensitive match is always performed. Use a single-element empty string to disable sanitization. Default value: [ \"password\", \"secret\", \"key\", \"token\", \".*credentials.*\", \"vcap_services\" ]" + "description": "Gets the list of keys to sanitize. A key can be a simple string that the property must end with, or a regular expression. A case-insensitive match is always performed. Use a single-element empty string to disable sanitization. Default value: [ \"password\", \"secret\", \"key\", \"token\", \".*credentials.*\", \"vcap_services\", \".*connectionstring.*\" ]" }, "Path": { "type": "string", @@ -204,7 +204,7 @@ "Restricted", "Full" ], - "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Restricted." + "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full." } } }, @@ -387,7 +387,7 @@ "Restricted", "Full" ], - "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Restricted." + "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full." } } }, @@ -624,9 +624,13 @@ "SerializerOptions": { "type": "object", "properties": { + "AllowDuplicateProperties": { + "type": "boolean", + "description": "Gets or sets a value that indicates whether duplicate property names are allowed when deserializing JSON objects." + }, "AllowOutOfOrderMetadataProperties": { "type": "boolean", - "description": "Allows JSON metadata properties to be specified after regular properties in a deserialized JSON object." + "description": "Gets or sets a value that indicates whether JSON metadata properties can be specified after regular properties in a deserialized JSON object." }, "AllowTrailingCommas": { "type": "boolean", @@ -641,7 +645,9 @@ "Never", "Always", "WhenWritingDefault", - "WhenWritingNull" + "WhenWritingNull", + "WhenWriting", + "WhenReading" ], "description": "Gets or sets a value that determines when properties with default values are ignored during serialization or deserialization. The default value is 'System.Text.Json.Serialization.JsonIgnoreCondition.Never'." }, @@ -798,13 +804,13 @@ "Restricted", "Full" ], - "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Restricted." + "description": "Gets or sets the permissions required to access this endpoint, when running on Cloud Foundry. Default value: Full." } } }, "UseStatusCodeFromResponse": { "type": "boolean", - "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." + "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." }, "Web": { "type": "object", diff --git a/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs b/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs index 761727907f..aa69b31ff9 100644 --- a/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs +++ b/src/Management/src/Endpoint/ManagementPort/ManagementPortMiddleware.cs @@ -14,7 +14,7 @@ namespace Steeltoe.Management.Endpoint.ManagementPort; /// /// Blocks access to actuator endpoints on ports other than the management port. Blocks access to non-actuator endpoints on the management port. /// -internal sealed class ManagementPortMiddleware +internal sealed partial class ManagementPortMiddleware { private readonly IOptionsMonitor _managementOptionsMonitor; private readonly RequestDelegate? _next; @@ -36,9 +36,9 @@ public async Task InvokeAsync(HttpContext context) ArgumentNullException.ThrowIfNull(context); ManagementOptions managementOptions = _managementOptionsMonitor.CurrentValue; - _logger.LogDebug("InvokeAsync({RequestPath}), OptionsPath: {OptionsPath}", context.Request.Path.Value, managementOptions.Path); + LogEntering(context.Request.Path.Value, managementOptions.Path); - bool allowRequest = IsRequestAllowed(context.Request, managementOptions); + bool allowRequest = IsRequestAllowed(context, managementOptions); if (!allowRequest) { @@ -53,13 +53,15 @@ public async Task InvokeAsync(HttpContext context) } } - private bool IsRequestAllowed(HttpRequest request, ManagementOptions managementOptions) + private bool IsRequestAllowed(HttpContext context, ManagementOptions managementOptions) { if (managementOptions.Port is > 0 and < 65536) { - bool isManagementPath = request.Path.StartsWithSegments(managementOptions.Path); - bool isManagementScheme = managementOptions.SslEnabled ? request.Scheme == Uri.UriSchemeHttps : request.Scheme == Uri.UriSchemeHttp; - bool isManagementPort = request.Host.Port == managementOptions.Port || HasMappedInstancePort(managementOptions.Port, request.Host.Port); + bool isManagementPath = context.Request.Path.StartsWithSegments(managementOptions.Path); + bool isManagementScheme = managementOptions.SslEnabled ? context.Request.Scheme == Uri.UriSchemeHttps : context.Request.Scheme == Uri.UriSchemeHttp; + + bool isManagementPort = context.Connection.LocalPort == managementOptions.Port || + HasMappedInstancePort(managementOptions.Port, context.Connection.LocalPort); return isManagementPath ? isManagementScheme && isManagementPort : !isManagementScheme || !isManagementPort; } @@ -74,19 +76,14 @@ private bool HasMappedInstancePort(int managementPort, int? requestPort) if (!string.IsNullOrEmpty(instancePorts)) { - var portMappings = JsonSerializer.Deserialize>(instancePorts); + List? portMappings = JsonSerializer.Deserialize(instancePorts, PortMappingJsonSerializerContext.Default.ListPortMapping); PortMapping? portMapping = portMappings?.Find(mapping => mapping.Internal == managementPort && (requestPort == mapping.ExternalTlsProxy || requestPort == mapping.InternalTlsProxy)); if (portMapping != null) { - if (_logger.IsEnabled(LogLevel.Trace)) - { - _logger.LogTrace( - "Request received on port {RequestPort}. Allowed by CF_INSTANCE_PORTS mapping: [ Internal: {InternalPort}, ExternalTlsProxy: {ExternalTlsProxy}, InternalTlsProxy: {InternalTlsProxy} ]", - requestPort, portMapping.Internal, portMapping.ExternalTlsProxy, portMapping.InternalTlsProxy); - } + LogPortMappingAllowed(requestPort, portMapping.Internal, portMapping.ExternalTlsProxy, portMapping.InternalTlsProxy); return true; } @@ -97,20 +94,24 @@ private bool HasMappedInstancePort(int managementPort, int? requestPort) private void SetResponseError(HttpContext context, int managementPort) { - int? defaultPort = null; - - if (context.Request.Host.Port == null) - { - defaultPort = context.Request.Scheme == "http" ? 80 : 443; - } - - _logger.LogWarning("Access to {Path} on port {Port} denied because 'Management:Endpoints:Port' is set to {ManagementPort}.", context.Request.Path, - defaultPort ?? context.Request.Host.Port, managementPort); + LogAccessDenied(context.Request.Path, context.Connection.LocalPort, managementPort); context.Response.StatusCode = StatusCodes.Status404NotFound; } - private sealed record PortMapping + [LoggerMessage(Level = LogLevel.Debug, Message = "Handling request at path {RequestPath} with options path {OptionsPath}.")] + private partial void LogEntering(string? requestPath, string? optionsPath); + + [LoggerMessage(Level = LogLevel.Trace, + Message = + "Request received on port {RequestPort}, allowed by CF_INSTANCE_PORTS mapping with internal port {InternalPort}, external TLS proxy {ExternalTlsProxy} and internal TLS proxy {InternalTlsProxy}.")] + private partial void LogPortMappingAllowed(int? requestPort, int? internalPort, int? externalTlsProxy, int? internalTlsProxy); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "Access to {Path} on port {Port} denied because 'Management:Endpoints:Port' is set to {ManagementPort}.")] + private partial void LogAccessDenied(PathString path, int? port, int managementPort); + + internal sealed record PortMapping { [JsonPropertyName("internal")] public int? Internal { get; init; } diff --git a/src/Management/src/Endpoint/ManagementPort/ManagementPortStartupFilter.cs b/src/Management/src/Endpoint/ManagementPort/ManagementPortStartupFilter.cs index f51eb3d137..8a525bcdcf 100644 --- a/src/Management/src/Endpoint/ManagementPort/ManagementPortStartupFilter.cs +++ b/src/Management/src/Endpoint/ManagementPort/ManagementPortStartupFilter.cs @@ -47,7 +47,7 @@ public Action Configure(Action next) int managementPort = _configuration.GetValue(ManagementPortConfigurationKey) ?? 0; bool useHttps = _configuration.GetValue(ManagementPortSslConfigurationKey) ?? false; - if (managementPort > 0) + if (managementPort is > 0 and < 65536) { var server = applicationBuilder.ApplicationServices.GetRequiredService(); ICollection addresses = server.Features.GetRequiredFeature().Addresses; diff --git a/src/Discovery/src/Eureka/Transport/JsonApplicationRoot.cs b/src/Management/src/Endpoint/ManagementPort/PortMappingJsonSerializerContext.cs similarity index 50% rename from src/Discovery/src/Eureka/Transport/JsonApplicationRoot.cs rename to src/Management/src/Endpoint/ManagementPort/PortMappingJsonSerializerContext.cs index 5957fa4a22..1e59b524ed 100644 --- a/src/Discovery/src/Eureka/Transport/JsonApplicationRoot.cs +++ b/src/Management/src/Endpoint/ManagementPort/PortMappingJsonSerializerContext.cs @@ -4,10 +4,8 @@ using System.Text.Json.Serialization; -namespace Steeltoe.Discovery.Eureka.Transport; +namespace Steeltoe.Management.Endpoint.ManagementPort; -internal sealed class JsonApplicationRoot -{ - [JsonPropertyName("application")] - public JsonApplication? Application { get; set; } -} +[JsonSourceGenerationOptions] +[JsonSerializable(typeof(List))] +internal sealed partial class PortMappingJsonSerializerContext : JsonSerializerContext; diff --git a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs index 462fd375a5..c25348d82d 100644 --- a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs @@ -7,13 +7,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Steeltoe.Management.Configuration; using Steeltoe.Management.Endpoint.Configuration; namespace Steeltoe.Management.Endpoint.Middleware; -public abstract class EndpointMiddleware : IEndpointMiddleware +public abstract partial class EndpointMiddleware : IEndpointMiddleware { private readonly ILogger _logger; protected IOptionsMonitor ManagementOptionsMonitor { get; } @@ -57,25 +58,24 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) { if (!allowedVerbs.Contains(context.Request.Method)) { - _logger.LogTrace("{Method} method is unavailable at path {Path}.", context.Request.Method, context.Request.Path.Value); + LogUnavailableHttpMethod(context.Request.Method, context.Request.Path.Value); context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; } else if (!IsValidContentType(context.Request)) { - _logger.LogDebug("Content-Type header '{RequestContentType}' is not supported for this request.", context.Request.ContentType); + LogUnsupportedContentType(context.Request.ContentType); context.Response.StatusCode = (int)HttpStatusCode.UnsupportedMediaType; await context.Response.WriteAsync($"Only the '{ContentType}' content type is supported.", context.RequestAborted); } else if (!IsCompatibleAcceptHeader(context.Request)) { - _logger.LogDebug("Accept header '{AcceptType}' is not supported for this request.", context.Request.Headers.Accept.ToString()); + LogUnsupportedAcceptHeader(context.Request.Headers.Accept); context.Response.StatusCode = (int)HttpStatusCode.NotAcceptable; await context.Response.WriteAsync($"Only the '{ContentType}' content type is supported.", context.RequestAborted); } else { - _logger.LogDebug("Reading {Method} request at path {Path} using {MiddlewareType}.", context.Request.Method, context.Request.Path.Value, - GetType()); + LogReadingRequest(context.Request.Method, context.Request.Path.Value, GetType()); TRequest? request = await ParseRequestAsync(context, context.RequestAborted); TResponse response = await InvokeEndpointHandlerAsync(request, context.RequestAborted); @@ -87,7 +87,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) } else { - _logger.LogTrace("CanInvoke returned false for {Method} request at path {Path}.", context.Request.Method, context.Request.Path.Value); + LogInvokeDenied(context.Request.Method, context.Request.Path.Value); } context.Response.StatusCode = (int)HttpStatusCode.NotFound; @@ -147,4 +147,19 @@ protected virtual async Task WriteResponseAsync(TResponse response, HttpContext JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } + + [LoggerMessage(Level = LogLevel.Trace, Message = "{Method} method is unavailable at path {Path}.")] + private partial void LogUnavailableHttpMethod(string method, string? path); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Content-Type header '{RequestContentType}' is not supported for this request.")] + private partial void LogUnsupportedContentType(string? requestContentType); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Accept header '{AcceptType}' is not supported for this request.")] + private partial void LogUnsupportedAcceptHeader(StringValues acceptType); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Reading {Method} request at path {Path} using {MiddlewareType}.")] + private partial void LogReadingRequest(string method, string? path, Type middlewareType); + + [LoggerMessage(Level = LogLevel.Trace, Message = "CanInvoke returned false for {Method} request at path {Path}.")] + private partial void LogInvokeDenied(string method, string? path); } diff --git a/src/Management/src/Endpoint/PublicAPI.Shipped.txt b/src/Management/src/Endpoint/PublicAPI.Shipped.txt index 94b37734d4..ae2391b51b 100644 --- a/src/Management/src/Endpoint/PublicAPI.Shipped.txt +++ b/src/Management/src/Endpoint/PublicAPI.Shipped.txt @@ -2,14 +2,20 @@ abstract Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.InvokeEndpointHandlerAsync(TRequest? request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! const Steeltoe.Management.Endpoint.Actuators.Health.Availability.ApplicationAvailability.LivenessKey = "Liveness" -> string! const Steeltoe.Management.Endpoint.Actuators.Health.Availability.ApplicationAvailability.ReadinessKey = "Readiness" -> string! +override Steeltoe.Management.Endpoint.Actuators.Environment.EnvironmentEndpointOptions.RequiredPermissions.get -> Steeltoe.Management.Configuration.EndpointPermissions +override Steeltoe.Management.Endpoint.Actuators.Environment.EnvironmentEndpointOptions.RequiredPermissions.set -> void override Steeltoe.Management.Endpoint.Actuators.Health.Availability.AvailabilityState.ToString() -> string! override Steeltoe.Management.Endpoint.Actuators.Health.HealthEndpointOptions.RequiresExactMatch() -> bool +override Steeltoe.Management.Endpoint.Actuators.HeapDump.HeapDumpEndpointOptions.RequiredPermissions.get -> Steeltoe.Management.Configuration.EndpointPermissions +override Steeltoe.Management.Endpoint.Actuators.HeapDump.HeapDumpEndpointOptions.RequiredPermissions.set -> void override Steeltoe.Management.Endpoint.Actuators.Info.EpochSecondsDateTimeConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> System.DateTime override Steeltoe.Management.Endpoint.Actuators.Info.EpochSecondsDateTimeConverter.Write(System.Text.Json.Utf8JsonWriter! writer, System.DateTime value, System.Text.Json.JsonSerializerOptions! options) -> void override Steeltoe.Management.Endpoint.Actuators.Loggers.LoggersEndpointOptions.RequiresExactMatch() -> bool override Steeltoe.Management.Endpoint.Actuators.Loggers.LoggersRequest.ToString() -> string! override Steeltoe.Management.Endpoint.Actuators.Services.ServiceRegistration.ToString() -> string! override Steeltoe.Management.Endpoint.Actuators.ThreadDump.StackTraceElement.ToString() -> string! +override Steeltoe.Management.Endpoint.Actuators.ThreadDump.ThreadDumpEndpointOptions.RequiredPermissions.get -> Steeltoe.Management.Configuration.EndpointPermissions +override Steeltoe.Management.Endpoint.Actuators.ThreadDump.ThreadDumpEndpointOptions.RequiredPermissions.set -> void override Steeltoe.Management.Endpoint.Actuators.ThreadDump.ThreadInfo.ToString() -> string! static readonly Steeltoe.Management.Endpoint.Actuators.Health.Availability.LivenessState.Broken -> Steeltoe.Management.Endpoint.Actuators.Health.Availability.LivenessState! static readonly Steeltoe.Management.Endpoint.Actuators.Health.Availability.LivenessState.Correct -> Steeltoe.Management.Endpoint.Actuators.Health.Availability.LivenessState! @@ -204,6 +210,7 @@ Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangePrincipal.Name. Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Headers.get -> System.Collections.Generic.IDictionary! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.HttpExchangeRequest(string! method, System.Uri! uri, System.Collections.Generic.IDictionary! headers, string? remoteAddress) -> void +Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.JsonUri.get -> string! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Method.get -> string! Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.RemoteAddress.get -> string? Steeltoe.Management.Endpoint.Actuators.HttpExchanges.HttpExchangeRequest.Uri.get -> System.Uri! diff --git a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt index a2ab2cf104..7dc5c58110 100755 --- a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt +++ b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt @@ -1 +1 @@ -#nullable enable +#nullable enable diff --git a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs index f2cf82a106..9477238be3 100644 --- a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs +++ b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs @@ -8,7 +8,7 @@ namespace Steeltoe.Management.Endpoint.SpringBootAdminClient; -internal sealed class SpringBootAdminPeriodicRefresh : IAsyncDisposable +internal sealed partial class SpringBootAdminPeriodicRefresh : IAsyncDisposable { private readonly SpringBootAdminRefreshRunner _runner; private readonly ILogger _logger; @@ -39,29 +39,34 @@ private async Task TimerLoopAsync(TimeSpan interval) { try { - _logger.LogDebug("Starting periodic refresh loop with interval {Interval}.", interval); + LogStartingPeriodicRefreshLoop(interval); + bool isFirstTime = true; do { - _logger.LogDebug("Starting refresh cycle."); - - try - { - await _runner.RunAsync(_timerTokenSource.Token); - } - catch (Exception exception) when (!exception.IsCancellation()) + // A tick queued just before periodic refresh was disabled would still be delivered here. + // Checking the period prevents executing a stale tick when refresh has been turned off. + if (isFirstTime || _periodicTimer.Period != Timeout.InfiniteTimeSpan) { - _logger.LogWarning(exception, "Refresh cycle failed."); + LogStartingRefreshCycle(); + + try + { + await _runner.RunAsync(isFirstTime, _timerTokenSource.Token); + } + catch (Exception exception) when (!exception.IsCancellation()) + { + LogRefreshCycleFailed(exception); + } } + + isFirstTime = false; } while (await _periodicTimer.WaitForNextTickAsync(_timerTokenSource.Token)); } catch (OperationCanceledException) { -#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - // Justification: The exception contains no useful information. Logging it suggests something crashed, while this is expected behavior. - _logger.LogDebug("Stopped periodic refresh loop."); -#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + LogPeriodicRefreshLoopStopped(); } } @@ -72,7 +77,7 @@ private void ChangeInterval(TimeSpan interval) if (safeInterval != _periodicTimer.Period) { _periodicTimer.Period = safeInterval; - _logger.LogDebug("Refresh interval changed to {Interval}.", safeInterval); + LogRefreshIntervalChanged(safeInterval); } } @@ -84,10 +89,10 @@ private static TimeSpan InfiniteWhenZero(TimeSpan interval) public async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogDebug("Signaling to stop periodic refresh loop."); + LogSignalingStop(); await DisposeAsync(); - _logger.LogDebug("Starting cleanup."); + LogStartingCleanup(); await _runner.CleanupAsync(cancellationToken); } @@ -104,4 +109,25 @@ public async ValueTask DisposeAsync() _periodicTimer.Dispose(); } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting periodic refresh loop with interval {Interval}.")] + private partial void LogStartingPeriodicRefreshLoop(TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting refresh cycle.")] + private partial void LogStartingRefreshCycle(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Refresh cycle failed.")] + private partial void LogRefreshCycleFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Stopped periodic refresh loop.")] + private partial void LogPeriodicRefreshLoopStopped(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Refresh interval changed to {Interval}.")] + private partial void LogRefreshIntervalChanged(TimeSpan interval); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Signaling to stop periodic refresh loop.")] + private partial void LogSignalingStop(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Starting cleanup.")] + private partial void LogStartingCleanup(); } diff --git a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs index 4f1ce42ff5..fa5d7ba2d0 100644 --- a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs +++ b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminRefreshRunner.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Management.Endpoint.SpringBootAdminClient; -internal sealed class SpringBootAdminRefreshRunner +internal sealed partial class SpringBootAdminRefreshRunner { private readonly AppUrlCalculator _appUrlCalculator; private readonly SpringBootAdminApiClient _springBootAdminApiClient; @@ -54,19 +54,19 @@ public SpringBootAdminRefreshRunner(AppUrlCalculator appUrlCalculator, SpringBoo _logger = logger; } - public async Task RunAsync(CancellationToken cancellationToken) + public async Task RunAsync(bool isFirstTime, CancellationToken cancellationToken) { - _logger.LogDebug("Validating options."); + LogValidatingOptions(); SpringBootAdminClientOptions clientOptions = _clientOptionsMonitor.CurrentValue; ValidateAndSetOptions(clientOptions); if (_lastGoodOptions?.Url != null && !string.Equals(_lastGoodOptions.Url, clientOptions.Url, StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Spring Boot Admin Server URL changed from {LastUrl} to {NewUrl}, unregistering first.", _lastGoodOptions.Url, clientOptions.Url); + LogUrlChanged(_lastGoodOptions.Url, clientOptions.Url); await SafeUnregisterAsync(_lastGoodOptions, cancellationToken); } - await RegisterAsync(clientOptions, cancellationToken); + await RegisterAsync(clientOptions, isFirstTime, cancellationToken); } private void ValidateAndSetOptions(SpringBootAdminClientOptions options) @@ -124,11 +124,19 @@ private void ValidateAndSetOptions(SpringBootAdminClientOptions options) } } - private async Task RegisterAsync(SpringBootAdminClientOptions clientOptions, CancellationToken cancellationToken) + private async Task RegisterAsync(SpringBootAdminClientOptions clientOptions, bool isFirstTime, CancellationToken cancellationToken) { Application app = CreateApplication(new Uri(clientOptions.BaseUrl!), clientOptions); - _logger.LogInformation("Registering with Spring Boot Admin Server at {Url}.", clientOptions.Url); + if (isFirstTime) + { + LogRegisteringFirstTime(clientOptions.Url); + } + else + { + LogRegisteringNotFirstTime(clientOptions.Url); + } + _lastRegistrationId = await _springBootAdminApiClient.RegisterAsync(app, clientOptions, cancellationToken); _lastGoodOptions = clientOptions; } @@ -175,7 +183,7 @@ private async Task SafeUnregisterAsync(SpringBootAdminClientOptions clientOption { try { - _logger.LogInformation("Unregistering from Spring Boot Admin Server at {Url}.", clientOptions.Url); + LogUnregistering(clientOptions.Url); await _springBootAdminApiClient.UnregisterAsync(_lastRegistrationId, clientOptions, cancellationToken); _lastRegistrationId = null; } @@ -183,9 +191,27 @@ private async Task SafeUnregisterAsync(SpringBootAdminClientOptions clientOption { if (!exception.IsCancellation()) { - _logger.LogWarning(exception, "Failed to unregister from Spring Boot Admin server at {Url}.", clientOptions.Url); + LogUnregisterFailed(exception, clientOptions.Url); } } } } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Validating options.")] + private partial void LogValidatingOptions(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Spring Boot Admin Server URL changed from {LastUrl} to {NewUrl}, unregistering first.")] + private partial void LogUrlChanged(string? lastUrl, string? newUrl); + + [LoggerMessage(Level = LogLevel.Information, Message = "Registering with Spring Boot Admin Server at {Url}.")] + private partial void LogRegisteringFirstTime(string? url); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Registering with Spring Boot Admin Server at {Url}.")] + private partial void LogRegisteringNotFirstTime(string? url); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Unregistering from Spring Boot Admin Server at {Url}.")] + private partial void LogUnregistering(string? url); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to unregister from Spring Boot Admin server at {Url}.")] + private partial void LogUnregisterFailed(Exception exception, string? url); } diff --git a/src/Management/src/Endpoint/Steeltoe.Management.Endpoint.csproj b/src/Management/src/Endpoint/Steeltoe.Management.Endpoint.csproj index 6f7828f732..eba86e4344 100755 --- a/src/Management/src/Endpoint/Steeltoe.Management.Endpoint.csproj +++ b/src/Management/src/Endpoint/Steeltoe.Management.Endpoint.csproj @@ -1,6 +1,6 @@ - + - net8.0 + net10.0;net8.0 Steeltoe management endpoints, also known as actuators. Includes support for Cloud Foundry integration. actuator;actuators;management;monitoring;Spring;Boot;dbmigrations;health;heap-dump;loggers;route-mappings;thread-dump;tanzu true @@ -24,19 +24,6 @@ - - - $(Pkgdotnet-gcdump)\tools\net8.0\any\dotnet-gcdump.dll - - - - $(Pkgdotnet-gcdump)\tools\net8.0\any\Microsoft.Diagnostics.FastSerialization.dll - - - @@ -44,4 +31,24 @@ + + + + + $(Pkgdotnet-gcdump)\tools\net8.0\any\dotnet-gcdump.dll + + + + + + + <_DotNetGCDumpNet10 Include="$(MSBuildProjectDirectory)\bin\$(Configuration)\net10.0\dotnet-gcdump.dll" Pack="True" PackagePath="lib\net10.0" /> + <_DotNetGCDumpNet8 Include="$(MSBuildProjectDirectory)\bin\$(Configuration)\net8.0\dotnet-gcdump.dll" Pack="True" PackagePath="lib\net8.0" /> + + + + diff --git a/src/Management/src/Prometheus/PrometheusExtensions.cs b/src/Management/src/Prometheus/PrometheusExtensions.cs index 7d59c42236..ef7a4b7276 100644 --- a/src/Management/src/Prometheus/PrometheusExtensions.cs +++ b/src/Management/src/Prometheus/PrometheusExtensions.cs @@ -17,7 +17,7 @@ namespace Steeltoe.Management.Prometheus; -public static class PrometheusExtensions +public static partial class PrometheusExtensions { /// /// Adds the services used by the Steeltoe-configured OpenTelemetry Prometheus exporter and configures the ASP.NET Core middleware pipeline. @@ -140,7 +140,7 @@ public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder if (permissionsProvider is null) { - logger.LogWarning("The Cloud Foundry Actuator is required in order to run the Prometheus exporter under the Cloud Foundry context."); + LogCloudFoundryActuatorRequired(logger); } else { @@ -154,13 +154,12 @@ public static IApplicationBuilder UsePrometheusActuator(this IApplicationBuilder if (applyActuatorConventions && !isEndpointRoutingEnabled) { - logger.LogWarning("Customizing endpoints is only supported when using endpoint routing."); + LogEndpointRoutingRequired(logger); } - if (managementOptions.Port == 0 && !applyActuatorConventions && configurePrometheusPipeline is null) + if (managementOptions.Port is not (> 0 and < 65536) && !applyActuatorConventions && configurePrometheusPipeline is null) { - logger.LogWarning( - "The Prometheus endpoint may not be configured securely. Consider using a dedicated management port, adding actuator conventions or configuring the Prometheus middleware pipeline."); + LogPrometheusEndpointNotSecure(logger); } builder.UseOpenTelemetryPrometheusScrapingEndpoint(null, null, endpointPath, ConfigureBranchedPipeline, null); @@ -195,4 +194,16 @@ void ConfigureBranchedPipeline(IApplicationBuilder branchedApplicationBuilder) } } } + + [LoggerMessage(Level = LogLevel.Warning, + Message = "The Cloud Foundry Actuator is required in order to run the Prometheus exporter under the Cloud Foundry context.")] + private static partial void LogCloudFoundryActuatorRequired(ILogger logger); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Customizing endpoints is only supported when using endpoint routing.")] + private static partial void LogEndpointRoutingRequired(ILogger logger); + + [LoggerMessage(Level = LogLevel.Warning, + Message = "The Prometheus endpoint may not be configured securely. " + + "Consider using a dedicated management port, adding actuator conventions or configuring the Prometheus middleware pipeline.")] + private static partial void LogPrometheusEndpointNotSecure(ILogger logger); } diff --git a/src/Management/src/Prometheus/Steeltoe.Management.Prometheus.csproj b/src/Management/src/Prometheus/Steeltoe.Management.Prometheus.csproj index 138d7bc362..f37e894adf 100644 --- a/src/Management/src/Prometheus/Steeltoe.Management.Prometheus.csproj +++ b/src/Management/src/Prometheus/Steeltoe.Management.Prometheus.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Adds Prometheus support for Steeltoe management endpoints. actuator;actuators;management;monitoring;metrics;prometheus;tanzu;appmetrics;aspnetcore true diff --git a/src/Management/src/Tasks/Steeltoe.Management.Tasks.csproj b/src/Management/src/Tasks/Steeltoe.Management.Tasks.csproj index c283adb44f..375ca05744 100644 --- a/src/Management/src/Tasks/Steeltoe.Management.Tasks.csproj +++ b/src/Management/src/Tasks/Steeltoe.Management.Tasks.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Extensions for running tasks embedded in your .NET application. Ideal for cf run-task in Cloud Foundry. tasks;management;Spring;Cloud;cf;run-task true diff --git a/src/Management/src/Tasks/TaskHostExtensions.cs b/src/Management/src/Tasks/TaskHostExtensions.cs index 766f2e033e..c52dee9702 100644 --- a/src/Management/src/Tasks/TaskHostExtensions.cs +++ b/src/Management/src/Tasks/TaskHostExtensions.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Management.Tasks; -public static class TaskHostExtensions +public static partial class TaskHostExtensions { /// /// Indicates whether will run an application task, instead of the regular application. @@ -132,7 +132,7 @@ private static async Task RunTaskAsync(string taskName, IServiceProvider service { var loggerFactory = serviceProvider.GetRequiredService(); ILogger logger = loggerFactory.CreateLogger($"{typeof(TaskHostExtensions).Namespace}.CloudFoundryTasks"); - logger.LogError("No task with name '{TaskName}' is registered in the service container.", taskName); + LogTaskNotFound(logger, taskName); } } @@ -147,4 +147,7 @@ private static async Task DisposeHostAsync(IDisposable host) host.Dispose(); } } + + [LoggerMessage(Level = LogLevel.Error, Message = "No task with name '{TaskName}' is registered in the service container.")] + private static partial void LogTaskNotFound(ILogger logger, string taskName); } diff --git a/src/Management/src/Tracing/ConfigurationSchema.json b/src/Management/src/Tracing/ConfigurationSchema.json index a36a6c531c..bcccfa21ed 100644 --- a/src/Management/src/Tracing/ConfigurationSchema.json +++ b/src/Management/src/Tracing/ConfigurationSchema.json @@ -1,4 +1,19 @@ { + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Management": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Management.Tracing": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, "type": "object", "properties": { "Spring": { diff --git a/src/Management/src/Tracing/Properties/AssemblyInfo.cs b/src/Management/src/Tracing/Properties/AssemblyInfo.cs index 882eca9492..44d789a80e 100644 --- a/src/Management/src/Tracing/Properties/AssemblyInfo.cs +++ b/src/Management/src/Tracing/Properties/AssemblyInfo.cs @@ -7,5 +7,6 @@ using Steeltoe.Common.Configuration; [assembly: ConfigurationSchema("Spring:Application", typeof(SpringApplicationSettings))] +[assembly: LoggingCategories("Steeltoe", "Steeltoe.Management", "Steeltoe.Management.Tracing")] [assembly: InternalsVisibleTo("Steeltoe.Management.Tracing.Test")] diff --git a/src/Management/src/Tracing/Steeltoe.Management.Tracing.csproj b/src/Management/src/Tracing/Steeltoe.Management.Tracing.csproj index c409ab9324..fc918c0843 100644 --- a/src/Management/src/Tracing/Steeltoe.Management.Tracing.csproj +++ b/src/Management/src/Tracing/Steeltoe.Management.Tracing.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Adds trace information to logging output in distributed systems. management;monitoring;distributed;tracing true diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudControllerPermissionsMock.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudControllerPermissionsMock.cs index 5d581d2d1b..3c7a6e22de 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudControllerPermissionsMock.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudControllerPermissionsMock.cs @@ -32,11 +32,17 @@ internal static DelegateToMockHttpClientHandler GetHttpMessageHandler() httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/exception/permissions") .Throw(new HttpRequestException(HttpRequestError.NameResolutionError)); - httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/no_sensitive_data/permissions").Respond(HttpStatusCode.OK, + httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/broken-response/permissions") + .Respond(HttpStatusCode.OK, "application/json", "{"); + + httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/no-permissions/permissions").Respond(HttpStatusCode.OK, "application/json", + """{"read_sensitive_data": false, "read_basic_data": false}"""); + + httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/restricted-permissions/permissions").Respond(HttpStatusCode.OK, "application/json", """{"read_sensitive_data": false, "read_basic_data": true}"""); - httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/success/permissions").Respond(HttpStatusCode.OK, "application/json", - """{"read_sensitive_data": true, "read_basic_data": true}"""); + httpClientHandler.Mock.When(HttpMethod.Get, "https://example.api.com/v2/apps/full-permissions/permissions").Respond(HttpStatusCode.OK, + "application/json", """{"read_sensitive_data": true, "read_basic_data": true}"""); return httpClientHandler; } diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs index 9faf688a6d..6a4e565aea 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs @@ -27,7 +27,7 @@ public sealed class CloudFoundryActuatorTest private const string VcapApplicationForMock = """ { "cf_api": "https://example.api.com", - "application_id": "success" + "application_id": "full-permissions" } """; @@ -176,7 +176,7 @@ public async Task Endpoint_returns_expected_data_with_all_actuators_registered(H }, "health": { "href": "http://localhost/cloudfoundryapplication/health", - "templated": true + "templated": false }, "heapdump": { "href": "http://localhost/cloudfoundryapplication/heapdump", @@ -192,7 +192,7 @@ public async Task Endpoint_returns_expected_data_with_all_actuators_registered(H }, "loggers": { "href": "http://localhost/cloudfoundryapplication/loggers", - "templated": true + "templated": false }, "mappings": { "href": "http://localhost/cloudfoundryapplication/mappings", @@ -410,7 +410,7 @@ public async Task Hides_disabled_actuators_and_ignores_exposure() "_links": { "loggers": { "href": "http://localhost/cloudfoundryapplication/loggers", - "templated": true + "templated": false }, "self": { "href": "http://localhost/cloudfoundryapplication", @@ -421,6 +421,57 @@ public async Task Hides_disabled_actuators_and_ignores_exposure() """); } + [Fact] + public async Task Ignores_exposure_with_UsePathBase() + { + using var scope = new EnvironmentVariableScope("VCAP_APPLICATION", VcapApplicationForMock); + + var appSettings = new Dictionary + { + ["Management:Endpoints:Actuator:Exposure:Include:0"] = "*", + ["Management:Endpoints:Actuator:Exposure:Exclude:1"] = "loggers" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddCloudFoundry(); + builder.Configuration.AddInMemoryCollection(appSettings); + builder.Services.AddCloudFoundryActuator(false); + builder.Services.AddLoggersActuator(false); + + await using WebApplication host = builder.Build(); + + host.UsePathBase("/some/prefix"); + host.UseRouting(); + host.UseCloudFoundrySecurity(); + host.UseActuatorEndpoints(); + + host.Services.GetRequiredService().Using(CloudControllerPermissionsMock.GetHttpMessageHandler()); + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage response = await AuthenticatedGetAsync(httpClient, new Uri("http://localhost/some/prefix/cloudfoundryapplication")); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + string responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + responseBody.Should().BeJson(""" + { + "type": "steeltoe", + "_links": { + "loggers": { + "href": "http://localhost/some/prefix/cloudfoundryapplication/loggers", + "templated": false + }, + "self": { + "href": "http://localhost/some/prefix/cloudfoundryapplication", + "templated": false + } + } + } + """); + } + [Theory] [InlineData("http://somehost:1234", "https://somehost:1234", "https")] [InlineData("http://somehost:443", "https://somehost", "https")] @@ -471,21 +522,21 @@ public async Task Can_change_configuration_at_runtime() var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddCloudFoundry(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddCloudFoundryActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -507,7 +558,7 @@ public async Task Can_change_configuration_at_runtime() "_links": { "health": { "href": "http://localhost/cloudfoundryapplication/health", - "templated": true + "templated": false }, "self": { "href": "http://localhost/cloudfoundryapplication", @@ -517,17 +568,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTest.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTest.cs index c3639b9ac4..1197f57d97 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTest.cs @@ -327,15 +327,17 @@ public async Task RedactsHttpHeaders() { var appSettings = new Dictionary { - ["vcap:application:application_id"] = "success", + ["vcap:application:application_id"] = "full-permissions", ["vcap:application:cf_api"] = "https://example.api.com" }; - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("System.Net.Http.HttpClient", StringComparison.Ordinal)); + using var capturingLoggerProvider = + new CapturingLoggerProvider(category => category.StartsWith("System.Net.Http.HttpClient", StringComparison.Ordinal)); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(appSettings); builder.Configuration.AddCloudFoundry(); + // ReSharper disable once AccessToDisposedClosure builder.Services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider)); builder.Services.AddCloudFoundryActuator(); await using WebApplication app = builder.Build(); @@ -377,27 +379,29 @@ public async Task Returns_expected_response_on_permission_check(string scenario, host.Services.GetRequiredService().Using(CloudControllerPermissionsMock.GetHttpMessageHandler()); await host.StartAsync(TestContext.Current.CancellationToken); + // ReSharper disable once ShortLivedHttpClient using var client = new HttpClient(); - var testAuthenticationRequestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost:5000/cloudfoundryapplication")); - testAuthenticationRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MockAccessToken); - var testAuthorizationRequestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost:5000/cloudfoundryapplication/info")); - testAuthorizationRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MockAccessToken); - HttpResponseMessage response = await client.SendAsync(testAuthenticationRequestMessage, TestContext.Current.CancellationToken); - response.StatusCode.Should().Be(steeltoeStatusCode); + var authenticationRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost:5000/cloudfoundryapplication")); + authenticationRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MockAccessToken); + var authorizationRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost:5000/cloudfoundryapplication/info")); + authorizationRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MockAccessToken); + + HttpResponseMessage authenticationResponse = await client.SendAsync(authenticationRequest, TestContext.Current.CancellationToken); + authenticationResponse.StatusCode.Should().Be(steeltoeStatusCode); if (errorMessage != null) { string jsonErrorValue = JsonValue.Create(errorMessage).ToJsonString(); - string errorText = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + string errorText = await authenticationResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); errorText.Should().Be(steeltoeStatusCode == HttpStatusCode.InternalServerError ? errorMessage : $$"""{"security_error":{{jsonErrorValue}}}"""); } else { - string responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + string authenticationResponseBody = await authenticationResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - responseBody.Should().BeJson(""" + authenticationResponseBody.Should().BeJson(""" { "type":"steeltoe", "_links":{ @@ -414,8 +418,8 @@ public async Task Returns_expected_response_on_permission_check(string scenario, """); } - HttpResponseMessage fullPermissionResponse = await client.SendAsync(testAuthorizationRequestMessage, TestContext.Current.CancellationToken); - fullPermissionResponse.StatusCode.Should().Be(scenario == "no_sensitive_data" ? HttpStatusCode.Forbidden : steeltoeStatusCode); + HttpResponseMessage authorizationResponse = await client.SendAsync(authorizationRequest, TestContext.Current.CancellationToken); + authorizationResponse.StatusCode.Should().Be(scenario == "restricted-permissions" ? HttpStatusCode.Forbidden : steeltoeStatusCode); string logLines = loggerProvider.GetAsText(); logLines.Should().ContainAll(expectedLogs); diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs index 133ee605a7..4ab86ca1be 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundrySecurityMiddlewareTestScenarios.cs @@ -10,81 +10,95 @@ namespace Steeltoe.Management.Endpoint.Test.Actuators.CloudFoundry; internal sealed class CloudFoundrySecurityMiddlewareTestScenarios : TheoryData { - private const string SuccessLog = - "INFO System.Net.Http.HttpClient.CloudFoundrySecurity.ClientHandler: Sending HTTP request GET https://example.api.com/v2/apps/success/permissions"; + private const string FullPermissionsLog = + "INFO System.Net.Http.HttpClient.CloudFoundrySecurity.ClientHandler: Sending HTTP request GET https://example.api.com/v2/apps/full-permissions/permissions"; - private readonly string _permissionsCheckForbiddenLog = - $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status: Forbidden while obtaining permissions from: https://example.api.com/v2/apps/forbidden/permissions"; + private static readonly string PermissionsCheckForbiddenLog = + $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status Forbidden while obtaining permissions from https://example.api.com/v2/apps/forbidden/permissions."; - private readonly string _permissionsCheckUnauthorizedLog = - $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status: Unauthorized while obtaining permissions from: https://example.api.com/v2/apps/unauthorized/permissions"; + private static readonly string PermissionsCheckUnauthorizedLog = + $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status Unauthorized while obtaining permissions from https://example.api.com/v2/apps/unauthorized/permissions."; - private readonly string _permissionsCheckNotFoundLog = - $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status: NotFound while obtaining permissions from: https://example.api.com/v2/apps/not-found/permissions"; + private static readonly string PermissionsCheckNotFoundLog = + $"INFO {typeof(PermissionsProvider)}: Cloud Foundry returned status NotFound while obtaining permissions from https://example.api.com/v2/apps/not-found/permissions."; - private readonly string _middlewareForbiddenLog = - $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator Security Error: Forbidden - {Messages.AccessDenied}"; + private static readonly string MiddlewareForbiddenLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status Forbidden: '{Messages.AccessDenied}'."; - private readonly string _middlewareExceptionLog = - $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator Security Error: ServiceUnavailable - Exception of type 'System.Net.Http.HttpRequestException' with error 'NameResolutionError' was thrown"; + private static readonly string MiddlewareBrokenResponseLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status BadGateway: '{Messages.CloudFoundryBrokenResponse}'."; - private readonly string _middlewareTimeoutLog = - $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator Security Error: ServiceUnavailable - {Messages.CloudFoundryTimeout}"; + private static readonly string MiddlewareExceptionLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status ServiceUnavailable: " + + $"'Exception of type '{typeof(HttpRequestException)}' with error '{nameof(HttpRequestError.NameResolutionError)}' was thrown'."; - private readonly string _middlewareUnauthorizedLog = - $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator Security Error: Unauthorized - {Messages.InvalidToken}"; + private static readonly string MiddlewareTimeoutLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status ServiceUnavailable: '{Messages.CloudFoundryTimeout}'."; - private readonly string _middlewareUnavailableLog = - $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator Security Error: ServiceUnavailable - {Messages.CloudFoundryNotReachable}"; + private static readonly string MiddlewareUnauthorizedLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status Unauthorized: '{Messages.InvalidToken}'."; + + private static readonly string MiddlewareUnavailableLog = + $"FAIL {typeof(CloudFoundrySecurityMiddleware)}: Actuator security error with status ServiceUnavailable: '{Messages.CloudFoundryNotReachable}'."; public CloudFoundrySecurityMiddlewareTestScenarios() { Add("exception", HttpStatusCode.ServiceUnavailable, - "Exception of type 'System.Net.Http.HttpRequestException' with error 'NameResolutionError' was thrown", [_middlewareExceptionLog], true); + $"Exception of type '{typeof(HttpRequestException)}' with error '{nameof(HttpRequestError.NameResolutionError)}' was thrown", + [MiddlewareExceptionLog], true); - Add("exception", HttpStatusCode.OK, "Exception of type 'System.Net.Http.HttpRequestException' with error 'NameResolutionError' was thrown", - [_middlewareExceptionLog], false); + Add("exception", HttpStatusCode.OK, + $"Exception of type '{typeof(HttpRequestException)}' with error '{nameof(HttpRequestError.NameResolutionError)}' was thrown", + [MiddlewareExceptionLog], false); Add("forbidden", HttpStatusCode.Forbidden, Messages.AccessDenied, [ - _permissionsCheckForbiddenLog, - _middlewareForbiddenLog + PermissionsCheckForbiddenLog, + MiddlewareForbiddenLog ], true); Add("forbidden", HttpStatusCode.Forbidden, Messages.AccessDenied, [ - _permissionsCheckForbiddenLog, - _middlewareForbiddenLog + PermissionsCheckForbiddenLog, + MiddlewareForbiddenLog ], false); - Add("no_sensitive_data", HttpStatusCode.OK, null, [_middlewareForbiddenLog], true); + Add("broken-response", HttpStatusCode.BadGateway, Messages.CloudFoundryBrokenResponse, [MiddlewareBrokenResponseLog], true); + + Add("broken-response", HttpStatusCode.OK, Messages.CloudFoundryBrokenResponse, [MiddlewareBrokenResponseLog], false); + + Add("no-permissions", HttpStatusCode.Forbidden, Messages.AccessDenied, [MiddlewareForbiddenLog], true); + + Add("no-permissions", HttpStatusCode.Forbidden, Messages.AccessDenied, [MiddlewareForbiddenLog], false); + + Add("restricted-permissions", HttpStatusCode.OK, null, [MiddlewareForbiddenLog], true); + + Add("full-permissions", HttpStatusCode.OK, null, [FullPermissionsLog], true); Add("not-found", HttpStatusCode.Unauthorized, Messages.InvalidToken, [ - _permissionsCheckNotFoundLog, - _middlewareUnauthorizedLog + PermissionsCheckNotFoundLog, + MiddlewareUnauthorizedLog ], true); Add("not-found", HttpStatusCode.Unauthorized, Messages.InvalidToken, [ - _permissionsCheckNotFoundLog, - _middlewareUnauthorizedLog + PermissionsCheckNotFoundLog, + MiddlewareUnauthorizedLog ], false); - Add("success", HttpStatusCode.OK, null, [SuccessLog], true); - - Add("timeout", HttpStatusCode.ServiceUnavailable, Messages.CloudFoundryTimeout, [_middlewareTimeoutLog], true); + Add("timeout", HttpStatusCode.ServiceUnavailable, Messages.CloudFoundryTimeout, [MiddlewareTimeoutLog], true); - Add("timeout", HttpStatusCode.OK, Messages.CloudFoundryTimeout, [_middlewareTimeoutLog], false); + Add("timeout", HttpStatusCode.OK, Messages.CloudFoundryTimeout, [MiddlewareTimeoutLog], false); Add("unauthorized", HttpStatusCode.Unauthorized, Messages.InvalidToken, [ - _permissionsCheckUnauthorizedLog, - _middlewareUnauthorizedLog + PermissionsCheckUnauthorizedLog, + MiddlewareUnauthorizedLog ], true); Add("unauthorized", HttpStatusCode.Unauthorized, Messages.InvalidToken, [ - _permissionsCheckUnauthorizedLog, - _middlewareUnauthorizedLog + PermissionsCheckUnauthorizedLog, + MiddlewareUnauthorizedLog ], false); - Add("unavailable", HttpStatusCode.ServiceUnavailable, Messages.CloudFoundryNotReachable, [_middlewareUnavailableLog], true); + Add("unavailable", HttpStatusCode.ServiceUnavailable, Messages.CloudFoundryNotReachable, [MiddlewareUnavailableLog], true); - Add("unavailable", HttpStatusCode.OK, Messages.CloudFoundryNotReachable, [_middlewareUnavailableLog], false); + Add("unavailable", HttpStatusCode.OK, Messages.CloudFoundryNotReachable, [MiddlewareUnavailableLog], false); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/PermissionsProviderTest.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/PermissionsProviderTest.cs index 288885eb46..8b5c55ffe7 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/PermissionsProviderTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/PermissionsProviderTest.cs @@ -33,7 +33,9 @@ public async Task EmptyTokenIsUnauthorized() } [Theory] + [InlineData(false, false, EndpointPermissions.None)] [InlineData(false, true, EndpointPermissions.Restricted)] + [InlineData(true, false, EndpointPermissions.None)] [InlineData(true, true, EndpointPermissions.Full)] public async Task ParsePermissionsResponseAsyncReturnsExpected(bool readSensitive, bool readBasic, EndpointPermissions expectedPermissions) { @@ -48,7 +50,7 @@ public async Task ParsePermissionsResponseAsyncReturnsExpected(bool readSensitiv }) }; - EndpointPermissions result = await permissionsProvider.ParsePermissionsResponseAsync(cloudControllerResponse, TestContext.Current.CancellationToken); + EndpointPermissions? result = await permissionsProvider.ParsePermissionsResponseAsync(cloudControllerResponse, TestContext.Current.CancellationToken); result.Should().Be(expectedPermissions); } @@ -59,9 +61,11 @@ public async Task ParsePermissionsResponseAsyncReturnsExpected(bool readSensitiv [InlineData("forbidden", HttpStatusCode.Forbidden, PermissionsProvider.Messages.AccessDenied)] [InlineData("timeout", HttpStatusCode.ServiceUnavailable, PermissionsProvider.Messages.CloudFoundryTimeout)] [InlineData("exception", HttpStatusCode.ServiceUnavailable, - "Exception of type 'System.Net.Http.HttpRequestException' with error 'NameResolutionError' was thrown")] - [InlineData("no_sensitive_data", HttpStatusCode.OK, "")] - [InlineData("success", HttpStatusCode.OK, "")] + $"Exception of type 'System.Net.Http.HttpRequestException' with error '{nameof(HttpRequestError.NameResolutionError)}' was thrown")] + [InlineData("broken-response", HttpStatusCode.BadGateway, PermissionsProvider.Messages.CloudFoundryBrokenResponse)] + [InlineData("no-permissions", HttpStatusCode.OK, "")] + [InlineData("restricted-permissions", HttpStatusCode.OK, "")] + [InlineData("full-permissions", HttpStatusCode.OK, "")] public async Task Returns_expected_response_on_permission_check(string scenario, HttpStatusCode? steeltoeStatusCode, string errorMessage) { var appSettings = new Dictionary @@ -87,10 +91,10 @@ public async Task Returns_expected_response_on_permission_check(string scenario, switch (scenario) { - case "success": + case "full-permissions": result.Permissions.Should().Be(EndpointPermissions.Full); break; - case "no_sensitive_data": + case "restricted-permissions": result.Permissions.Should().Be(EndpointPermissions.Restricted); break; default: diff --git a/src/Management/test/Endpoint.Test/Actuators/DbMigrations/DbMigrationsActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/DbMigrations/DbMigrationsActuatorTest.cs index 864d6d45bb..491af47e67 100644 --- a/src/Management/test/Endpoint.Test/Actuators/DbMigrations/DbMigrationsActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/DbMigrations/DbMigrationsActuatorTest.cs @@ -190,6 +190,6 @@ public async Task Endpoint_returns_all_migrations_when_pending_migrations_are_un """); IList logLines = loggerProvider.GetAll(); - logLines.Should().Contain($"WARN {typeof(DbMigrationsEndpointHandler).FullName}: Failed to load pending/applied migrations, returning all migrations."); + logLines.Should().Contain($"WARN {typeof(DbMigrationsEndpointHandler)}: Failed to load pending/applied migrations, returning all migrations."); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs index 13fe635f8b..4bfff41e06 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs @@ -48,11 +48,11 @@ public async Task Configures_default_settings() EnvironmentEndpointOptions options = serviceProvider.GetRequiredService>().Value; - options.KeysToSanitize.Should().BeEquivalentTo("password", "secret", "key", "token", ".*credentials.*", "vcap_services"); + options.KeysToSanitize.Should().BeEquivalentTo("password", "secret", "key", "token", ".*credentials.*", "vcap_services", ".*connectionstring.*"); options.Enabled.Should().BeNull(); options.Id.Should().Be("env"); options.Path.Should().Be("env"); - options.RequiredPermissions.Should().Be(EndpointPermissions.Restricted); + options.RequiredPermissions.Should().Be(EndpointPermissions.Full); options.GetSafeAllowedVerbs().Should().ContainSingle().Subject.Should().Be("GET"); options.RequiresExactMatch().Should().BeTrue(); @@ -149,10 +149,13 @@ public async Task Endpoint_returns_expected_data(HostBuilderType hostBuilderType { var appSettings = new Dictionary(AppSettings) { + ["App:ShippingDatabaseQueue"] = "amqp://user:pass@shipping.db.com", + ["ConnectionStrings:Default"] = "Server=db;User Id=app;Password=secret123;", + ["Do:Not:Show:This:Secret"] = "hidden-in-response", ["Logging:LogLevel:Default"] = "Warning", ["Logging:LogLevel:Steeltoe"] = "Information", ["Logging:LogLevel:TestApp"] = "Error", - ["Do:Not:Show:This:Secret"] = "hidden-in-response" + ["OrderDbConnection"] = "Server=orders.db.com;Database=orders;Pwd=order-pass;Uid=order-user" }; await using HostWrapper host = hostBuilderType.Build(builder => @@ -187,6 +190,12 @@ public async Task Endpoint_returns_expected_data(HostBuilderType hostBuilderType { "name": "MemoryConfigurationProvider", "properties": { + "App:ShippingDatabaseQueue": { + "value": "amqp://user:******@shipping.db.com" + }, + "ConnectionStrings:Default": { + "value": "******" + }, "Do:Not:Show:This:Secret": { "value": "******" }, @@ -201,6 +210,9 @@ public async Task Endpoint_returns_expected_data(HostBuilderType hostBuilderType }, "Management:Endpoints:Actuator:Exposure:Include:0": { "value": "env" + }, + "OrderDbConnection": { + "value": "Server=orders.db.com;Database=orders;Pwd=******;Uid=order-user" } } } @@ -323,34 +335,34 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "Password" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "Password" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.Sources.Clear(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddEnvironmentActuator(); await using WebApplication host = builder.Build(); @@ -390,30 +402,30 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "AccessToken" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "AccessToken" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Environment/SanitizerTest.cs b/src/Management/test/Endpoint.Test/Actuators/Environment/SanitizerTest.cs new file mode 100644 index 0000000000..9767687908 --- /dev/null +++ b/src/Management/test/Endpoint.Test/Actuators/Environment/SanitizerTest.cs @@ -0,0 +1,82 @@ +// 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.Management.Endpoint.Actuators.Environment; + +namespace Steeltoe.Management.Endpoint.Test.Actuators.Environment; + +public sealed class SanitizerTest +{ + [Theory] + [InlineData(null, null)] + [InlineData("normalValue", "normalValue")] + [InlineData("amqp://127.0.0.1", "amqp://127.0.0.1")] + [InlineData("https://host:8080/path", "https://host:8080/path")] + [InlineData("http://user@host", "http://user@host")] + public void Sanitize_Does_Not_Mask_Non_Sensitive_Key_Value(string? value, string? expected) + { + var sanitizer = new Sanitizer([ + "password", + "secret" + ]); + + sanitizer.Sanitize("someKey", value).Should().Be(expected); + } + + [Theory] + [InlineData("somePassword", null)] + [InlineData("mySecret", null)] + [InlineData("somePassword", "mysecretvalue")] + [InlineData("mySecret", "anothervalue")] + public void Sanitize_Fully_Masks_Sensitive_Key_Value(string key, string? value) + { + var sanitizer = new Sanitizer([ + "password", + "secret" + ]); + + sanitizer.Sanitize(key, value).Should().Be("******"); + } + + [Theory] + [InlineData("ConnectionStrings:OrderDb", "Server=orders.db.com;Pwd=order-pass;Uid=order-user")] + [InlineData("vcap:services:p.rabbitmq:0:credentials:uri", "amqp://user:pass@127.0.0.1/instance")] + public void Sanitize_Fully_Masks_Sensitive_Key_Value_Even_When_Value_Contains_Credentials(string key, string value) + { + var sanitizer = new Sanitizer([ + ".*connectionstring.*", + ".*credentials.*" + ]); + + sanitizer.Sanitize(key, value).Should().Be("******"); + } + + [Theory] + [InlineData("host=localhost;password=secret;port=1", "host=localhost;password=******;port=1")] + [InlineData("host=localhost;pwd=secret;port=1", "host=localhost;pwd=******;port=1")] + [InlineData("password=secret;host=localhost;port=1", "password=******;host=localhost;port=1")] + [InlineData("host=localhost;port=1;password=secret", "host=localhost;port=1;password=******")] + [InlineData("host=localhost;notapassword=secret;port=1", "host=localhost;notapassword=secret;port=1")] + [InlineData("PWD=secret;Password=other", "PWD=******;Password=******")] + [InlineData("password=secret", "password=******")] + [InlineData("host=1; password = abc ;port=2", "host=1; password = ******;port=2")] + [InlineData("", "")] + [InlineData("host=localhost;notapassword=secret;password=real;port=1", "host=localhost;notapassword=secret;password=******;port=1")] + [InlineData("host=localhost;password=;port=1", "host=localhost;password=;port=1")] + [InlineData("password=;host=localhost", "password=;host=localhost")] + [InlineData("host=localhost;port=1;password=", "host=localhost;port=1;password=")] + [InlineData("host=localhost;pwd=;password=;port=1", "host=localhost;pwd=;password=;port=1")] + [InlineData("host=localhost;password=;password=secret;port=1", "host=localhost;password=;password=******;port=1")] + [InlineData("amqp://user:pass@127.0.0.1", "amqp://user:******@127.0.0.1")] + [InlineData("https://user:pass@host:8080/path", "https://user:******@host:8080/path")] + [InlineData("ftp://user:pass@host", "ftp://user:******@host")] + [InlineData("http://:password@127.0.0.1", "http://:******@127.0.0.1")] + [InlineData("http://user1:pass1@127.0.0.1,https://user2:pass2@host2", "http://user1:******@127.0.0.1,https://user2:******@host2")] + public void Sanitize_Masks_Passwords_Within_Uri_And_Connection_String_Values(string input, string expected) + { + var sanitizer = new Sanitizer([]); + + sanitizer.Sanitize("someKey", input).Should().Be(expected); + } +} diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/Availability/ApplicationAvailabilityTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/Availability/ApplicationAvailabilityTest.cs index ad6a8e5872..4a01115076 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/Availability/ApplicationAvailabilityTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/Availability/ApplicationAvailabilityTest.cs @@ -20,9 +20,9 @@ public void TracksAndReturnsState() { var availability = new ApplicationAvailability(NullLogger.Instance); - availability.SetAvailabilityState("Test", LivenessState.Broken, GetType().Name); - availability.SetAvailabilityState(ApplicationAvailability.LivenessKey, LivenessState.Correct, GetType().Name); - availability.SetAvailabilityState(ApplicationAvailability.ReadinessKey, ReadinessState.AcceptingTraffic, GetType().Name); + availability.SetAvailabilityState("Test", LivenessState.Broken, nameof(ApplicationAvailabilityTest)); + availability.SetAvailabilityState(ApplicationAvailability.LivenessKey, LivenessState.Correct, nameof(ApplicationAvailabilityTest)); + availability.SetAvailabilityState(ApplicationAvailability.ReadinessKey, ReadinessState.AcceptingTraffic, nameof(ApplicationAvailabilityTest)); availability.GetAvailabilityState("Test").Should().Be(LivenessState.Broken); availability.GetLivenessState().Should().Be(LivenessState.Correct); diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs index 2310840bea..4a5e408466 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs @@ -578,26 +578,26 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping", - "ShowComponents": "Always" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping", + "ShowComponents": "Always" + } + } } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHealthActuator(); WebApplication host = builder.Build(); @@ -622,21 +622,21 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping" + } + } } } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs index 5717db1942..172f589c1c 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs @@ -7,6 +7,7 @@ using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -365,9 +366,9 @@ public async Task Aggregates_contributors_in_parallel() { List contributors = [ - new SlowContributor(1.Seconds()), new SlowContributor(2.Seconds()), - new SlowContributor(3.Seconds()) + new SlowContributor(3.Seconds()), + new SlowContributor(4.Seconds()) ]; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); @@ -407,7 +408,8 @@ public async Task Aggregates_contributors_in_parallel() } """); - stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(5.Seconds()); + // Upper bound must be less than 2+3+4=9s if contributors ran sequentially. + stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(9.Seconds()); } [Fact] @@ -516,6 +518,97 @@ public async Task Converts_AspNet_health_check_results() """); } + [Fact] + public async Task Can_skip_AspNet_health_check() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(AppSettings); + builder.Services.AddHealthActuator(); + + IHealthChecksBuilder checksBuilder = builder.Services.AddHealthChecks(); + checksBuilder.AddCheck("aspnet-unhealthy-check", tags: ["ExcludeFromHealthActuator"]); + checksBuilder.AddCheck("aspnet-healthy-check"); + + await using WebApplication host = builder.Build(); + + host.MapHealthChecks("/health"); + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage actuatorResponse = await httpClient.GetAsync(new Uri("http://localhost/actuator/health"), TestContext.Current.CancellationToken); + + actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + string actuatorResponseBody = await actuatorResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + actuatorResponseBody.Should().BeJson(""" + { + "status": "UP", + "components": { + "aspnet-healthy-check": { + "status": "UP", + "description": "healthy-description", + "details": { + "healthy-data-key": "healthy-data-value" + } + } + } + } + """); + + HttpResponseMessage aspNetResponse = await httpClient.GetAsync(new Uri("http://localhost/health"), TestContext.Current.CancellationToken); + + aspNetResponse.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); + + string aspNetResponseBody = await aspNetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + aspNetResponseBody.Should().Be("Unhealthy"); + } + + [Fact] + public async Task Can_use_scoped_AspNet_health_check() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(AppSettings); + builder.Services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); + builder.Services.AddHealthChecks().AddDbContextCheck(); + builder.Services.AddHealthActuator(); + await using WebApplication host = builder.Build(); + + // ReSharper disable once AccessToDisposedClosure + Action action = () => host.Services.GetRequiredService(); + action.Should().ThrowExactly(); + + host.MapHealthChecks("/health"); + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage actuatorResponse = await httpClient.GetAsync(new Uri("http://localhost/actuator/health"), TestContext.Current.CancellationToken); + + actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + string actuatorResponseBody = await actuatorResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + actuatorResponseBody.Should().BeJson(""" + { + "status": "UP", + "components": { + "TestDbContext": { + "status": "UP" + } + } + } + """); + + HttpResponseMessage aspNetResponse = await httpClient.GetAsync(new Uri("http://localhost/health"), TestContext.Current.CancellationToken); + + aspNetResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + string aspNetResponseBody = await aspNetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + aspNetResponseBody.Should().Be("Healthy"); + } + private sealed class AspNetHealthyCheck : IHealthCheck { public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) @@ -562,4 +655,7 @@ public Task CheckHealthAsync(HealthCheckContext cont throw new InvalidOperationException("test-exception"); } } + + private sealed class TestDbContext(DbContextOptions options) + : DbContext(options); } diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/TestContributors/ComplexDetailsContributor.cs b/src/Management/test/Endpoint.Test/Actuators/Health/TestContributors/ComplexDetailsContributor.cs index b23a8cd54c..6017635ba5 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/TestContributors/ComplexDetailsContributor.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/TestContributors/ComplexDetailsContributor.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.Text.Json.Serialization; using Steeltoe.Common.HealthChecks; namespace Steeltoe.Management.Endpoint.Test.Actuators.Health.TestContributors; @@ -38,12 +39,23 @@ internal sealed class ComplexDetailsContributor : IHealthContributor private sealed class TestHealthDetails { + // ReSharper disable UnusedAutoPropertyAccessor.Local + [JsonPropertyName("testString")] public string TestString { get; set; } = "test-string"; + + [JsonPropertyName("testInteger")] public int TestInteger { get; set; } = 123; + + [JsonPropertyName("testFloatingPoint")] public double TestFloatingPoint { get; set; } = 1.23; + + [JsonPropertyName("testBoolean")] public bool TestBoolean { get; set; } = true; + + [JsonPropertyName("nestedComplexType")] public TestHealthDetails? NestedComplexType { get; set; } + [JsonPropertyName("testList")] public List TestList { get; set; } = [ "A", @@ -51,11 +63,13 @@ private sealed class TestHealthDetails "C" ]; + [JsonPropertyName("testDictionary")] public Dictionary TestDictionary { get; set; } = new() { ["One"] = 1, ["Two"] = 2, ["Three"] = 3 }; + // ReSharper restore UnusedAutoPropertyAccessor.Local } } diff --git a/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumpActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumpActuatorTest.cs index da6de1a61d..988ba89e02 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumpActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumpActuatorTest.cs @@ -50,7 +50,7 @@ public async Task Configures_default_settings() options.Enabled.Should().BeNull(); options.Id.Should().Be("heapdump"); options.Path.Should().Be("heapdump"); - options.RequiredPermissions.Should().Be(EndpointPermissions.Restricted); + options.RequiredPermissions.Should().Be(EndpointPermissions.Full); options.GetSafeAllowedVerbs().Should().ContainSingle().Subject.Should().Be("GET"); options.RequiresExactMatch().Should().BeTrue(); diff --git a/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumperTest.cs b/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumperTest.cs index bde13d99b7..65b94240cf 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumperTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HeapDump/HeapDumperTest.cs @@ -10,11 +10,12 @@ namespace Steeltoe.Management.Endpoint.Test.Actuators.HeapDump; +[Collection("TestsForMemoryDumpsMustRunSequentially")] +[Trait("Category", "MemoryDumps")] public sealed class HeapDumperTest { private static readonly TimeSpan DumpTimeout = TimeSpan.FromMinutes(3); - [Trait("Category", "MemoryDumps")] [Theory] [InlineData(HeapDumpType.Full, "fulldump_", "full dump")] [InlineData(HeapDumpType.Heap, "heapdump_", "dump with heap")] @@ -50,14 +51,14 @@ public async Task Can_create_heap_dump(HeapDumpType heapDumpType, string fileNam File.Delete(path); IList logLines = loggerProvider.GetAll(); - logLines.Should().Contain($"INFO {typeof(HeapDumper).FullName}: Attempting to create a {description}."); - logLines.Should().Contain($"INFO {typeof(HeapDumper).FullName}: Successfully created a {description}."); + logLines.Should().Contain($"INFO {typeof(HeapDumper)}: Attempting to create a {description}."); + logLines.Should().Contain($"INFO {typeof(HeapDumper)}: Successfully created a {description}."); if (heapDumpType == HeapDumpType.GCDump) { string logText = loggerProvider.GetAsText(); - logText.Should().Contain($"TRCE {typeof(HeapDumper).FullName}: Captured log from gcdump:"); + logText.Should().Contain($"TRCE {typeof(HeapDumper)}: Captured log from gcdump:"); logText.Should().Contain("Done Dumping .NET heap success=True"); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/DiagnosticObserverHttpExchangeRecorderTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/DiagnosticObserverHttpExchangeRecorderTest.cs index 2892b85934..47b82fefff 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/DiagnosticObserverHttpExchangeRecorderTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/DiagnosticObserverHttpExchangeRecorderTest.cs @@ -17,6 +17,7 @@ namespace Steeltoe.Management.Endpoint.Test.Actuators.HttpExchanges; +[Collection("TestsForMemoryDumpsMustRunSequentially")] [Trait("Category", "MemoryDumps")] public sealed class DiagnosticObserverHttpExchangeRecorderTest { @@ -51,6 +52,7 @@ public async Task Records_http_requests() host.MapGet("/hello", () => "Hello World!"); await host.StartAsync(TestContext.Current.CancellationToken); + // ReSharper disable once ShortLivedHttpClient using var httpClient = new HttpClient(); HttpResponseMessage helloResponse = diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs index 25f49eb277..b28ed28eb7 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs @@ -183,7 +183,7 @@ public async Task Endpoint_returns_expected_data_without_filters(HostBuilderType }, "request": { "method": "GET", - "uri": "http://api.test.com:8080/path/to/data?filter=A", + "uri": "http://****:****@api.test.com:8080/path/to/data?filter=A", "headers": { "Accept": [ "application/json" @@ -250,7 +250,7 @@ public async Task Endpoint_returns_expected_data_with_filters() "timestamp": "2025-01-01T21:18:43Z", "request": { "method": "GET", - "uri": "http://api.test.com:8080/" + "uri": "http://****:****@api.test.com:8080/" }, "response": { "status": 200 @@ -301,7 +301,7 @@ public async Task Configured_header_names_are_case_insensitive() "timestamp": "2025-01-01T21:18:43Z", "request": { "method": "POST", - "uri": "http://localhost:80/", + "uri": "http://localhost/", "headers": { "X-Whitelisted-Request-Header": [ "visible-request-header-value" @@ -366,7 +366,7 @@ public async Task Respects_maximum_queue_capacity() "timestamp": "2024-09-19T00:00:25", "request": { "method": "GET", - "uri": "http://localhost:80/id/25" + "uri": "http://localhost/id/25" }, "response": { "status": 200 @@ -376,7 +376,7 @@ public async Task Respects_maximum_queue_capacity() "timestamp": "2024-09-19T00:00:24", "request": { "method": "GET", - "uri": "http://localhost:80/id/24" + "uri": "http://localhost/id/24" }, "response": { "status": 200 @@ -386,7 +386,7 @@ public async Task Respects_maximum_queue_capacity() "timestamp": "2024-09-19T00:00:23", "request": { "method": "GET", - "uri": "http://localhost:80/id/23" + "uri": "http://localhost/id/23" }, "response": { "status": 200 @@ -396,7 +396,7 @@ public async Task Respects_maximum_queue_capacity() "timestamp": "2024-09-19T00:00:22", "request": { "method": "GET", - "uri": "http://localhost:80/id/22" + "uri": "http://localhost/id/22" }, "response": { "status": 200 @@ -406,7 +406,7 @@ public async Task Respects_maximum_queue_capacity() "timestamp": "2024-09-19T00:00:21", "request": { "method": "GET", - "uri": "http://localhost:80/id/21" + "uri": "http://localhost/id/21" }, "response": { "status": 200 @@ -422,17 +422,17 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "IncludeQueryString": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "IncludeQueryString": false + } + } } } - } - } - """); + """); DateTime currentTime = 19.September(2024); @@ -446,7 +446,7 @@ public async Task Can_change_configuration_at_runtime() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(new FakeHttpExchangeRecorder(httpExchanges)); builder.Services.AddHttpExchangesActuator(); await using WebApplication host = builder.Build(); @@ -467,7 +467,7 @@ public async Task Can_change_configuration_at_runtime() "timestamp": "2024-09-19T00:00:02", "request": { "method": "GET", - "uri": "http://localhost:80/id/2" + "uri": "http://localhost/id/2" }, "response": { "status": 200 @@ -477,7 +477,7 @@ public async Task Can_change_configuration_at_runtime() "timestamp": "2024-09-19T00:00:01", "request": { "method": "GET", - "uri": "http://localhost:80/id/1" + "uri": "http://localhost/id/1" }, "response": { "status": 200 @@ -487,17 +487,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "Reverse": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "Reverse": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); @@ -514,7 +514,7 @@ public async Task Can_change_configuration_at_runtime() "timestamp": "2024-09-19T00:00:01", "request": { "method": "GET", - "uri": "http://localhost:80/id/1?q=test-query-string" + "uri": "http://localhost/id/1?q=test-query-string" }, "response": { "status": 200 @@ -524,7 +524,7 @@ public async Task Can_change_configuration_at_runtime() "timestamp": "2024-09-19T00:00:02", "request": { "method": "GET", - "uri": "http://localhost:80/id/2?q=test-query-string" + "uri": "http://localhost/id/2?q=test-query-string" }, "response": { "status": 200 @@ -549,7 +549,7 @@ private static HttpExchange CreateTestHttpExchange() ["X-Redacted-Response-Header"] = "Redact-Me" }; - var request = new HttpExchangeRequest("GET", new Uri("http://api.test.com:8080/path/to/data?filter=A"), requestHeaders, "192.168.0.1"); + var request = new HttpExchangeRequest("GET", new Uri("http://johndoe:secret@api.test.com:8080/path/to/data?filter=A"), requestHeaders, "192.168.0.1"); var response = new HttpExchangeResponse((int)HttpStatusCode.OK, responseHeaders); return new HttpExchange(request, response, 1.January(2025).At(21, 18, 43).AsUtc(), new HttpExchangePrincipal("test-user"), diff --git a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs index 91d1a37f33..7325d9b4b1 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs @@ -111,7 +111,7 @@ public async Task Endpoint_returns_expected_data_with_all_actuators_registered(H "_links": { "health": { "href": "http://localhost/actuator/health", - "templated": true + "templated": false }, "info": { "href": "http://localhost/actuator/info", @@ -196,7 +196,7 @@ public async Task Can_use_alternate_IDs_and_paths() }, "loggers": { "href": "http://localhost/alt-actuator/alt-loggers-path", - "templated": true + "templated": false }, "self": { "href": "http://localhost/alt-actuator/hypermedia", @@ -327,7 +327,7 @@ public async Task Logs_warning_when_duplicate_endpoint_ID_detected() response.StatusCode.Should().Be(HttpStatusCode.OK); IList logLines = loggerProvider.GetAll(); - logLines.Should().ContainSingle().Which.Should().Be($"WARN {typeof(HypermediaService).FullName}: Duplicate endpoint with ID 'same' detected."); + logLines.Should().ContainSingle().Which.Should().Be($"WARN {typeof(HypermediaService)}: Duplicate endpoint with ID 'same' detected."); } [Fact] @@ -370,6 +370,44 @@ public async Task Can_use_slash_as_management_path() """); } + [Fact] + public async Task Includes_base_path_from_UsePathBase_in_hypermedia_links() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Services.AddHypermediaActuator(false); + builder.Services.AddInfoActuator(false); + await using WebApplication host = builder.Build(); + + host.UsePathBase("/some/prefix"); + host.UseRouting(); + host.UseActuatorEndpoints(); + + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage response = await httpClient.GetAsync(new Uri("http://localhost/some/prefix/actuator"), TestContext.Current.CancellationToken); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + string responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + responseBody.Should().BeJson(""" + { + "type": "steeltoe", + "_links": { + "info": { + "href": "http://localhost/some/prefix/actuator/info", + "templated": false + }, + "self": { + "href": "http://localhost/some/prefix/actuator", + "templated": false + } + } + } + """); + } + [Theory] [InlineData("http://somehost:1234", "https://somehost:1234", "https")] [InlineData("http://somehost:443", "https://somehost", "https")] @@ -414,20 +452,20 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHypermediaActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -448,7 +486,7 @@ public async Task Can_change_configuration_at_runtime() "_links": { "health": { "href": "http://localhost/actuator/health", - "templated": true + "templated": false }, "self": { "href": "http://localhost/actuator", @@ -458,17 +496,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Info/InfoActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Info/InfoActuatorTest.cs index 56350212d3..7d7756a273 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Info/InfoActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Info/InfoActuatorTest.cs @@ -4,6 +4,7 @@ using System.Net; using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; @@ -33,6 +34,13 @@ public sealed class InfoActuatorTest private static readonly Assembly SteeltoeAssembly = typeof(IInfoContributor).Assembly; private static readonly string SteeltoeFileVersion = SteeltoeAssembly.GetCustomAttribute()!.Version; private static readonly string SteeltoeProductVersion = SteeltoeAssembly.GetCustomAttribute()!.InformationalVersion; + private static readonly string RuntimeName = RuntimeInformation.FrameworkDescription; + private static readonly string RuntimeVersion = System.Environment.Version.ToString(); + private static readonly string RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier; + private static readonly string ProcessArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + private static readonly string OSArchitecture = RuntimeInformation.OSArchitecture.ToString(); + private static readonly string OSDescription = RuntimeInformation.OSDescription; + private static readonly string OSVersion = System.Environment.OSVersion.ToString(); [Fact] public async Task Registers_dependent_services() @@ -184,6 +192,15 @@ public async Task Endpoint_returns_expected_data(HostBuilderType hostBuilderType }, "build": { "version": "{{AppAssemblyVersion}}" + }, + "runtime": { + "runtimeName": "{{RuntimeName}}", + "runtimeVersion": "{{RuntimeVersion}}", + "runtimeIdentifier": "{{RuntimeIdentifier}}", + "processArchitecture": "{{ProcessArchitecture}}", + "osArchitecture": "{{OSArchitecture}}", + "osDescription": "{{OSDescription}}", + "osVersion": "{{OSVersion}}" } } """); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs index 02ccd12aca..d7d800c214 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs @@ -214,11 +214,8 @@ public async Task Can_change_minimum_levels_with_serilog() } """); - HttpResponseMessage resetResponse = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake.Category"), new StringContent(""" - { - "configuredLevel": null - } - """, RequestContentType), TestContext.Current.CancellationToken); + HttpResponseMessage resetResponse = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake.Category"), + new StringContent("{}", RequestContentType), TestContext.Current.CancellationToken); resetResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); (await resetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)).Should().BeEmpty(); @@ -275,23 +272,23 @@ public async Task Can_change_serilog_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Error", - "Override": { - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Error", + "Override": { + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Logging.AddDynamicSerilog(); builder.Services.AddLoggersActuator(); @@ -345,19 +342,19 @@ public async Task Can_change_serilog_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs index c5d72935cb..0e28b20870 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs @@ -289,11 +289,8 @@ public async Task Can_change_minimum_levels() } """); - HttpResponseMessage resetResponse = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake.Category"), new StringContent(""" - { - "configuredLevel": null - } - """, RequestContentType), TestContext.Current.CancellationToken); + HttpResponseMessage resetResponse = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake.Category"), + new StringContent("{}", RequestContentType), TestContext.Current.CancellationToken); resetResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); (await resetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)).Should().BeEmpty(); @@ -345,6 +342,112 @@ public async Task Can_change_minimum_levels() """); } + [Fact] + public async Task Can_reset_minimum_level_by_sending_configuredLevel_null() + { + var appSettings = new Dictionary(AppSettings) + { + ["Logging:LogLevel:Default"] = "Error", + ["Logging:LogLevel:Fake"] = "Debug" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(appSettings); + EnsureLoggingConfigurationIsBound(builder.Logging, builder.Configuration); + builder.Services.AddSingleton(); + builder.Services.AddLoggersActuator(); + await using WebApplication host = builder.Build(); + + using var loggerFactory = host.Services.GetRequiredService(); + _ = loggerFactory.CreateLogger("Fake.Some"); + + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage setResponse1 = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake"), new StringContent(""" + { + "configuredLevel": "TRACE" + } + """, RequestContentType), TestContext.Current.CancellationToken); + + setResponse1.StatusCode.Should().Be(HttpStatusCode.NoContent); + (await setResponse1.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)).Should().BeEmpty(); + + HttpResponseMessage getResponse1 = await httpClient.GetAsync(new Uri("http://localhost/actuator/loggers"), TestContext.Current.CancellationToken); + + getResponse1.StatusCode.Should().Be(HttpStatusCode.OK); + + string getResponseBody1 = await getResponse1.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + getResponseBody1.Should().BeJson(""" + { + "levels": [ + "OFF", + "FATAL", + "ERROR", + "WARN", + "INFO", + "DEBUG", + "TRACE" + ], + "loggers": { + "Default": { + "effectiveLevel": "ERROR" + }, + "Fake": { + "configuredLevel": "DEBUG", + "effectiveLevel": "TRACE" + }, + "Fake.Some": { + "effectiveLevel": "TRACE" + } + }, + "groups": {} + } + """); + + HttpResponseMessage resetResponse = await httpClient.PostAsync(new Uri("http://localhost/actuator/loggers/Fake"), new StringContent(""" + { + "configuredLevel": null + } + """, RequestContentType), TestContext.Current.CancellationToken); + + resetResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); + (await resetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)).Should().BeEmpty(); + + HttpResponseMessage getResponse2 = await httpClient.GetAsync(new Uri("http://localhost/actuator/loggers"), TestContext.Current.CancellationToken); + + getResponse2.StatusCode.Should().Be(HttpStatusCode.OK); + + string getResponseBody2 = await getResponse2.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + getResponseBody2.Should().BeJson(""" + { + "levels": [ + "OFF", + "FATAL", + "ERROR", + "WARN", + "INFO", + "DEBUG", + "TRACE" + ], + "loggers": { + "Default": { + "effectiveLevel": "ERROR" + }, + "Fake": { + "effectiveLevel": "DEBUG" + }, + "Fake.Some": { + "effectiveLevel": "DEBUG" + } + }, + "groups": {} + } + """); + } + [Fact] public async Task Fails_on_invalid_request_body() { @@ -485,21 +588,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Error", - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Error", + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); EnsureLoggingConfigurationIsBound(builder.Logging, builder.Configuration); builder.Services.AddSingleton(); builder.Services.AddLoggersActuator(); @@ -553,17 +656,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs index 902db72709..bb571357e7 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(options => options.EnableEndpointRouting = false); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs index 3687723972..80610b9771 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs index 0faf8a75af..7ede0f15bd 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs @@ -200,21 +200,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Refresh": { - "ReturnConfiguration": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Refresh": { + "ReturnConfiguration": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddRefreshActuator(); await using WebApplication host = builder.Build(); @@ -229,10 +229,10 @@ public async Task Can_change_configuration_at_runtime() responseBody1.Should().BeJson("[]"); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/ApiControllerTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/ApiControllerTest.cs index 24036e793d..5d17186e73 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/ApiControllerTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/ApiControllerTest.cs @@ -91,7 +91,7 @@ public async Task Can_get_routes_for_simple_controller() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -208,7 +208,7 @@ public async Task Can_get_routes_for_controller_with_parameters_and_annotations( } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -398,7 +398,7 @@ public async Task Can_get_routes_for_multiple_verbs_in_single_action_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -461,7 +461,7 @@ public async Task Can_get_routes_for_any_verb_in_single_action_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -536,7 +536,7 @@ public async Task Can_get_routes_using_WebHostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -613,6 +613,6 @@ public async Task Can_get_routes_using_HostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MinimalApiTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MinimalApiTest.cs index f4c586a17f..774a7248e1 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MinimalApiTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MinimalApiTest.cs @@ -83,7 +83,7 @@ public async Task Can_get_routes_for_handler_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -145,7 +145,7 @@ public async Task Can_get_routes_for_inline_lambda() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -268,7 +268,7 @@ public async Task Can_get_routes_for_inline_lambda_with_parameters_and_annotatio } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -364,7 +364,7 @@ public async Task Can_get_routes_for_multiple_verbs_in_single_endpoint() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -432,7 +432,7 @@ public async Task Can_get_routes_for_any_verb_in_single_endpoint() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -523,7 +523,7 @@ public async Task Can_get_routes_for_separate_verbs_in_single_endpoint() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -615,7 +615,7 @@ public async Task Can_get_routes_for_groups_using_same_handler_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -720,7 +720,7 @@ public async Task Can_get_routes_for_groups_using_inline_lambdas() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -781,7 +781,7 @@ public async Task Can_get_routes_using_WebHostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -845,7 +845,7 @@ public async Task Can_get_routes_using_HostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } private static string HandlePingRequest() diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MvcControllerTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MvcControllerTest.cs index f261f0b125..f61f0f26ff 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MvcControllerTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/MvcControllerTest.cs @@ -136,7 +136,7 @@ public async Task Can_get_routes_for_simple_controller() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -300,7 +300,7 @@ public async Task Can_get_routes_for_controller_with_parameters_and_annotations( } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -432,7 +432,7 @@ public async Task Can_get_routes_for_multiple_verbs_in_single_action_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -511,7 +511,7 @@ public async Task Can_get_routes_for_any_verb_in_single_action_method() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -630,7 +630,7 @@ public async Task Can_get_routes_using_WebHostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } [Fact] @@ -750,6 +750,6 @@ public async Task Can_get_routes_using_HostBuilder() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/RazorPagesExternalAppTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/RazorPagesExternalAppTest.cs index 83bbcdaa92..8b0f91b08a 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/RazorPagesExternalAppTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/AppTypes/RazorPagesExternalAppTest.cs @@ -3,14 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Net; -using Microsoft.AspNetCore.Mvc.Testing; -using Steeltoe.Management.Endpoint.RazorPagesTestWebApp.Pages; +using Steeltoe.Common.TestResources; namespace Steeltoe.Management.Endpoint.Test.Actuators.RouteMappings.AppTypes; -public sealed class RazorPagesExternalAppTest(WebApplicationFactory factory) : IClassFixture> +public sealed class RazorPagesExternalAppTest(RazorPagesWebApplicationFactory factory) : IClassFixture { - private readonly WebApplicationFactory _factory = factory; + private readonly RazorPagesWebApplicationFactory _factory = factory; [Fact] public async Task Can_get_routes_for_razor_pages() @@ -272,6 +271,6 @@ public async Task Can_get_routes_for_razor_pages() } } } - """); + """, IgnoreLineEndingsComparer.Instance); } } diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RazorPagesWebApplicationFactory.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RazorPagesWebApplicationFactory.cs new file mode 100644 index 0000000000..322e07311b --- /dev/null +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RazorPagesWebApplicationFactory.cs @@ -0,0 +1,29 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Steeltoe.Management.Endpoint.RazorPagesTestWebApp.Pages; +using Steeltoe.Management.Endpoint.Test.Actuators.RouteMappings.AppTypes; + +namespace Steeltoe.Management.Endpoint.Test.Actuators.RouteMappings; + +public sealed class RazorPagesWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + string? testAssemblyName = typeof(RazorPagesExternalAppTest).Assembly.GetName().Name; + string? appAssemblyName = typeof(IndexModel).Assembly.GetName().Name; + + string absoluteContentRoot = System.Environment.CurrentDirectory; + absoluteContentRoot = absoluteContentRoot.Replace($"/{testAssemblyName}/", $"/{appAssemblyName}/", StringComparison.Ordinal); + absoluteContentRoot = absoluteContentRoot.Replace($@"\{testAssemblyName}\", $@"\{appAssemblyName}\", StringComparison.Ordinal); + + // Workaround for https://github.com/dotnet/aspnetcore/issues/55867. + builder.UseContentRoot(absoluteContentRoot); + + // Workaround for https://github.com/dotnet/aspnetcore/issues/55867#issuecomment-3046941805. + builder.UseEnvironment("Production"); + } +} diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs index cdbf2d628f..6c63c31926 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs @@ -183,7 +183,7 @@ public async Task Returns_no_endpoints_and_logs_warning_when_conventional_routin """); IList logMessages = loggerProvider.GetAll(); - logMessages.Should().Contain($"WARN {typeof(AspNetEndpointProvider).FullName}: Conventional routing is not supported."); + logMessages.Should().Contain($"WARN {typeof(AspNetEndpointProvider)}: Conventional routing is not supported."); } [Theory] @@ -373,21 +373,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Mappings": { - "IncludeActuators": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Mappings": { + "IncludeActuators": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication host = builder.Build(); @@ -400,10 +400,10 @@ public async Task Can_change_configuration_at_runtime() responseNode1["contexts"]!["application"]!["mappings"]!["dispatcherServlets"]!["dispatcherServlet"].Should().BeOfType().Subject.Should() .BeEmpty(); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs index 01346608b3..43711fff72 100644 --- a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs @@ -8,26 +8,38 @@ namespace Steeltoe.Management.Endpoint.Test.Actuators.ThreadDump; +[Collection("TestsForMemoryDumpsMustRunSequentially")] +[Trait("Category", "MemoryDumps")] public sealed class EventPipeThreadDumperTest { - [Trait("Category", "MemoryDumps")] [Fact] public async Task Can_resolve_source_location_from_pdb() { using var backgroundCancellationSource = new CancellationTokenSource(); + using var threadStarted = new ManualResetEventSlim(false); var backgroundThread = new Thread(NestedType.BackgroundThreadCallback) { IsBackground = true }; - backgroundThread.Start(backgroundCancellationSource.Token); + backgroundThread.Start((backgroundCancellationSource.Token, threadStarted)); + threadStarted.Wait(TestContext.Current.CancellationToken); using var loggerProvider = new CapturingLoggerProvider(); using var loggerFactory = new LoggerFactory([loggerProvider]); ILogger logger = loggerFactory.CreateLogger(); +#if NET8_0 + // Use a longer collection window on .NET 8 to compensate for the Sleep(0) yield. + var optionsMonitor = TestOptionsMonitor.Create(new ThreadDumpEndpointOptions + { + Duration = 100 + }); +#else var optionsMonitor = new TestOptionsMonitor(); +#endif + var dumper = new EventPipeThreadDumper(optionsMonitor, logger); IList threads = await dumper.DumpThreadsAsync(TestContext.Current.CancellationToken); @@ -35,7 +47,12 @@ public async Task Can_resolve_source_location_from_pdb() StackTraceElement? backgroundThreadFrame = threads.SelectMany(thread => thread.StackTrace) .FirstOrDefault(frame => frame.MethodName == "BackgroundThreadCallback(class System.Object)"); - backgroundThreadFrame.Should().NotBeNull(); + if (backgroundThreadFrame == null) + { + string logs = loggerProvider.GetAsText(); + throw new InvalidOperationException($"Failed to find expected stack frame. Captured log:{System.Environment.NewLine}{logs}"); + } + backgroundThreadFrame.IsNativeMethod.Should().BeFalse(); backgroundThreadFrame.ModuleName.Should().Be(GetType().Assembly.GetName().Name); backgroundThreadFrame.ClassName.Should().Be(typeof(NestedType).FullName); @@ -47,11 +64,11 @@ public async Task Can_resolve_source_location_from_pdb() backgroundThread.Join(); IList logLines = loggerProvider.GetAll(); - logLines.Should().Contain($"INFO {typeof(EventPipeThreadDumper).FullName}: Attempting to create a thread dump."); - logLines.Should().Contain($"INFO {typeof(EventPipeThreadDumper).FullName}: Successfully created a thread dump."); + logLines.Should().Contain($"INFO {typeof(EventPipeThreadDumper)}: Attempting to create a thread dump."); + logLines.Should().Contain($"INFO {typeof(EventPipeThreadDumper)}: Successfully created a thread dump."); string logText = loggerProvider.GetAsText(); - logText.Should().Contain($"TRCE {typeof(EventPipeThreadDumper).FullName}: Captured log from thread dump:"); + logText.Should().Contain($"TRCE {typeof(EventPipeThreadDumper)}: Captured log from thread dump:"); logText.Should().Contain("Created SymbolReader with SymbolPath"); } @@ -81,11 +98,18 @@ private static class NestedType { public static void BackgroundThreadCallback(object? argument) { - var cancellationToken = (CancellationToken)argument!; + (CancellationToken cancellationToken, ManualResetEventSlim threadStarted) = ((CancellationToken, ManualResetEventSlim))argument!; + + threadStarted.Set(); while (!cancellationToken.IsCancellationRequested) { - Thread.Sleep(TimeSpan.FromMilliseconds(50)); + // Only actively-running threads are shown in the thread dump, so we need to make sure the CPU is in use. + Thread.SpinWait(250); +#if NET8_0 + // Yield to allow the EventPipe rundown thread to make progress on .NET 8. + Thread.Sleep(0); +#endif } } } diff --git a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/ThreadDumpActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/ThreadDumpActuatorTest.cs index a6887e2ad5..693939d748 100644 --- a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/ThreadDumpActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/ThreadDumpActuatorTest.cs @@ -47,7 +47,7 @@ public async Task Configures_default_settings() options.Enabled.Should().BeNull(); options.Id.Should().Be("threaddump"); options.Path.Should().Be("threaddump"); - options.RequiredPermissions.Should().Be(EndpointPermissions.Restricted); + options.RequiredPermissions.Should().Be(EndpointPermissions.Full); options.GetSafeAllowedVerbs().Should().ContainSingle().Subject.Should().Be("GET"); options.RequiresExactMatch().Should().BeTrue(); diff --git a/src/Management/test/Endpoint.Test/ContentNegotiationTest.cs b/src/Management/test/Endpoint.Test/ContentNegotiationTest.cs index e3647e9e31..a9f20d2358 100644 --- a/src/Management/test/Endpoint.Test/ContentNegotiationTest.cs +++ b/src/Management/test/Endpoint.Test/ContentNegotiationTest.cs @@ -34,6 +34,7 @@ public async Task Can_use_content_type_with_alternate_casing() using HttpClient client = host.GetTestClient(); MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("APPLICATION/vnd.Spring-Boot.Actuator.v3+JSON"); + HttpContent requestContent = new StringContent("{}", contentType); HttpResponseMessage response = @@ -54,6 +55,7 @@ public async Task Can_use_content_type_including_charset() using HttpClient client = host.GetTestClient(); MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("application/vnd.spring-boot.actuator.v3+json; charset=utf-8"); + HttpContent requestContent = new StringContent("{}", contentType); HttpResponseMessage response = @@ -74,6 +76,7 @@ public async Task Cannot_use_invalid_content_type() using HttpClient client = host.GetTestClient(); MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("application/xhtml+xml"); + HttpContent requestContent = new StringContent("{}", contentType); HttpResponseMessage response = diff --git a/src/Management/test/Endpoint.Test/CorsPolicyTest.cs b/src/Management/test/Endpoint.Test/CorsPolicyTest.cs index 88ef4cef2c..c1ff824cd8 100644 --- a/src/Management/test/Endpoint.Test/CorsPolicyTest.cs +++ b/src/Management/test/Endpoint.Test/CorsPolicyTest.cs @@ -183,6 +183,7 @@ public async Task ConfiguresDefaultActuatorsCorsPolicyForGetRequestOnCloudFoundr mock.Expect(HttpMethod.Get, "https://api.cloud.com/v2/apps/798c2495-fe75-49b1-88da-b81197f2bf06/permissions") .WithHeaders("Authorization", $"bearer {token}").Respond("application/json", """ { + "read_basic_data": true, "read_sensitive_data": true } """); diff --git a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs index 5610cf05e4..f5d9858575 100644 --- a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs +++ b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs @@ -86,14 +86,14 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -103,12 +103,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); fileProvider.NotifyChanged(); @@ -121,15 +121,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -139,11 +139,11 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); fileProvider.NotifyChanged(); @@ -156,15 +156,15 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -174,12 +174,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); fileProvider.NotifyChanged(); @@ -192,15 +192,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -210,12 +210,12 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/ManagementOptionsTest.cs b/src/Management/test/Endpoint.Test/ManagementOptionsTest.cs index 096108dd39..0868057508 100644 --- a/src/Management/test/Endpoint.Test/ManagementOptionsTest.cs +++ b/src/Management/test/Endpoint.Test/ManagementOptionsTest.cs @@ -20,7 +20,7 @@ namespace Steeltoe.Management.Endpoint.Test; public sealed class ManagementOptionsTest { - private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions + private static readonly JsonSerializerOptions ExpectedJsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = @@ -51,7 +51,7 @@ public async Task Configures_default_settings() options.Port.Should().Be(0); options.SslEnabled.Should().BeFalse(); options.UseStatusCodeFromResponse.Should().BeTrue(); - options.SerializerOptions.Should().BeEquivalentTo(DefaultJsonSerializerOptions); + options.SerializerOptions.Should().BeEquivalentTo(ExpectedJsonSerializerOptions); options.CustomJsonConverters.Should().BeEmpty(); options.GetBasePath("/cloudfoundryapplication/info").Should().Be("/cloudfoundryapplication"); @@ -97,7 +97,7 @@ public async Task Configures_custom_settings() options.SslEnabled.Should().BeTrue(); options.UseStatusCodeFromResponse.Should().BeFalse(); - options.SerializerOptions.Should().BeEquivalentTo(new JsonSerializerOptions(DefaultJsonSerializerOptions) + options.SerializerOptions.Should().BeEquivalentTo(new JsonSerializerOptions(ExpectedJsonSerializerOptions) { WriteIndented = true, Converters = diff --git a/src/Management/test/Endpoint.Test/ManagementPort/ManagementEndpointServedOnDifferentPortTest.cs b/src/Management/test/Endpoint.Test/ManagementPort/ManagementEndpointServedOnDifferentPortTest.cs index 988a82d745..bda2840057 100644 --- a/src/Management/test/Endpoint.Test/ManagementPort/ManagementEndpointServedOnDifferentPortTest.cs +++ b/src/Management/test/Endpoint.Test/ManagementPort/ManagementEndpointServedOnDifferentPortTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Net; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -102,6 +103,44 @@ public async Task AspNetDefaultPort_AlternateManagementPortConfigured_Accessible actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } + [Fact] + public async Task AspNetDefaultPort_AlternateManagementPortConfigured_IgnoresSpoofedHostHeader() + { + const string managementPort = "8000"; + + var appSettings = new Dictionary + { + ["Management:Endpoints:Port"] = managementPort + }; + + await using WebApplication app = await CreateAppAsync(appSettings); + + IFeatureCollection serverFeatures = app.Services.GetRequiredService().Features; + ICollection addresses = serverFeatures.GetRequiredFeature().Addresses; + + addresses.Should().HaveCount(2); + addresses.ElementAt(0).Should().Be($"http://localhost:{AspNetDefaultPort}"); + addresses.ElementAt(1).Should().Be($"http://[::]:{managementPort}"); + + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage appResponse = await httpClient.GetAsync(new Uri($"http://localhost:{AspNetDefaultPort}"), TestContext.Current.CancellationToken); + appResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + appResponse = await httpClient.GetAsync(new Uri($"http://localhost:{managementPort}"), TestContext.Current.CancellationToken); + appResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); + + HttpResponseMessage actuatorResponse = + await httpClient.GetAsync(new Uri($"http://localhost:{AspNetDefaultPort}/actuator"), TestContext.Current.CancellationToken); + + actuatorResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); + + var spoofRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://localhost:{AspNetDefaultPort}/actuator")); + spoofRequest.Headers.Host = $"anything:{managementPort}"; + actuatorResponse = await httpClient.SendAsync(spoofRequest, TestContext.Current.CancellationToken); + actuatorResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + [Fact] public async Task AspNetDefaultPort_AlternateManagementPortConfigured_AccessibleOnCfInstancePorts() { @@ -142,8 +181,7 @@ public async Task AspNetDefaultPort_AlternateManagementPortConfigured_Accessible actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetDefaultPort_AlternateManagementPortAndSchemeConfigured_AccessibleOnSeparatePortsAndSchemes() { const string managementPort = "8000"; @@ -180,8 +218,7 @@ public async Task AspNetDefaultPort_AlternateManagementPortAndSchemeConfigured_A actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsConfigured_NoManagementPortConfigured_BothAccessibleOnSamePort() { const string appHttpPort = "6000"; @@ -218,8 +255,7 @@ public async Task AspNetCustomPortsConfigured_NoManagementPortConfigured_BothAcc actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsConfigured_SameManagementPortConfigured_OnlyActuatorAccessible() { const string appHttpPort = "6000"; @@ -257,8 +293,7 @@ public async Task AspNetCustomPortsConfigured_SameManagementPortConfigured_OnlyA actuatorResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsConfigured_AlternateManagementPortConfigured_AccessibleOnSeparatePorts() { const string appHttpPort = "6000"; @@ -304,8 +339,7 @@ public async Task AspNetCustomPortsConfigured_AlternateManagementPortConfigured_ actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsConfigured_AlternateHttpsManagementPortConfigured_AccessibleOnSeparatePortsAndSchemes() { const string appHttpPort = "6000"; @@ -352,8 +386,7 @@ public async Task AspNetCustomPortsConfigured_AlternateHttpsManagementPortConfig actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInEnvironmentVariables_NoManagementPortConfigured_BothAccessibleOnSamePort() { const string appHttpPort = "6000"; @@ -388,8 +421,7 @@ public async Task AspNetCustomPortsInEnvironmentVariables_NoManagementPortConfig actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInEnvironmentVariables_SameManagementPortConfigured_OnlyActuatorAccessible() { const string appHttpPort = "6000"; @@ -429,8 +461,7 @@ public async Task AspNetCustomPortsInEnvironmentVariables_SameManagementPortConf actuatorResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInEnvironmentVariables_AlternateManagementPortConfigured_AccessibleOnSeparatePorts() { const string appHttpPort = "6000"; @@ -478,8 +509,7 @@ public async Task AspNetCustomPortsInEnvironmentVariables_AlternateManagementPor actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInEnvironmentVariables_AlternateHttpsManagementPortConfigured_AccessibleOnSeparatePortsAndSchemes() { const string appHttpPort = "6000"; @@ -528,8 +558,7 @@ public async Task AspNetCustomPortsInEnvironmentVariables_AlternateHttpsManageme actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInCode_NoManagementPortConfigured_BothAccessibleOnSamePort() { const string appHttpPort = "6000"; @@ -565,8 +594,7 @@ public async Task AspNetCustomPortsInCode_NoManagementPortConfigured_BothAccessi actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInCode_SameManagementPortConfigured_OnlyActuatorAccessible() { const string appHttpPort = "6000"; @@ -607,8 +635,7 @@ public async Task AspNetCustomPortsInCode_SameManagementPortConfigured_OnlyActua actuatorResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInCode_AlternateManagementPortConfigured_AccessibleOnSeparatePorts() { const string appHttpPort = "6000"; @@ -657,8 +684,7 @@ public async Task AspNetCustomPortsInCode_AlternateManagementPortConfigured_Acce actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetCustomPortsInCode_AlternateHttpsManagementPortConfigured_AccessibleOnSeparatePortsAndSchemes() { const string appHttpPort = "6000"; @@ -708,8 +734,7 @@ public async Task AspNetCustomPortsInCode_AlternateHttpsManagementPortConfigured actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetDynamicPortsConfigured_NoManagementPortConfigured_BothAccessibleOnSamePort() { var appSettings = new Dictionary @@ -747,8 +772,7 @@ public async Task AspNetDynamicPortsConfigured_NoManagementPortConfigured_BothAc actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetDynamicPortsConfigured_AlternateManagementPortConfigured_AccessibleOnSeparatePorts() { const string managementPort = "8000"; @@ -796,8 +820,7 @@ public async Task AspNetDynamicPortsConfigured_AlternateManagementPortConfigured actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] // https://github.com/dotnet/aspnetcore/issues/42273 + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task AspNetDynamicPortsConfigured_AlternateHttpsManagementPortConfigured_AccessibleOnSeparatePortsAndSchemes() { const string managementPort = "8000"; diff --git a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs index ae623702d9..81dfc18dee 100644 --- a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs +++ b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http.Json; +using System.Runtime.InteropServices; using System.Text; using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; @@ -58,8 +59,7 @@ public async Task CanUseDynamicHttpPort() requestApplication.ServiceUrl.Port.Should().BePositive(); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task CanUseDynamicHttpsPort() { var appSettings = new Dictionary @@ -154,7 +154,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -171,7 +171,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -186,7 +186,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() handler.Mock.GetMatchCount(registerMock).Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -213,7 +213,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -230,7 +230,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -246,7 +246,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() handler.Mock.GetMatchCount(registerMock).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -370,7 +370,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -387,7 +387,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -405,7 +405,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -434,7 +434,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -451,7 +451,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -470,7 +470,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { diff --git a/src/Management/test/Endpoint.Test/SpringBootAdminClient/SpringBootAdminRefreshRunnerTest.cs b/src/Management/test/Endpoint.Test/SpringBootAdminClient/SpringBootAdminRefreshRunnerTest.cs index 9761d19fe4..c1349cdbd7 100644 --- a/src/Management/test/Endpoint.Test/SpringBootAdminClient/SpringBootAdminRefreshRunnerTest.cs +++ b/src/Management/test/Endpoint.Test/SpringBootAdminClient/SpringBootAdminRefreshRunnerTest.cs @@ -83,7 +83,7 @@ public async Task BindsConfiguration() app.Services.GetRequiredService().Using(handler); var runner = app.Services.GetRequiredService(); - await runner.RunAsync(TestContext.Current.CancellationToken); + await runner.RunAsync(true, TestContext.Current.CancellationToken); SpringBootAdminClientOptions? options = runner.LastGoodOptions; options.Should().NotBeNull(); @@ -116,7 +116,7 @@ public async Task FailsOnMissingConfiguration() await using WebApplication app = builder.Build(); var runner = app.Services.GetRequiredService(); - Func action = async () => await runner.RunAsync(TestContext.Current.CancellationToken); + Func action = async () => await runner.RunAsync(true, TestContext.Current.CancellationToken); string[] errorsExpected = [ @@ -145,7 +145,7 @@ public async Task FailsOnInvalidConfiguration() await using WebApplication app = builder.Build(); var runner = app.Services.GetRequiredService(); - Func action = async () => await runner.RunAsync(TestContext.Current.CancellationToken); + Func action = async () => await runner.RunAsync(true, TestContext.Current.CancellationToken); string[] errorsExpected = [ @@ -175,7 +175,7 @@ public async Task FailsWhenConfigurationForBasePathIsUrl() await using WebApplication app = builder.Build(); var runner = app.Services.GetRequiredService(); - Func action = async () => await runner.RunAsync(TestContext.Current.CancellationToken); + Func action = async () => await runner.RunAsync(true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync() .WithMessage("Use BaseUrl instead of BasePath to configure the absolute URL to register with"); @@ -202,7 +202,7 @@ public async Task BindsApplicationNameFromSpringConfiguration() app.Services.GetRequiredService().Using(handler); var runner = app.Services.GetRequiredService(); - await runner.RunAsync(TestContext.Current.CancellationToken); + await runner.RunAsync(true, TestContext.Current.CancellationToken); SpringBootAdminClientOptions? options = runner.LastGoodOptions; options.Should().NotBeNull(); @@ -249,7 +249,7 @@ public async Task SendsRegisterRequestForDefaultConfiguration() app.Services.GetRequiredService().Using(handler); var runner = app.Services.GetRequiredService(); - await runner.RunAsync(TestContext.Current.CancellationToken); + await runner.RunAsync(true, TestContext.Current.CancellationToken); runner.LastRegistrationId.Should().Be("1234567"); runner.LastGoodOptions.Should().NotBeNull(); @@ -301,7 +301,7 @@ public async Task SendsRegisterRequestForCustomConfiguration() app.Services.GetRequiredService().Using(handler); var runner = app.Services.GetRequiredService(); - await runner.RunAsync(TestContext.Current.CancellationToken); + await runner.RunAsync(true, TestContext.Current.CancellationToken); runner.LastRegistrationId.Should().Be("1234567"); runner.LastGoodOptions.Should().NotBeNull(); diff --git a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj index afaade1a7f..97607ba184 100644 --- a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj +++ b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 @@ -16,6 +16,7 @@ + diff --git a/src/Management/test/Endpoint.Test/xunit.runner.json b/src/Management/test/Endpoint.Test/xunit.runner.json index a2f869986e..fdeefaa456 100644 --- a/src/Management/test/Endpoint.Test/xunit.runner.json +++ b/src/Management/test/Endpoint.Test/xunit.runner.json @@ -1,4 +1,4 @@ -{ - "maxParallelThreads": 1, - "parallelizeTestCollections": false -} +{ + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} diff --git a/src/Management/test/Prometheus.Test/Steeltoe.Management.Prometheus.Test.csproj b/src/Management/test/Prometheus.Test/Steeltoe.Management.Prometheus.Test.csproj index 95444f1e2c..484afb7186 100644 --- a/src/Management/test/Prometheus.Test/Steeltoe.Management.Prometheus.Test.csproj +++ b/src/Management/test/Prometheus.Test/Steeltoe.Management.Prometheus.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Management/test/RazorPagesTestWebApp/Pages/Shared/_Layout.cshtml b/src/Management/test/RazorPagesTestWebApp/Pages/Shared/_Layout.cshtml index ee77ef62e6..4748f9de45 100644 --- a/src/Management/test/RazorPagesTestWebApp/Pages/Shared/_Layout.cshtml +++ b/src/Management/test/RazorPagesTestWebApp/Pages/Shared/_Layout.cshtml @@ -1,50 +1,50 @@ - - - - @ViewData["Title"] - Steeltoe.Management.Endpoint.RazorPagesTestWebApp - - - - -
- -
-
-
- @RenderBody() -
-
-
-
- © 2025 - Steeltoe.Management.Endpoint.RazorPagesTestWebApp - Privacy -
-
+
+
+ © 2025 - Steeltoe.Management.Endpoint.RazorPagesTestWebApp - Privacy +
+
- - - + + + -@await RenderSectionAsync("Scripts", false) - + @await RenderSectionAsync("Scripts", false) + diff --git a/src/Management/test/RazorPagesTestWebApp/Program.cs b/src/Management/test/RazorPagesTestWebApp/Program.cs index 508e5edcfa..b4295ca480 100644 --- a/src/Management/test/RazorPagesTestWebApp/Program.cs +++ b/src/Management/test/RazorPagesTestWebApp/Program.cs @@ -12,7 +12,7 @@ { // This project intentionally does NOT include appsettings*.json files, because they get copied to test projects // that reference this project, and that affects test outcomes. For example, setting the minimum log level - // to Trace on WebApplicationBuilder wouldn't work, because these files overrule log levels. + // to Trace on WebApplicationBuilder wouldn't work, because these files override log levels. ["DetailedErrors"] = builder.Environment.IsDevelopment() ? "true" : "false", ["Logging:LogLevel:Default"] = "Information", diff --git a/src/Management/test/RazorPagesTestWebApp/Steeltoe.Management.Endpoint.RazorPagesTestWebApp.csproj b/src/Management/test/RazorPagesTestWebApp/Steeltoe.Management.Endpoint.RazorPagesTestWebApp.csproj index 260873cc43..69cfd6dd8d 100644 --- a/src/Management/test/RazorPagesTestWebApp/Steeltoe.Management.Endpoint.RazorPagesTestWebApp.csproj +++ b/src/Management/test/RazorPagesTestWebApp/Steeltoe.Management.Endpoint.RazorPagesTestWebApp.csproj @@ -1,6 +1,6 @@ - + - net9.0;net8.0 + net10.0;net9.0;net8.0 false diff --git a/src/Management/test/Tasks.Test/Steeltoe.Management.Tasks.Test.csproj b/src/Management/test/Tasks.Test/Steeltoe.Management.Tasks.Test.csproj index dc3b1e985f..c8c75a12ee 100644 --- a/src/Management/test/Tasks.Test/Steeltoe.Management.Tasks.Test.csproj +++ b/src/Management/test/Tasks.Test/Steeltoe.Management.Tasks.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Management/test/Tasks.Test/TaskHostExtensionsTest.cs b/src/Management/test/Tasks.Test/TaskHostExtensionsTest.cs index 03a7917f7c..82bb71d688 100644 --- a/src/Management/test/Tasks.Test/TaskHostExtensionsTest.cs +++ b/src/Management/test/Tasks.Test/TaskHostExtensionsTest.cs @@ -198,7 +198,8 @@ public async Task WebApplication_LogsErrorOnUnknownTask() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(args); - var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal)); + // ReSharper disable once AccessToDisposedClosure builder.Services.AddLogging(options => options.AddProvider(capturingLoggerProvider)); WebApplication app = builder.Build(); @@ -338,6 +339,7 @@ public async Task RunAsync(CancellationToken cancellationToken) } } + // ReSharper disable once ClassNeverInstantiated.Local private sealed class ThrowingApplicationTask : IApplicationTask { public Task RunAsync(CancellationToken cancellationToken) diff --git a/src/Management/test/Tracing.Test/Steeltoe.Management.Tracing.Test.csproj b/src/Management/test/Tracing.Test/Steeltoe.Management.Tracing.Test.csproj index 5e73996d4e..c28991c23d 100644 --- a/src/Management/test/Tracing.Test/Steeltoe.Management.Tracing.Test.csproj +++ b/src/Management/test/Tracing.Test/Steeltoe.Management.Tracing.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Management/test/Tracing.Test/xunit.runner.json b/src/Management/test/Tracing.Test/xunit.runner.json index a2f869986e..fdeefaa456 100644 --- a/src/Management/test/Tracing.Test/xunit.runner.json +++ b/src/Management/test/Tracing.Test/xunit.runner.json @@ -1,4 +1,4 @@ -{ - "maxParallelThreads": 1, - "parallelizeTestCollections": false -} +{ + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} diff --git a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Build/Steeltoe.Bootstrap.Autoconfig.targets b/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Build/Steeltoe.Bootstrap.Autoconfig.targets deleted file mode 100644 index 19ead75e0d..0000000000 --- a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Build/Steeltoe.Bootstrap.Autoconfig.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/PackageReadme.md b/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/PackageReadme.md deleted file mode 100644 index 274f63fff7..0000000000 --- a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Bootstrap.AutoConfiguration` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Steeltoe.Bootstrap.Autoconfig.csproj b/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Steeltoe.Bootstrap.Autoconfig.csproj deleted file mode 100644 index b725b15a7a..0000000000 --- a/src/Obsolete/Steeltoe.Bootstrap.Autoconfig/Steeltoe.Bootstrap.Autoconfig.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for automatically configuring Steeltoe packages that have separately been added to a project. - Autoconfiguration;automatic configuration;application bootstrapping - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Build/Steeltoe.CircuitBreaker.Abstractions.targets b/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Build/Steeltoe.CircuitBreaker.Abstractions.targets deleted file mode 100644 index 178fef2229..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Build/Steeltoe.CircuitBreaker.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Steeltoe.CircuitBreaker.Abstractions.csproj b/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Steeltoe.CircuitBreaker.Abstractions.csproj deleted file mode 100644 index f7982c518f..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Abstractions/Steeltoe.CircuitBreaker.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Circuit breaker abstractions. - Spring Cloud;Hystrix Client;Circuit Breaker - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.targets b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.targets deleted file mode 100644 index c33c814ae6..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/PackageReadme.md b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.csproj b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.csproj deleted file mode 100644 index fdca65766a..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore/Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Netflix Hystrix Metrics Event Stream ASP.NET Core. - aspnetcore;Circuit Breaker;Spring;Spring Cloud;Spring Cloud Hystrix;Hystrix - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.targets b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.targets deleted file mode 100644 index f694771dbb..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Build/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/PackageReadme.md b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.csproj b/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.csproj deleted file mode 100644 index 4e401f5d36..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore/Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Netflix Hystrix metrics event stream for ASP.NET Core over RabbitMQ. - aspnetcore;Circuit Breaker;Spring;Spring Cloud;Spring Cloud Hystrix;Hystrix;turbine;cloudfoundry - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Build/Steeltoe.CircuitBreaker.HystrixBase.targets b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Build/Steeltoe.CircuitBreaker.HystrixBase.targets deleted file mode 100644 index 77bca67dfb..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Build/Steeltoe.CircuitBreaker.HystrixBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/PackageReadme.md b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Steeltoe.CircuitBreaker.HystrixBase.csproj b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Steeltoe.CircuitBreaker.HystrixBase.csproj deleted file mode 100644 index 124b01eb60..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixBase/Steeltoe.CircuitBreaker.HystrixBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe's implementation of Netflix's Hystrix, for .NET. - Spring Cloud;Netflix;Hystrix Client;Circuit Breaker - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Build/Steeltoe.CircuitBreaker.HystrixCore.targets b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Build/Steeltoe.CircuitBreaker.HystrixCore.targets deleted file mode 100644 index 5d0b75f6f0..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Build/Steeltoe.CircuitBreaker.HystrixCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/PackageReadme.md b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Steeltoe.CircuitBreaker.HystrixCore.csproj b/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Steeltoe.CircuitBreaker.HystrixCore.csproj deleted file mode 100644 index 458c21b763..0000000000 --- a/src/Obsolete/Steeltoe.CircuitBreaker.HystrixCore/Steeltoe.CircuitBreaker.HystrixCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for adding Steeltoe Hystrix to ASP.NET Core applications. - aspnetcore;Spring Cloud;Netflix;Hystrix Client;Circuit Breaker - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Abstractions/Build/Steeltoe.Common.Abstractions.targets b/src/Obsolete/Steeltoe.Common.Abstractions/Build/Steeltoe.Common.Abstractions.targets deleted file mode 100644 index 1b9eb0f3b2..0000000000 --- a/src/Obsolete/Steeltoe.Common.Abstractions/Build/Steeltoe.Common.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Abstractions/PackageReadme.md deleted file mode 100644 index a6390dae27..0000000000 --- a/src/Obsolete/Steeltoe.Common.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Common` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Abstractions/Steeltoe.Common.Abstractions.csproj b/src/Obsolete/Steeltoe.Common.Abstractions/Steeltoe.Common.Abstractions.csproj deleted file mode 100644 index fd4d319a32..0000000000 --- a/src/Obsolete/Steeltoe.Common.Abstractions/Steeltoe.Common.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions commonly used across Steeltoe components. - Steeltoe - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Expression/Build/Steeltoe.Common.Expression.targets b/src/Obsolete/Steeltoe.Common.Expression/Build/Steeltoe.Common.Expression.targets deleted file mode 100644 index 699afd69d7..0000000000 --- a/src/Obsolete/Steeltoe.Common.Expression/Build/Steeltoe.Common.Expression.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Expression/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Expression/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Common.Expression/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Expression/Steeltoe.Common.Expression.csproj b/src/Obsolete/Steeltoe.Common.Expression/Steeltoe.Common.Expression.csproj deleted file mode 100644 index 205a0483a9..0000000000 --- a/src/Obsolete/Steeltoe.Common.Expression/Steeltoe.Common.Expression.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe common expression language library. - NET Core;Expression;SPEL - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Kubernetes/Build/Steeltoe.Common.Kubernetes.targets b/src/Obsolete/Steeltoe.Common.Kubernetes/Build/Steeltoe.Common.Kubernetes.targets deleted file mode 100644 index 34f6dcd0fd..0000000000 --- a/src/Obsolete/Steeltoe.Common.Kubernetes/Build/Steeltoe.Common.Kubernetes.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Kubernetes/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Kubernetes/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Common.Kubernetes/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Kubernetes/Steeltoe.Common.Kubernetes.csproj b/src/Obsolete/Steeltoe.Common.Kubernetes/Steeltoe.Common.Kubernetes.csproj deleted file mode 100644 index 271fcc6d8e..0000000000 --- a/src/Obsolete/Steeltoe.Common.Kubernetes/Steeltoe.Common.Kubernetes.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe common library for Kubernetes. - Kubernetes - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Retry/Build/Steeltoe.Common.Retry.targets b/src/Obsolete/Steeltoe.Common.Retry/Build/Steeltoe.Common.Retry.targets deleted file mode 100644 index 5ec00bc904..0000000000 --- a/src/Obsolete/Steeltoe.Common.Retry/Build/Steeltoe.Common.Retry.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Retry/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Retry/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Common.Retry/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Retry/Steeltoe.Common.Retry.csproj b/src/Obsolete/Steeltoe.Common.Retry/Steeltoe.Common.Retry.csproj deleted file mode 100644 index ab1ec15cca..0000000000 --- a/src/Obsolete/Steeltoe.Common.Retry/Steeltoe.Common.Retry.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Retries for circuit breaker. - retries;circuit breaker. - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Security/Build/Steeltoe.Common.Security.targets b/src/Obsolete/Steeltoe.Common.Security/Build/Steeltoe.Common.Security.targets deleted file mode 100644 index 0cc80dad48..0000000000 --- a/src/Obsolete/Steeltoe.Common.Security/Build/Steeltoe.Common.Security.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Security/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Security/PackageReadme.md deleted file mode 100644 index 0c39078f7c..0000000000 --- a/src/Obsolete/Steeltoe.Common.Security/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Common.Certificates` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj b/src/Obsolete/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj deleted file mode 100644 index efbaed9ae9..0000000000 --- a/src/Obsolete/Steeltoe.Common.Security/Steeltoe.Common.Security.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe common library for security. - Steeltoe;security - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Utils/Build/Steeltoe.Common.Utils.targets b/src/Obsolete/Steeltoe.Common.Utils/Build/Steeltoe.Common.Utils.targets deleted file mode 100644 index d8e9ca7c13..0000000000 --- a/src/Obsolete/Steeltoe.Common.Utils/Build/Steeltoe.Common.Utils.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Common.Utils/PackageReadme.md b/src/Obsolete/Steeltoe.Common.Utils/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Common.Utils/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Common.Utils/Steeltoe.Common.Utils.csproj b/src/Obsolete/Steeltoe.Common.Utils/Steeltoe.Common.Utils.csproj deleted file mode 100644 index b9fc95be98..0000000000 --- a/src/Obsolete/Steeltoe.Common.Utils/Steeltoe.Common.Utils.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe common utility libraries. - Steeltoe - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.Abstractions/Build/Steeltoe.Connector.Abstractions.targets b/src/Obsolete/Steeltoe.Connector.Abstractions/Build/Steeltoe.Connector.Abstractions.targets deleted file mode 100644 index d57f3a8438..0000000000 --- a/src/Obsolete/Steeltoe.Connector.Abstractions/Build/Steeltoe.Connector.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.Abstractions/PackageReadme.md deleted file mode 100644 index 8598e8519c..0000000000 --- a/src/Obsolete/Steeltoe.Connector.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Connectors` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.Abstractions/Steeltoe.Connector.Abstractions.csproj b/src/Obsolete/Steeltoe.Connector.Abstractions/Steeltoe.Connector.Abstractions.csproj deleted file mode 100644 index 2007b5bd63..0000000000 --- a/src/Obsolete/Steeltoe.Connector.Abstractions/Steeltoe.Connector.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for working with backing services. - connectors;services - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.CloudFoundry/Build/Steeltoe.Connector.CloudFoundry.targets b/src/Obsolete/Steeltoe.Connector.CloudFoundry/Build/Steeltoe.Connector.CloudFoundry.targets deleted file mode 100644 index 6ac082b5ad..0000000000 --- a/src/Obsolete/Steeltoe.Connector.CloudFoundry/Build/Steeltoe.Connector.CloudFoundry.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.CloudFoundry/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.CloudFoundry/PackageReadme.md deleted file mode 100644 index 8598e8519c..0000000000 --- a/src/Obsolete/Steeltoe.Connector.CloudFoundry/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Connectors` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.CloudFoundry/Steeltoe.Connector.CloudFoundry.csproj b/src/Obsolete/Steeltoe.Connector.CloudFoundry/Steeltoe.Connector.CloudFoundry.csproj deleted file mode 100644 index 8d7d6f03f2..0000000000 --- a/src/Obsolete/Steeltoe.Connector.CloudFoundry/Steeltoe.Connector.CloudFoundry.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for enabling Steeltoe Connectors on Cloud Foundry. - CloudFoundry;vcap;connectors - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorBase/Build/Steeltoe.Connector.ConnectorBase.targets b/src/Obsolete/Steeltoe.Connector.ConnectorBase/Build/Steeltoe.Connector.ConnectorBase.targets deleted file mode 100644 index 285c31b244..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorBase/Build/Steeltoe.Connector.ConnectorBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorBase/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.ConnectorBase/PackageReadme.md deleted file mode 100644 index 8598e8519c..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Connectors` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorBase/Steeltoe.Connector.ConnectorBase.csproj b/src/Obsolete/Steeltoe.Connector.ConnectorBase/Steeltoe.Connector.ConnectorBase.csproj deleted file mode 100644 index 7a2cf9fa7c..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorBase/Steeltoe.Connector.ConnectorBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Connectors for using service bindings in your application. - connectors;services - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorCore/Build/Steeltoe.Connector.ConnectorCore.targets b/src/Obsolete/Steeltoe.Connector.ConnectorCore/Build/Steeltoe.Connector.ConnectorCore.targets deleted file mode 100644 index 8c33be76bf..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorCore/Build/Steeltoe.Connector.ConnectorCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorCore/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.ConnectorCore/PackageReadme.md deleted file mode 100644 index 8598e8519c..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Connectors` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.ConnectorCore/Steeltoe.Connector.ConnectorCore.csproj b/src/Obsolete/Steeltoe.Connector.ConnectorCore/Steeltoe.Connector.ConnectorCore.csproj deleted file mode 100644 index 73316835af..0000000000 --- a/src/Obsolete/Steeltoe.Connector.ConnectorCore/Steeltoe.Connector.ConnectorCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using service connectors in your .NET Core application. - connectors;aspnetcore;services - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.EF6Core/Build/Steeltoe.Connector.EF6Core.targets b/src/Obsolete/Steeltoe.Connector.EF6Core/Build/Steeltoe.Connector.EF6Core.targets deleted file mode 100644 index 1afad722c8..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EF6Core/Build/Steeltoe.Connector.EF6Core.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.EF6Core/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.EF6Core/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EF6Core/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.EF6Core/Steeltoe.Connector.EF6Core.csproj b/src/Obsolete/Steeltoe.Connector.EF6Core/Steeltoe.Connector.EF6Core.csproj deleted file mode 100644 index 5ce8299759..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EF6Core/Steeltoe.Connector.EF6Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Connector Extensions for Entity Framework. - connectors;EntityFramework;aspnetcore;services - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.EFCore/Build/Steeltoe.Connector.EFCore.targets b/src/Obsolete/Steeltoe.Connector.EFCore/Build/Steeltoe.Connector.EFCore.targets deleted file mode 100644 index 36c7910be1..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EFCore/Build/Steeltoe.Connector.EFCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Connector.EFCore/PackageReadme.md b/src/Obsolete/Steeltoe.Connector.EFCore/PackageReadme.md deleted file mode 100644 index 0b94145db9..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EFCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Connectors.EntityFrameworkCore` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Connector.EFCore/Steeltoe.Connector.EFCore.csproj b/src/Obsolete/Steeltoe.Connector.EFCore/Steeltoe.Connector.EFCore.csproj deleted file mode 100644 index 47c638ba60..0000000000 --- a/src/Obsolete/Steeltoe.Connector.EFCore/Steeltoe.Connector.EFCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using Steeltoe Connectors with Entity Framework Core. - connectors;EFCore;EntityFrameworkCore;EF;Entity Framework Core;entity-framework-core;services - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.Abstractions/Build/Steeltoe.Discovery.Abstractions.targets b/src/Obsolete/Steeltoe.Discovery.Abstractions/Build/Steeltoe.Discovery.Abstractions.targets deleted file mode 100644 index de86a3c355..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Abstractions/Build/Steeltoe.Discovery.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Discovery.Abstractions/PackageReadme.md deleted file mode 100644 index a6390dae27..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Common` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Discovery.Abstractions/Steeltoe.Discovery.Abstractions.csproj b/src/Obsolete/Steeltoe.Discovery.Abstractions/Steeltoe.Discovery.Abstractions.csproj deleted file mode 100644 index f4206bc0b0..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Abstractions/Steeltoe.Discovery.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for service registration and discovery. - service discovery;service registry;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.ClientBase/Build/Steeltoe.Discovery.ClientBase.targets b/src/Obsolete/Steeltoe.Discovery.ClientBase/Build/Steeltoe.Discovery.ClientBase.targets deleted file mode 100644 index 58033869a2..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientBase/Build/Steeltoe.Discovery.ClientBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.ClientBase/PackageReadme.md b/src/Obsolete/Steeltoe.Discovery.ClientBase/PackageReadme.md deleted file mode 100644 index 0138850c57..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Discovery.Configuration` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Discovery.ClientBase/Steeltoe.Discovery.ClientBase.csproj b/src/Obsolete/Steeltoe.Discovery.ClientBase/Steeltoe.Discovery.ClientBase.csproj deleted file mode 100644 index ae4596b9a7..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientBase/Steeltoe.Discovery.ClientBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Base package for using Steeltoe Service Discovery. - service discovery;service registry;Spring Cloud;eureka;consul;kubernetes - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.ClientCore/Build/Steeltoe.Discovery.ClientCore.targets b/src/Obsolete/Steeltoe.Discovery.ClientCore/Build/Steeltoe.Discovery.ClientCore.targets deleted file mode 100644 index 235e1127dd..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientCore/Build/Steeltoe.Discovery.ClientCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.ClientCore/PackageReadme.md b/src/Obsolete/Steeltoe.Discovery.ClientCore/PackageReadme.md deleted file mode 100644 index 0138850c57..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Discovery.Configuration` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Discovery.ClientCore/Steeltoe.Discovery.ClientCore.csproj b/src/Obsolete/Steeltoe.Discovery.ClientCore/Steeltoe.Discovery.ClientCore.csproj deleted file mode 100644 index 7bb699582c..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.ClientCore/Steeltoe.Discovery.ClientCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using Steeltoe Service Discovery Client in ASP.NET Core applications. - aspnetcore;service discovery;service registry;Spring Cloud;eureka;consul;kubernetes - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.Kubernetes/Build/Steeltoe.Discovery.Kubernetes.targets b/src/Obsolete/Steeltoe.Discovery.Kubernetes/Build/Steeltoe.Discovery.Kubernetes.targets deleted file mode 100644 index 418fc9cf31..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Kubernetes/Build/Steeltoe.Discovery.Kubernetes.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Discovery.Kubernetes/PackageReadme.md b/src/Obsolete/Steeltoe.Discovery.Kubernetes/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Kubernetes/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Discovery.Kubernetes/Steeltoe.Discovery.Kubernetes.csproj b/src/Obsolete/Steeltoe.Discovery.Kubernetes/Steeltoe.Discovery.Kubernetes.csproj deleted file mode 100644 index a5f829127a..0000000000 --- a/src/Obsolete/Steeltoe.Discovery.Kubernetes/Steeltoe.Discovery.Kubernetes.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Client for service discovery with Kubernetes native service discovery. - aspnetcore;Kubernetes;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Build/Steeltoe.Extensions.Configuration.Abstractions.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Build/Steeltoe.Extensions.Configuration.Abstractions.targets deleted file mode 100644 index d84bc174ce..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Build/Steeltoe.Extensions.Configuration.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/PackageReadme.md deleted file mode 100644 index 0d52dac0d5..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.Abstractions` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Steeltoe.Extensions.Configuration.Abstractions.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Steeltoe.Extensions.Configuration.Abstractions.csproj deleted file mode 100644 index 9f2c573e48..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Abstractions/Steeltoe.Extensions.Configuration.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions used in Steeltoe Configuration libraries. - configuration;spring boot - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Build/Steeltoe.Extensions.Configuration.CloudFoundryBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Build/Steeltoe.Extensions.Configuration.CloudFoundryBase.targets deleted file mode 100644 index d1c4d2be2f..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Build/Steeltoe.Extensions.Configuration.CloudFoundryBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/PackageReadme.md deleted file mode 100644 index 0886311b76..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.CloudFoundry` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Steeltoe.Extensions.Configuration.CloudFoundryBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Steeltoe.Extensions.Configuration.CloudFoundryBase.csproj deleted file mode 100644 index 5fbd4ba3c8..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryBase/Steeltoe.Extensions.Configuration.CloudFoundryBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration Provider for reading Cloud Foundry Environment Variables. - configuration;CloudFoundry;vcap - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Build/Steeltoe.Extensions.Configuration.CloudFoundryCore.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Build/Steeltoe.Extensions.Configuration.CloudFoundryCore.targets deleted file mode 100644 index 35f400a323..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Build/Steeltoe.Extensions.Configuration.CloudFoundryCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/PackageReadme.md deleted file mode 100644 index 0886311b76..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.CloudFoundry` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Steeltoe.Extensions.Configuration.CloudFoundryCore.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Steeltoe.Extensions.Configuration.CloudFoundryCore.csproj deleted file mode 100644 index 07fb9d28a1..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.CloudFoundryCore/Steeltoe.Extensions.Configuration.CloudFoundryCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for adding Cloud Foundry environment variable configuration provider to ASP.NET Core applications. - aspnetcore;CloudFoundry;Spring;Spring Cloud;vcap - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Build/Steeltoe.Extensions.Configuration.ConfigServerBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Build/Steeltoe.Extensions.Configuration.ConfigServerBase.targets deleted file mode 100644 index 682b2298e1..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Build/Steeltoe.Extensions.Configuration.ConfigServerBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/PackageReadme.md deleted file mode 100644 index ff0b8ed709..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.ConfigServer` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Steeltoe.Extensions.Configuration.ConfigServerBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Steeltoe.Extensions.Configuration.ConfigServerBase.csproj deleted file mode 100644 index 0f657d42b7..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerBase/Steeltoe.Extensions.Configuration.ConfigServerBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration provider for reading from Spring Cloud Config Server. - configuration;Spring Cloud;Spring Cloud Config Server - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Build/Steeltoe.Extensions.Configuration.ConfigServerCore.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Build/Steeltoe.Extensions.Configuration.ConfigServerCore.targets deleted file mode 100644 index 9fbbf06d4b..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Build/Steeltoe.Extensions.Configuration.ConfigServerCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/PackageReadme.md deleted file mode 100644 index ff0b8ed709..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.ConfigServer` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Steeltoe.Extensions.Configuration.ConfigServerCore.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Steeltoe.Extensions.Configuration.ConfigServerCore.csproj deleted file mode 100644 index 8745390baf..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.ConfigServerCore/Steeltoe.Extensions.Configuration.ConfigServerCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for adding Spring Cloud Config Server configuration provider to ASP.NET Core applications. - aspnetcore;Spring Cloud;Spring Cloud Config Server - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Build/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Build/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.targets deleted file mode 100644 index c38b683dc3..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Build/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/PackageReadme.md deleted file mode 100644 index 64cd1b85fe..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.Kubernetes.ServiceBindings` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.csproj deleted file mode 100644 index 4017414592..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding/Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration Provider for reading Kubernetes Service Bindings. - Configuration;Kubernetes;Services;Bindings - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Build/Steeltoe.Extensions.Configuration.KubernetesBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Build/Steeltoe.Extensions.Configuration.KubernetesBase.targets deleted file mode 100644 index 46e05db9b1..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Build/Steeltoe.Extensions.Configuration.KubernetesBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Steeltoe.Extensions.Configuration.KubernetesBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Steeltoe.Extensions.Configuration.KubernetesBase.csproj deleted file mode 100644 index 21c169dbdf..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesBase/Steeltoe.Extensions.Configuration.KubernetesBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration Provider for reading Cloud Foundry Environment Variables. - configuration;Kubernetes - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Build/Steeltoe.Extensions.Configuration.KubernetesCore.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Build/Steeltoe.Extensions.Configuration.KubernetesCore.targets deleted file mode 100644 index 9eddcafcb4..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Build/Steeltoe.Extensions.Configuration.KubernetesCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Steeltoe.Extensions.Configuration.KubernetesCore.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Steeltoe.Extensions.Configuration.KubernetesCore.csproj deleted file mode 100644 index efdf80788e..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.KubernetesCore/Steeltoe.Extensions.Configuration.KubernetesCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for adding Kubernetes environment variables, ConfigMaps and Secrets to .NET applications. - Kubernetes;Spring;Spring Cloud;configuration;configmap - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Build/Steeltoe.Extensions.Configuration.PlaceholderBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Build/Steeltoe.Extensions.Configuration.PlaceholderBase.targets deleted file mode 100644 index f3e899d71d..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Build/Steeltoe.Extensions.Configuration.PlaceholderBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/PackageReadme.md deleted file mode 100644 index 7a2cb0dad7..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.Placeholder` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Steeltoe.Extensions.Configuration.PlaceholderBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Steeltoe.Extensions.Configuration.PlaceholderBase.csproj deleted file mode 100644 index d4049b5eb2..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderBase/Steeltoe.Extensions.Configuration.PlaceholderBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration provider for resolving property placeholders in configuration values. - configuration;placeholders;spring boot - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Build/Steeltoe.Extensions.Configuration.PlaceholderCore.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Build/Steeltoe.Extensions.Configuration.PlaceholderCore.targets deleted file mode 100644 index 1bf089a2fb..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Build/Steeltoe.Extensions.Configuration.PlaceholderCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/PackageReadme.md deleted file mode 100644 index 7a2cb0dad7..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.Placeholder` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Steeltoe.Extensions.Configuration.PlaceholderCore.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Steeltoe.Extensions.Configuration.PlaceholderCore.csproj deleted file mode 100644 index 882d3186a2..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.PlaceholderCore/Steeltoe.Extensions.Configuration.PlaceholderCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for adding property placeholder resolving config provider to ASP.NET Core applications. - configuration;placeholders;aspnetcore;spring boot - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Build/Steeltoe.Extensions.Configuration.RandomValueBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Build/Steeltoe.Extensions.Configuration.RandomValueBase.targets deleted file mode 100644 index 8d322d52b6..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Build/Steeltoe.Extensions.Configuration.RandomValueBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/PackageReadme.md deleted file mode 100644 index 8e49bb4281..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.RandomValue` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Steeltoe.Extensions.Configuration.RandomValueBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Steeltoe.Extensions.Configuration.RandomValueBase.csproj deleted file mode 100644 index 7566376719..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.RandomValueBase/Steeltoe.Extensions.Configuration.RandomValueBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration provider for generating random values. - configuration;random values - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Build/Steeltoe.Extensions.Configuration.SpringBootBase.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Build/Steeltoe.Extensions.Configuration.SpringBootBase.targets deleted file mode 100644 index d8aa14c328..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Build/Steeltoe.Extensions.Configuration.SpringBootBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/PackageReadme.md deleted file mode 100644 index a1409eec02..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.SpringBoot` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Steeltoe.Extensions.Configuration.SpringBootBase.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Steeltoe.Extensions.Configuration.SpringBootBase.csproj deleted file mode 100644 index 56f603b646..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootBase/Steeltoe.Extensions.Configuration.SpringBootBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration provider for reading Spring Boot style configuration. - configuration;springboot - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Build/Steeltoe.Extensions.Configuration.SpringBootCore.targets b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Build/Steeltoe.Extensions.Configuration.SpringBootCore.targets deleted file mode 100644 index 6eab894027..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Build/Steeltoe.Extensions.Configuration.SpringBootCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/PackageReadme.md deleted file mode 100644 index a1409eec02..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Configuration.SpringBoot` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Steeltoe.Extensions.Configuration.SpringBootCore.csproj b/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Steeltoe.Extensions.Configuration.SpringBootCore.csproj deleted file mode 100644 index 56f603b646..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Configuration.SpringBootCore/Steeltoe.Extensions.Configuration.SpringBootCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Configuration provider for reading Spring Boot style configuration. - configuration;springboot - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Build/Steeltoe.Extensions.Logging.Abstractions.targets b/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Build/Steeltoe.Extensions.Logging.Abstractions.targets deleted file mode 100644 index 36f4043565..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Build/Steeltoe.Extensions.Logging.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/PackageReadme.md deleted file mode 100644 index e6e1a26e91..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Logging.Abstractions` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Steeltoe.Extensions.Logging.Abstractions.csproj b/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Steeltoe.Extensions.Logging.Abstractions.csproj deleted file mode 100644 index ba13f09780..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.Abstractions/Steeltoe.Extensions.Logging.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for use with dynamic logging. - logging;dynamic logging;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Build/Steeltoe.Extensions.Logging.DynamicLogger.targets b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Build/Steeltoe.Extensions.Logging.DynamicLogger.targets deleted file mode 100644 index 1224f18289..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Build/Steeltoe.Extensions.Logging.DynamicLogger.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/PackageReadme.md deleted file mode 100644 index 6696ca487c..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Logging.DynamicConsole` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Steeltoe.Extensions.Logging.DynamicLogger.csproj b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Steeltoe.Extensions.Logging.DynamicLogger.csproj deleted file mode 100644 index 4412937b49..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicLogger/Steeltoe.Extensions.Logging.DynamicLogger.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Dynamic Console Logger. - logging;dynamic logging;console;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Build/Steeltoe.Extensions.Logging.DynamicSerilogBase.targets b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Build/Steeltoe.Extensions.Logging.DynamicSerilogBase.targets deleted file mode 100644 index a21adb7989..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Build/Steeltoe.Extensions.Logging.DynamicSerilogBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/PackageReadme.md deleted file mode 100644 index 2e596ac382..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Logging.DynamicSerilog` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Steeltoe.Extensions.Logging.DynamicSerilogBase.csproj b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Steeltoe.Extensions.Logging.DynamicSerilogBase.csproj deleted file mode 100644 index d6e6fa7895..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogBase/Steeltoe.Extensions.Logging.DynamicSerilogBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe library for enabling dynamic management of Serilog. - logging;dynamic logging;serilog;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Build/Steeltoe.Extensions.Logging.DynamicSerilogCore.targets b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Build/Steeltoe.Extensions.Logging.DynamicSerilogCore.targets deleted file mode 100644 index 73ca107c27..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Build/Steeltoe.Extensions.Logging.DynamicSerilogCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/PackageReadme.md b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/PackageReadme.md deleted file mode 100644 index 2e596ac382..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Logging.DynamicSerilog` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Steeltoe.Extensions.Logging.DynamicSerilogCore.csproj b/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Steeltoe.Extensions.Logging.DynamicSerilogCore.csproj deleted file mode 100644 index 49ce002d56..0000000000 --- a/src/Obsolete/Steeltoe.Extensions.Logging.DynamicSerilogCore/Steeltoe.Extensions.Logging.DynamicSerilogCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe library for enabling dynamic management of Serilog in ASP.NET Core applications. Includes Console sink. - logging;dynamic logging;serilog;aspnetcore;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.Abstractions/Build/Steeltoe.Integration.Abstractions.targets b/src/Obsolete/Steeltoe.Integration.Abstractions/Build/Steeltoe.Integration.Abstractions.targets deleted file mode 100644 index b26bcaca9d..0000000000 --- a/src/Obsolete/Steeltoe.Integration.Abstractions/Build/Steeltoe.Integration.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Integration.Abstractions/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Integration.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Integration.Abstractions/Steeltoe.Integration.Abstractions.csproj b/src/Obsolete/Steeltoe.Integration.Abstractions/Steeltoe.Integration.Abstractions.csproj deleted file mode 100644 index 6fea06cf5a..0000000000 --- a/src/Obsolete/Steeltoe.Integration.Abstractions/Steeltoe.Integration.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for use with Steeltoe Integration libraries. - Integration;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.IntegrationBase/Build/Steeltoe.Integration.IntegrationBase.targets b/src/Obsolete/Steeltoe.Integration.IntegrationBase/Build/Steeltoe.Integration.IntegrationBase.targets deleted file mode 100644 index 871662c291..0000000000 --- a/src/Obsolete/Steeltoe.Integration.IntegrationBase/Build/Steeltoe.Integration.IntegrationBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.IntegrationBase/PackageReadme.md b/src/Obsolete/Steeltoe.Integration.IntegrationBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Integration.IntegrationBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Integration.IntegrationBase/Steeltoe.Integration.IntegrationBase.csproj b/src/Obsolete/Steeltoe.Integration.IntegrationBase/Steeltoe.Integration.IntegrationBase.csproj deleted file mode 100644 index 54338ac6a0..0000000000 --- a/src/Obsolete/Steeltoe.Integration.IntegrationBase/Steeltoe.Integration.IntegrationBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Integration Base. - Integration;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.RabbitMQ/Build/Steeltoe.Integration.RabbitMQ.targets b/src/Obsolete/Steeltoe.Integration.RabbitMQ/Build/Steeltoe.Integration.RabbitMQ.targets deleted file mode 100644 index 97750f85e1..0000000000 --- a/src/Obsolete/Steeltoe.Integration.RabbitMQ/Build/Steeltoe.Integration.RabbitMQ.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Integration.RabbitMQ/PackageReadme.md b/src/Obsolete/Steeltoe.Integration.RabbitMQ/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Integration.RabbitMQ/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Integration.RabbitMQ/Steeltoe.Integration.RabbitMQ.csproj b/src/Obsolete/Steeltoe.Integration.RabbitMQ/Steeltoe.Integration.RabbitMQ.csproj deleted file mode 100644 index c60a3f2d36..0000000000 --- a/src/Obsolete/Steeltoe.Integration.RabbitMQ/Steeltoe.Integration.RabbitMQ.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Integration RabbitMQ. - Integration;ASPNET Core;Spring;Spring Cloud;RabbitMQ - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Build/Steeltoe.Management.CloudFoundryCore.targets b/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Build/Steeltoe.Management.CloudFoundryCore.targets deleted file mode 100644 index b0d8d21205..0000000000 --- a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Build/Steeltoe.Management.CloudFoundryCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/PackageReadme.md b/src/Obsolete/Steeltoe.Management.CloudFoundryCore/PackageReadme.md deleted file mode 100644 index ccaa917bf0..0000000000 --- a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Endpoint` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Steeltoe.Management.CloudFoundryCore.csproj b/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Steeltoe.Management.CloudFoundryCore.csproj deleted file mode 100644 index fc75ecfea6..0000000000 --- a/src/Obsolete/Steeltoe.Management.CloudFoundryCore/Steeltoe.Management.CloudFoundryCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using Steeltoe management endpoints with ASP.NET Core on Cloud Foundry. - actuator;management;monitoring;aspnetcore;CloudFoundry;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.Diagnostics/Build/Steeltoe.Management.Diagnostics.targets b/src/Obsolete/Steeltoe.Management.Diagnostics/Build/Steeltoe.Management.Diagnostics.targets deleted file mode 100644 index 3c99503b09..0000000000 --- a/src/Obsolete/Steeltoe.Management.Diagnostics/Build/Steeltoe.Management.Diagnostics.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.Diagnostics/PackageReadme.md b/src/Obsolete/Steeltoe.Management.Diagnostics/PackageReadme.md deleted file mode 100644 index ccaa917bf0..0000000000 --- a/src/Obsolete/Steeltoe.Management.Diagnostics/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Endpoint` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.Diagnostics/Steeltoe.Management.Diagnostics.csproj b/src/Obsolete/Steeltoe.Management.Diagnostics/Steeltoe.Management.Diagnostics.csproj deleted file mode 100644 index fe9fe4476a..0000000000 --- a/src/Obsolete/Steeltoe.Management.Diagnostics/Steeltoe.Management.Diagnostics.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Management Runtime Diagnostics. - actuator;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.EndpointBase/Build/Steeltoe.Management.EndpointBase.targets b/src/Obsolete/Steeltoe.Management.EndpointBase/Build/Steeltoe.Management.EndpointBase.targets deleted file mode 100644 index f4d5a60157..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointBase/Build/Steeltoe.Management.EndpointBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.EndpointBase/PackageReadme.md b/src/Obsolete/Steeltoe.Management.EndpointBase/PackageReadme.md deleted file mode 100644 index ccaa917bf0..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Endpoint` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.EndpointBase/Steeltoe.Management.EndpointBase.csproj b/src/Obsolete/Steeltoe.Management.EndpointBase/Steeltoe.Management.EndpointBase.csproj deleted file mode 100644 index fb2eb15ceb..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointBase/Steeltoe.Management.EndpointBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe management endpoints. - actuator;management;monitoring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.EndpointCore/Build/Steeltoe.Management.EndpointCore.targets b/src/Obsolete/Steeltoe.Management.EndpointCore/Build/Steeltoe.Management.EndpointCore.targets deleted file mode 100644 index 74eebadb14..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointCore/Build/Steeltoe.Management.EndpointCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.EndpointCore/PackageReadme.md b/src/Obsolete/Steeltoe.Management.EndpointCore/PackageReadme.md deleted file mode 100644 index ccaa917bf0..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Endpoint` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.EndpointCore/Steeltoe.Management.EndpointCore.csproj b/src/Obsolete/Steeltoe.Management.EndpointCore/Steeltoe.Management.EndpointCore.csproj deleted file mode 100644 index 8509367149..0000000000 --- a/src/Obsolete/Steeltoe.Management.EndpointCore/Steeltoe.Management.EndpointCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using Steeltoe management endpoints with ASP.NET Core. - Spring Cloud;Actuator;Management;Monitoring - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.KubernetesCore/Build/Steeltoe.Management.KubernetesCore.targets b/src/Obsolete/Steeltoe.Management.KubernetesCore/Build/Steeltoe.Management.KubernetesCore.targets deleted file mode 100644 index 4b0876b2d8..0000000000 --- a/src/Obsolete/Steeltoe.Management.KubernetesCore/Build/Steeltoe.Management.KubernetesCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.KubernetesCore/PackageReadme.md b/src/Obsolete/Steeltoe.Management.KubernetesCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Management.KubernetesCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.KubernetesCore/Steeltoe.Management.KubernetesCore.csproj b/src/Obsolete/Steeltoe.Management.KubernetesCore/Steeltoe.Management.KubernetesCore.csproj deleted file mode 100644 index b3a6d6d2c4..0000000000 --- a/src/Obsolete/Steeltoe.Management.KubernetesCore/Steeltoe.Management.KubernetesCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Package for using Steeltoe management endpoints with ASP.NET Core on Kubernetes. - actuator;management;monitoring;aspnetcore;Kubernetes;Spring Cloud;k8s - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Build/Steeltoe.Management.OpenTelemetryBase.targets b/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Build/Steeltoe.Management.OpenTelemetryBase.targets deleted file mode 100644 index c2600f24d7..0000000000 --- a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Build/Steeltoe.Management.OpenTelemetryBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/PackageReadme.md b/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/PackageReadme.md deleted file mode 100644 index cc3a7d5d86..0000000000 --- a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Prometheus` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Steeltoe.Management.OpenTelemetryBase.csproj b/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Steeltoe.Management.OpenTelemetryBase.csproj deleted file mode 100644 index 000fcbb965..0000000000 --- a/src/Obsolete/Steeltoe.Management.OpenTelemetryBase/Steeltoe.Management.OpenTelemetryBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Management OpenTelemetry. - Tracing;OpenTelemetry;Management;Monitoring - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TaskCore/Build/Steeltoe.Management.TaskCore.targets b/src/Obsolete/Steeltoe.Management.TaskCore/Build/Steeltoe.Management.TaskCore.targets deleted file mode 100644 index 3ae7b04a2a..0000000000 --- a/src/Obsolete/Steeltoe.Management.TaskCore/Build/Steeltoe.Management.TaskCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TaskCore/PackageReadme.md b/src/Obsolete/Steeltoe.Management.TaskCore/PackageReadme.md deleted file mode 100644 index 3bf83d7451..0000000000 --- a/src/Obsolete/Steeltoe.Management.TaskCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Tasks` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.TaskCore/Steeltoe.Management.TaskCore.csproj b/src/Obsolete/Steeltoe.Management.TaskCore/Steeltoe.Management.TaskCore.csproj deleted file mode 100644 index 7c42c957a0..0000000000 --- a/src/Obsolete/Steeltoe.Management.TaskCore/Steeltoe.Management.TaskCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Extensions for running tasks embedded in your ASP.NET Core application. Ideal for cf run-task in Cloud Foundry. - tasks;management;monitoring;aspnetcore;Spring Cloud;cf run-task - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TracingBase/Build/Steeltoe.Management.TracingBase.targets b/src/Obsolete/Steeltoe.Management.TracingBase/Build/Steeltoe.Management.TracingBase.targets deleted file mode 100644 index 0a391d67fc..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingBase/Build/Steeltoe.Management.TracingBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TracingBase/PackageReadme.md b/src/Obsolete/Steeltoe.Management.TracingBase/PackageReadme.md deleted file mode 100644 index 324240b977..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Tracing` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.TracingBase/Steeltoe.Management.TracingBase.csproj b/src/Obsolete/Steeltoe.Management.TracingBase/Steeltoe.Management.TracingBase.csproj deleted file mode 100644 index 8273df78c4..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingBase/Steeltoe.Management.TracingBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Base package for enabling request tracing in distributed systems. - management;monitoring;distributed trace - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TracingCore/Build/Steeltoe.Management.TracingCore.targets b/src/Obsolete/Steeltoe.Management.TracingCore/Build/Steeltoe.Management.TracingCore.targets deleted file mode 100644 index 05b9df9337..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingCore/Build/Steeltoe.Management.TracingCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Management.TracingCore/PackageReadme.md b/src/Obsolete/Steeltoe.Management.TracingCore/PackageReadme.md deleted file mode 100644 index 324240b977..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Management.Tracing` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Management.TracingCore/Steeltoe.Management.TracingCore.csproj b/src/Obsolete/Steeltoe.Management.TracingCore/Steeltoe.Management.TracingCore.csproj deleted file mode 100644 index 00d4ca2d3f..0000000000 --- a/src/Obsolete/Steeltoe.Management.TracingCore/Steeltoe.Management.TracingCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Add distributed tracing to ASP.NET Core applications. - aspnetcore;management;monitoring;metrics;Distributed Trace - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.Abstractions/Build/Steeltoe.Messaging.Abstractions.targets b/src/Obsolete/Steeltoe.Messaging.Abstractions/Build/Steeltoe.Messaging.Abstractions.targets deleted file mode 100644 index 37bafaa6c0..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.Abstractions/Build/Steeltoe.Messaging.Abstractions.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Messaging.Abstractions/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Messaging.Abstractions/Steeltoe.Messaging.Abstractions.csproj b/src/Obsolete/Steeltoe.Messaging.Abstractions/Steeltoe.Messaging.Abstractions.csproj deleted file mode 100644 index c768e75a56..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.Abstractions/Steeltoe.Messaging.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for use with Steeltoe Messaging. - Messaging;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.MessagingBase/Build/Steeltoe.Messaging.MessagingBase.targets b/src/Obsolete/Steeltoe.Messaging.MessagingBase/Build/Steeltoe.Messaging.MessagingBase.targets deleted file mode 100644 index a72ae0044f..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.MessagingBase/Build/Steeltoe.Messaging.MessagingBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.MessagingBase/PackageReadme.md b/src/Obsolete/Steeltoe.Messaging.MessagingBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.MessagingBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Messaging.MessagingBase/Steeltoe.Messaging.MessagingBase.csproj b/src/Obsolete/Steeltoe.Messaging.MessagingBase/Steeltoe.Messaging.MessagingBase.csproj deleted file mode 100644 index ca4d4965ca..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.MessagingBase/Steeltoe.Messaging.MessagingBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Messaging Base. - Messaging;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Build/Steeltoe.Messaging.RabbitMQ.targets b/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Build/Steeltoe.Messaging.RabbitMQ.targets deleted file mode 100644 index 2ba7ed0997..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Build/Steeltoe.Messaging.RabbitMQ.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/PackageReadme.md b/src/Obsolete/Steeltoe.Messaging.RabbitMQ/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Steeltoe.Messaging.RabbitMQ.csproj b/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Steeltoe.Messaging.RabbitMQ.csproj deleted file mode 100644 index 32584dfe81..0000000000 --- a/src/Obsolete/Steeltoe.Messaging.RabbitMQ/Steeltoe.Messaging.RabbitMQ.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Messaging RabbitMQ. - Messaging;ASPNET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Build/Steeltoe.Security.Authentication.CloudFoundryBase.targets b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Build/Steeltoe.Security.Authentication.CloudFoundryBase.targets deleted file mode 100644 index 8ecc94edbc..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Build/Steeltoe.Security.Authentication.CloudFoundryBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/PackageReadme.md b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/PackageReadme.md deleted file mode 100644 index d3dc634352..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Security.Authentication.JwtBearer` / `Steeltoe.Security.Authentication.OpenIdConnect` / `Steeltoe.Security.Authorization.Certificate` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Steeltoe.Security.Authentication.CloudFoundryBase.csproj b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Steeltoe.Security.Authentication.CloudFoundryBase.csproj deleted file mode 100644 index ff75263b86..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryBase/Steeltoe.Security.Authentication.CloudFoundryBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Base Security Provider for CloudFoundry. - CloudFoundry;security;oauth2;sso;openid - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Build/Steeltoe.Security.Authentication.CloudFoundryCore.targets b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Build/Steeltoe.Security.Authentication.CloudFoundryCore.targets deleted file mode 100644 index 98248be8ff..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Build/Steeltoe.Security.Authentication.CloudFoundryCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/PackageReadme.md b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/PackageReadme.md deleted file mode 100644 index d3dc634352..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Security.Authentication.JwtBearer` / `Steeltoe.Security.Authentication.OpenIdConnect` / `Steeltoe.Security.Authorization.Certificate` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Steeltoe.Security.Authentication.CloudFoundryCore.csproj b/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Steeltoe.Security.Authentication.CloudFoundryCore.csproj deleted file mode 100644 index 4166c11874..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.CloudFoundryCore/Steeltoe.Security.Authentication.CloudFoundryCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - ASP.NET Core External Security Provider for CloudFoundry. - CloudFoundry;ASPNET Core;Security;OAuth2;SSO;OpenIDConnect - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Build/Steeltoe.Security.Authentication.MtlsCore.targets b/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Build/Steeltoe.Security.Authentication.MtlsCore.targets deleted file mode 100644 index f60cbec70b..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Build/Steeltoe.Security.Authentication.MtlsCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/PackageReadme.md b/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/PackageReadme.md deleted file mode 100644 index c06f35e8bf..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Security.Authorization.Certificate` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Steeltoe.Security.Authentication.MtlsCore.csproj b/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Steeltoe.Security.Authentication.MtlsCore.csproj deleted file mode 100644 index 0571350076..0000000000 --- a/src/Obsolete/Steeltoe.Security.Authentication.MtlsCore/Steeltoe.Security.Authentication.MtlsCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - ASP.NET Core middleware that enables an application to support certificate authentication. - aspnetcore;authentication;security;x509;certificate;mtls - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Build/Steeltoe.Security.DataProtection.CredHubBase.targets b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Build/Steeltoe.Security.DataProtection.CredHubBase.targets deleted file mode 100644 index 9b31f9dbfb..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Build/Steeltoe.Security.DataProtection.CredHubBase.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/PackageReadme.md b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Steeltoe.Security.DataProtection.CredHubBase.csproj b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Steeltoe.Security.DataProtection.CredHubBase.csproj deleted file mode 100644 index 328a3d8393..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubBase/Steeltoe.Security.DataProtection.CredHubBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - NET Client for CredHub - Base Package. - CloudFoundry;NET Core;Security;DataProtection;CredHub - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Build/Steeltoe.Security.DataProtection.CredHubCore.targets b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Build/Steeltoe.Security.DataProtection.CredHubCore.targets deleted file mode 100644 index 96e580d73a..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Build/Steeltoe.Security.DataProtection.CredHubCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/PackageReadme.md b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Steeltoe.Security.DataProtection.CredHubCore.csproj b/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Steeltoe.Security.DataProtection.CredHubCore.csproj deleted file mode 100644 index 6da6837b51..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.CredHubCore/Steeltoe.Security.DataProtection.CredHubCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - ASP.NET Core Extensions for CredHub Client. - CloudFoundry;aspnetcore;Security;DataProtection;CredHub - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Build/Steeltoe.Security.DataProtection.RedisCore.targets b/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Build/Steeltoe.Security.DataProtection.RedisCore.targets deleted file mode 100644 index 8be8a421e6..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Build/Steeltoe.Security.DataProtection.RedisCore.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/PackageReadme.md b/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/PackageReadme.md deleted file mode 100644 index 5b8a6b20da..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been superseded in Steeltoe v4. Reference `Steeltoe.Security.DataProtection.Redis` instead. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Steeltoe.Security.DataProtection.RedisCore.csproj b/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Steeltoe.Security.DataProtection.RedisCore.csproj deleted file mode 100644 index 254a8ba0ce..0000000000 --- a/src/Obsolete/Steeltoe.Security.DataProtection.RedisCore/Steeltoe.Security.DataProtection.RedisCore.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Support for storing data protection keys in Redis. - CloudFoundry;aspnetcore;security;dataprotection;redis - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.Abstractions/Build/Steeltoe.Stream.Abstractions.targets b/src/Obsolete/Steeltoe.Stream.Abstractions/Build/Steeltoe.Stream.Abstractions.targets deleted file mode 100644 index 336ee776e1..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Abstractions/Build/Steeltoe.Stream.Abstractions.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.Abstractions/PackageReadme.md b/src/Obsolete/Steeltoe.Stream.Abstractions/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Abstractions/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Stream.Abstractions/Steeltoe.Stream.Abstractions.csproj b/src/Obsolete/Steeltoe.Stream.Abstractions/Steeltoe.Stream.Abstractions.csproj deleted file mode 100644 index 0c5e0bcac9..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Abstractions/Steeltoe.Stream.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Abstractions for use with Steeltoe Stream. - Streams;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Build/Steeltoe.Stream.Binder.RabbitMQ.targets b/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Build/Steeltoe.Stream.Binder.RabbitMQ.targets deleted file mode 100644 index 4de530b280..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Build/Steeltoe.Stream.Binder.RabbitMQ.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/PackageReadme.md b/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Steeltoe.Stream.Binder.RabbitMQ.csproj b/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Steeltoe.Stream.Binder.RabbitMQ.csproj deleted file mode 100644 index 5b5cb8e9d5..0000000000 --- a/src/Obsolete/Steeltoe.Stream.Binder.RabbitMQ/Steeltoe.Stream.Binder.RabbitMQ.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Stream RabbitMQ Binder. - Streams;ASPNET Core;Spring;Spring Cloud;RabbitMQ;Binder - true - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.StreamBase/Build/Steeltoe.Stream.StreamBase.targets b/src/Obsolete/Steeltoe.Stream.StreamBase/Build/Steeltoe.Stream.StreamBase.targets deleted file mode 100644 index 8c974c9287..0000000000 --- a/src/Obsolete/Steeltoe.Stream.StreamBase/Build/Steeltoe.Stream.StreamBase.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/Obsolete/Steeltoe.Stream.StreamBase/PackageReadme.md b/src/Obsolete/Steeltoe.Stream.StreamBase/PackageReadme.md deleted file mode 100644 index 4db75ab196..0000000000 --- a/src/Obsolete/Steeltoe.Stream.StreamBase/PackageReadme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Steeltoe - -> [!IMPORTANT] -> This package has been removed from Steeltoe in v4. -> See for details. - -[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu). - -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. -- 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/) - -For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://steeltoe.io/docs). diff --git a/src/Obsolete/Steeltoe.Stream.StreamBase/Steeltoe.Stream.StreamBase.csproj b/src/Obsolete/Steeltoe.Stream.StreamBase/Steeltoe.Stream.StreamBase.csproj deleted file mode 100644 index 0b83621d24..0000000000 --- a/src/Obsolete/Steeltoe.Stream.StreamBase/Steeltoe.Stream.StreamBase.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net8.0 - Steeltoe Stream Base. - Streams;NET Core;Spring;Spring Cloud - true - - - - - - - - - diff --git a/src/Security/src/Authentication.JwtBearer/ConfigurationSchema.json b/src/Security/src/Authentication.JwtBearer/ConfigurationSchema.json new file mode 100644 index 0000000000..215b1d2242 --- /dev/null +++ b/src/Security/src/Authentication.JwtBearer/ConfigurationSchema.json @@ -0,0 +1,20 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.Authentication": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.Authentication.JwtBearer": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Security/src/Authentication.JwtBearer/JwtBearerAuthenticationBuilderExtensions.cs b/src/Security/src/Authentication.JwtBearer/JwtBearerAuthenticationBuilderExtensions.cs index 4ad1e9c1a4..535d383ebb 100644 --- a/src/Security/src/Authentication.JwtBearer/JwtBearerAuthenticationBuilderExtensions.cs +++ b/src/Security/src/Authentication.JwtBearer/JwtBearerAuthenticationBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Steeltoe.Security.Authentication.JwtBearer; @@ -24,6 +25,8 @@ public static AuthenticationBuilder ConfigureJwtBearerForCloudFoundry(this Authe { ArgumentNullException.ThrowIfNull(builder); + builder.Services.TryAddSingleton(TimeProvider.System); + builder.Services.TryAddSingleton(); builder.Services.AddSingleton, PostConfigureJwtBearerOptions>(); return builder; } diff --git a/src/Security/src/Authentication.JwtBearer/PostConfigureJwtBearerOptions.cs b/src/Security/src/Authentication.JwtBearer/PostConfigureJwtBearerOptions.cs index 5e14a8e94a..73d96d3793 100644 --- a/src/Security/src/Authentication.JwtBearer/PostConfigureJwtBearerOptions.cs +++ b/src/Security/src/Authentication.JwtBearer/PostConfigureJwtBearerOptions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Steeltoe.Common; namespace Steeltoe.Security.Authentication.JwtBearer; @@ -13,12 +14,15 @@ internal sealed class PostConfigureJwtBearerOptions : IPostConfigureOptions keyResolver.ResolveSigningKey(keyId); + options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, keyId, _) => + { + JsonWebKey? key = _tokenKeyResolver.ResolveSigningKey(options.Authority, keyId, options.Backchannel); + return key != null ? [key] : []; + }; } } diff --git a/src/Security/src/Authentication.JwtBearer/Properties/AssemblyInfo.cs b/src/Security/src/Authentication.JwtBearer/Properties/AssemblyInfo.cs index 5dbdb9307b..0b844a8323 100644 --- a/src/Security/src/Authentication.JwtBearer/Properties/AssemblyInfo.cs +++ b/src/Security/src/Authentication.JwtBearer/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.Security", "Steeltoe.Security.Authentication", "Steeltoe.Security.Authentication.JwtBearer")] [assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.JwtBearer.Test")] diff --git a/src/Security/src/Authentication.JwtBearer/Steeltoe.Security.Authentication.JwtBearer.csproj b/src/Security/src/Authentication.JwtBearer/Steeltoe.Security.Authentication.JwtBearer.csproj index 20eb1cf039..545c8e719b 100644 --- a/src/Security/src/Authentication.JwtBearer/Steeltoe.Security.Authentication.JwtBearer.csproj +++ b/src/Security/src/Authentication.JwtBearer/Steeltoe.Security.Authentication.JwtBearer.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Library for using JWT Bearer tokens with UAA-based systems, including Cloud Foundry. CloudFoundry;uaa;security;jwt;bearer;tanzu;aspnetcore true @@ -10,6 +10,7 @@ + diff --git a/src/Security/src/Authentication.JwtBearer/TokenKeyResolver.cs b/src/Security/src/Authentication.JwtBearer/TokenKeyResolver.cs index 2085ddca96..87ce352ab4 100644 --- a/src/Security/src/Authentication.JwtBearer/TokenKeyResolver.cs +++ b/src/Security/src/Authentication.JwtBearer/TokenKeyResolver.cs @@ -2,76 +2,181 @@ // 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.Collections.Concurrent; using System.Net.Http.Headers; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using Steeltoe.Common.Extensions; namespace Steeltoe.Security.Authentication.JwtBearer; -internal sealed class TokenKeyResolver +internal sealed partial class TokenKeyResolver : IDisposable { private static readonly MediaTypeWithQualityHeaderValue AcceptHeader = new("application/json"); - private readonly HttpClient _httpClient; - private readonly Uri _authorityUri; + private static readonly TimeSpan CacheTimeToLiveForKeyFound = TimeSpan.FromHours(12); + private static readonly TimeSpan CacheMinTimeToLiveForKeyNotFound = TimeSpan.FromSeconds(30); + private static readonly TimeSpan CacheMaxTimeToLiveForKeyNotFound = TimeSpan.FromSeconds(60); + private readonly MemoryCache _cache; + private readonly ILogger _logger; - internal static ConcurrentDictionary ResolvedSecurityKeysById { get; } = new(); + public TokenKeyResolver(TimeProvider timeProvider, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(timeProvider); + ArgumentNullException.ThrowIfNull(loggerFactory); + + _cache = new MemoryCache(new MemoryCacheOptions + { + Clock = new TimeProviderSystemClock(timeProvider) + }, loggerFactory); - public TokenKeyResolver(string authority, HttpClient httpClient) + _logger = loggerFactory.CreateLogger(); + } + + internal JsonWebKey? ResolveSigningKey(string authority, string keyId, HttpClient httpClient) { ArgumentException.ThrowIfNullOrWhiteSpace(authority); + ArgumentException.ThrowIfNullOrWhiteSpace(keyId); ArgumentNullException.ThrowIfNull(httpClient); + Uri tokenKeysUri = GetTokenKeysUri(authority); + return CachingResolveSigningKey(tokenKeysUri, keyId, httpClient); + } + + private static Uri GetTokenKeysUri(string authority) + { if (!authority.EndsWith('/')) { authority += '/'; } - _authorityUri = new Uri($"{authority}token_keys"); - _httpClient = httpClient; + var authorityUri = new Uri(authority); + return new Uri(authorityUri, "token_keys"); } - internal SecurityKey[] ResolveSigningKey(string keyId) + private JsonWebKey? CachingResolveSigningKey(Uri tokenKeysUri, string keyId, HttpClient httpClient) { - if (ResolvedSecurityKeysById.TryGetValue(keyId, out SecurityKey? resolved)) + string cacheKey = GetCacheKey(tokenKeysUri, keyId); + + if (!_cache.TryGetValue(cacheKey, out JsonWebKey? matchingWebKey)) { - return [resolved]; + JsonWebKeySet? webKeySet = FetchKeySet(tokenKeysUri, httpClient); + + foreach (JsonWebKey nextWebKey in webKeySet?.Keys ?? []) + { + string nextCacheKey = GetCacheKey(tokenKeysUri, nextWebKey.Kid); + _cache.Set(nextCacheKey, nextWebKey, CacheTimeToLiveForKeyFound); + + if (nextWebKey.Kid == keyId) + { + matchingWebKey = nextWebKey; + } + } + + if (matchingWebKey == null) + { + TimeSpan timeToLive = GetTimeToLiveForNotFound(); + _cache.Set(cacheKey, null, timeToLive); + + if (webKeySet == null) + { + LogDisableFetchAfterServerError(keyId, (int)timeToLive.TotalSeconds); + } + else + { + LogDisableFetchAfterKeyNotFound(keyId, (int)timeToLive.TotalSeconds); + } + } } + return matchingWebKey; + } + + private static string GetCacheKey(Uri tokenKeysUri, string keyId) + { + return $"{tokenKeysUri}:{keyId}"; + } + + private static TimeSpan GetTimeToLiveForNotFound() + { + double jitterSeconds = Random.Shared.NextDouble() * (CacheMaxTimeToLiveForKeyNotFound - CacheMinTimeToLiveForKeyNotFound).TotalSeconds; + return CacheMinTimeToLiveForKeyNotFound + TimeSpan.FromSeconds(jitterSeconds); + } + + private JsonWebKeySet? FetchKeySet(Uri tokenKeysUri, HttpClient httpClient) + { #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: can't be async all the way until updates are complete in Microsoft libraries // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468 - JsonWebKeySet? keySet = FetchKeySetAsync(CancellationToken.None).GetAwaiter().GetResult(); + return FetchKeySetAsync(tokenKeysUri, httpClient, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking + } + + private async Task FetchKeySetAsync(Uri tokenKeysUri, HttpClient httpClient, CancellationToken cancellationToken) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, tokenKeysUri); + requestMessage.Headers.Accept.Add(AcceptHeader); - if (keySet != null) + HttpResponseMessage response; + + try { - foreach (JsonWebKey key in keySet.Keys) - { - ResolvedSecurityKeysById[key.Kid] = key; - } + response = await httpClient.SendAsync(requestMessage, cancellationToken); + } + catch (Exception exception) when (exception is HttpRequestException || exception.IsHttpClientTimeout()) + { + LogTokenKeysEndpointUnreachable(exception, tokenKeysUri); + return null; } - if (ResolvedSecurityKeysById.TryGetValue(keyId, out resolved)) + if (!response.IsSuccessStatusCode) { - return [resolved]; + LogFetchTokenKeysStatusFailed(tokenKeysUri, (int)response.StatusCode); + return null; } - return []; + try + { + string result = await response.Content.ReadAsStringAsync(cancellationToken); + return JsonWebKeySet.Create(result); + } + catch (ArgumentException exception) + { + LogFetchTokenKeysParseFailed(exception, tokenKeysUri); + return null; + } } - internal async Task FetchKeySetAsync(CancellationToken cancellationToken) + public void Dispose() { - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, _authorityUri); - requestMessage.Headers.Accept.Add(AcceptHeader); + _cache.Dispose(); + } - HttpResponseMessage response = await _httpClient.SendAsync(requestMessage, cancellationToken); + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed.")] + private partial void LogTokenKeysEndpointUnreachable(Exception exception, MaskedUri tokenKeysUri); - if (!response.IsSuccessStatusCode) + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed with HTTP status {StatusCode}.")] + private partial void LogFetchTokenKeysStatusFailed(MaskedUri tokenKeysUri, int statusCode); + + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed because the returned JSON is invalid.")] + private partial void LogFetchTokenKeysParseFailed(Exception exception, MaskedUri tokenKeysUri); + + [LoggerMessage(LogLevel.Information, "Disabled fetch for key '{KeyId}' for {RetryAfterSeconds}s because the HTTP request failed.")] + private partial void LogDisableFetchAfterServerError(string keyId, int retryAfterSeconds); + + [LoggerMessage(LogLevel.Information, "Disabled fetch for key '{KeyId}' for {RetryAfterSeconds}s because the key was not found in the HTTP response.")] + private partial void LogDisableFetchAfterKeyNotFound(string keyId, int retryAfterSeconds); + + private sealed class TimeProviderSystemClock : ISystemClock + { + private readonly TimeProvider _timeProvider; + + public DateTimeOffset UtcNow => _timeProvider.GetUtcNow(); + + public TimeProviderSystemClock(TimeProvider timeProvider) { - return null; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } - - string result = await response.Content.ReadAsStringAsync(cancellationToken); - return JsonWebKeySet.Create(result); } } diff --git a/src/Security/src/Authentication.OpenIdConnect/ConfigurationSchema.json b/src/Security/src/Authentication.OpenIdConnect/ConfigurationSchema.json new file mode 100644 index 0000000000..59d837acda --- /dev/null +++ b/src/Security/src/Authentication.OpenIdConnect/ConfigurationSchema.json @@ -0,0 +1,20 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.Authentication": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.Authentication.OpenIdConnect": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Security/src/Authentication.OpenIdConnect/OpenIdConnectAuthenticationBuilderExtensions.cs b/src/Security/src/Authentication.OpenIdConnect/OpenIdConnectAuthenticationBuilderExtensions.cs index ffcae0c275..11dce14389 100644 --- a/src/Security/src/Authentication.OpenIdConnect/OpenIdConnectAuthenticationBuilderExtensions.cs +++ b/src/Security/src/Authentication.OpenIdConnect/OpenIdConnectAuthenticationBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Steeltoe.Security.Authentication.OpenIdConnect; @@ -24,6 +25,8 @@ public static AuthenticationBuilder ConfigureOpenIdConnectForCloudFoundry(this A { ArgumentNullException.ThrowIfNull(builder); + builder.Services.TryAddSingleton(TimeProvider.System); + builder.Services.TryAddSingleton(); builder.Services.AddSingleton, PostConfigureOpenIdConnectOptions>(); return builder; } diff --git a/src/Security/src/Authentication.OpenIdConnect/PostConfigureOpenIdConnectOptions.cs b/src/Security/src/Authentication.OpenIdConnect/PostConfigureOpenIdConnectOptions.cs index 60b26ff5dd..b3b2195d22 100644 --- a/src/Security/src/Authentication.OpenIdConnect/PostConfigureOpenIdConnectOptions.cs +++ b/src/Security/src/Authentication.OpenIdConnect/PostConfigureOpenIdConnectOptions.cs @@ -7,15 +7,28 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; namespace Steeltoe.Security.Authentication.OpenIdConnect; internal sealed class PostConfigureOpenIdConnectOptions : IPostConfigureOptions { - // The ClaimsIdentity is built off the id_token, but scopes are returned in the access_token. - // Identify scopes not already present as claims and add them to the ClaimsIdentity + private readonly TokenKeyResolver _tokenKeyResolver; + + public PostConfigureOpenIdConnectOptions(TokenKeyResolver tokenKeyResolver) + { + ArgumentNullException.ThrowIfNull(tokenKeyResolver); + + _tokenKeyResolver = tokenKeyResolver; + } + private static Task MapScopesToClaimsAsync(TokenValidatedContext context) { + ArgumentNullException.ThrowIfNull(context); + + // The ClaimsIdentity is built off the id_token, but scopes are returned in the access_token. + // Identify scopes not already present as claims and add them to the ClaimsIdentity. + if (context.Principal?.Identity is not ClaimsIdentity claimsIdentity) { return Task.CompletedTask; @@ -54,7 +67,10 @@ public void PostConfigure(string? name, OpenIdConnectOptions options) options.TokenValidationParameters.ValidIssuer = $"{options.Authority}/oauth/token"; - var keyResolver = new TokenKeyResolver(options.Authority, options.Backchannel); - options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, keyId, _) => keyResolver.ResolveSigningKey(keyId); + options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, keyId, _) => + { + JsonWebKey? key = _tokenKeyResolver.ResolveSigningKey(options.Authority, keyId, options.Backchannel); + return key != null ? [key] : []; + }; } } diff --git a/src/Security/src/Authentication.OpenIdConnect/Properties/AssemblyInfo.cs b/src/Security/src/Authentication.OpenIdConnect/Properties/AssemblyInfo.cs index c087fbbab4..cb4419c6c0 100644 --- a/src/Security/src/Authentication.OpenIdConnect/Properties/AssemblyInfo.cs +++ b/src/Security/src/Authentication.OpenIdConnect/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.Security", "Steeltoe.Security.Authentication", "Steeltoe.Security.Authentication.OpenIdConnect")] [assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.OpenIdConnect.Test")] diff --git a/src/Security/src/Authentication.OpenIdConnect/Steeltoe.Security.Authentication.OpenIdConnect.csproj b/src/Security/src/Authentication.OpenIdConnect/Steeltoe.Security.Authentication.OpenIdConnect.csproj index 427806c6ce..9be7a63572 100644 --- a/src/Security/src/Authentication.OpenIdConnect/Steeltoe.Security.Authentication.OpenIdConnect.csproj +++ b/src/Security/src/Authentication.OpenIdConnect/Steeltoe.Security.Authentication.OpenIdConnect.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Library for using OpenID Connect with UAA-based systems, including Cloud Foundry. CloudFoundry;uaa;security;sso;openid;oidc;tanzu;aspnetcore true @@ -10,6 +10,7 @@ + diff --git a/src/Security/src/Authentication.OpenIdConnect/TokenKeyResolver.cs b/src/Security/src/Authentication.OpenIdConnect/TokenKeyResolver.cs index 7b6b2f9d11..f2e55dde69 100644 --- a/src/Security/src/Authentication.OpenIdConnect/TokenKeyResolver.cs +++ b/src/Security/src/Authentication.OpenIdConnect/TokenKeyResolver.cs @@ -2,76 +2,181 @@ // 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.Collections.Concurrent; using System.Net.Http.Headers; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using Steeltoe.Common.Extensions; namespace Steeltoe.Security.Authentication.OpenIdConnect; -internal sealed class TokenKeyResolver +internal sealed partial class TokenKeyResolver : IDisposable { private static readonly MediaTypeWithQualityHeaderValue AcceptHeader = new("application/json"); - private readonly HttpClient _httpClient; - private readonly Uri _authorityUri; + private static readonly TimeSpan CacheTimeToLiveForKeyFound = TimeSpan.FromHours(12); + private static readonly TimeSpan CacheMinTimeToLiveForKeyNotFound = TimeSpan.FromSeconds(30); + private static readonly TimeSpan CacheMaxTimeToLiveForKeyNotFound = TimeSpan.FromSeconds(60); + private readonly MemoryCache _cache; + private readonly ILogger _logger; - internal static ConcurrentDictionary ResolvedSecurityKeysById { get; } = new(); + public TokenKeyResolver(TimeProvider timeProvider, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(timeProvider); + ArgumentNullException.ThrowIfNull(loggerFactory); + + _cache = new MemoryCache(new MemoryCacheOptions + { + Clock = new TimeProviderSystemClock(timeProvider) + }, loggerFactory); - public TokenKeyResolver(string authority, HttpClient httpClient) + _logger = loggerFactory.CreateLogger(); + } + + internal JsonWebKey? ResolveSigningKey(string authority, string keyId, HttpClient httpClient) { ArgumentException.ThrowIfNullOrWhiteSpace(authority); + ArgumentException.ThrowIfNullOrWhiteSpace(keyId); ArgumentNullException.ThrowIfNull(httpClient); + Uri tokenKeysUri = GetTokenKeysUri(authority); + return CachingResolveSigningKey(tokenKeysUri, keyId, httpClient); + } + + private static Uri GetTokenKeysUri(string authority) + { if (!authority.EndsWith('/')) { authority += '/'; } - _authorityUri = new Uri($"{authority}token_keys"); - _httpClient = httpClient; + var authorityUri = new Uri(authority); + return new Uri(authorityUri, "token_keys"); } - internal SecurityKey[] ResolveSigningKey(string keyId) + private JsonWebKey? CachingResolveSigningKey(Uri tokenKeysUri, string keyId, HttpClient httpClient) { - if (ResolvedSecurityKeysById.TryGetValue(keyId, out SecurityKey? resolved)) + string cacheKey = GetCacheKey(tokenKeysUri, keyId); + + if (!_cache.TryGetValue(cacheKey, out JsonWebKey? matchingWebKey)) { - return [resolved]; + JsonWebKeySet? webKeySet = FetchKeySet(tokenKeysUri, httpClient); + + foreach (JsonWebKey nextWebKey in webKeySet?.Keys ?? []) + { + string nextCacheKey = GetCacheKey(tokenKeysUri, nextWebKey.Kid); + _cache.Set(nextCacheKey, nextWebKey, CacheTimeToLiveForKeyFound); + + if (nextWebKey.Kid == keyId) + { + matchingWebKey = nextWebKey; + } + } + + if (matchingWebKey == null) + { + TimeSpan timeToLive = GetTimeToLiveForNotFound(); + _cache.Set(cacheKey, null, timeToLive); + + if (webKeySet == null) + { + LogDisableFetchAfterServerError(keyId, (int)timeToLive.TotalSeconds); + } + else + { + LogDisableFetchAfterKeyNotFound(keyId, (int)timeToLive.TotalSeconds); + } + } } + return matchingWebKey; + } + + private static string GetCacheKey(Uri tokenKeysUri, string keyId) + { + return $"{tokenKeysUri}:{keyId}"; + } + + private static TimeSpan GetTimeToLiveForNotFound() + { + double jitterSeconds = Random.Shared.NextDouble() * (CacheMaxTimeToLiveForKeyNotFound - CacheMinTimeToLiveForKeyNotFound).TotalSeconds; + return CacheMinTimeToLiveForKeyNotFound + TimeSpan.FromSeconds(jitterSeconds); + } + + private JsonWebKeySet? FetchKeySet(Uri tokenKeysUri, HttpClient httpClient) + { #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: can't be async all the way until updates are complete in Microsoft libraries // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468 - JsonWebKeySet? keySet = FetchKeySetAsync(CancellationToken.None).GetAwaiter().GetResult(); + return FetchKeySetAsync(tokenKeysUri, httpClient, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking + } + + private async Task FetchKeySetAsync(Uri tokenKeysUri, HttpClient httpClient, CancellationToken cancellationToken) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, tokenKeysUri); + requestMessage.Headers.Accept.Add(AcceptHeader); - if (keySet != null) + HttpResponseMessage response; + + try { - foreach (JsonWebKey key in keySet.Keys) - { - ResolvedSecurityKeysById[key.Kid] = key; - } + response = await httpClient.SendAsync(requestMessage, cancellationToken); + } + catch (Exception exception) when (exception is HttpRequestException || exception.IsHttpClientTimeout()) + { + LogTokenKeysEndpointUnreachable(exception, tokenKeysUri); + return null; } - if (ResolvedSecurityKeysById.TryGetValue(keyId, out resolved)) + if (!response.IsSuccessStatusCode) { - return [resolved]; + LogFetchTokenKeysStatusFailed(tokenKeysUri, (int)response.StatusCode); + return null; } - return []; + try + { + string result = await response.Content.ReadAsStringAsync(cancellationToken); + return JsonWebKeySet.Create(result); + } + catch (ArgumentException exception) + { + LogFetchTokenKeysParseFailed(exception, tokenKeysUri); + return null; + } } - internal async Task FetchKeySetAsync(CancellationToken cancellationToken) + public void Dispose() { - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, _authorityUri); - requestMessage.Headers.Accept.Add(AcceptHeader); + _cache.Dispose(); + } - HttpResponseMessage response = await _httpClient.SendAsync(requestMessage, cancellationToken); + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed.")] + private partial void LogTokenKeysEndpointUnreachable(Exception exception, MaskedUri tokenKeysUri); - if (!response.IsSuccessStatusCode) + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed with HTTP status {StatusCode}.")] + private partial void LogFetchTokenKeysStatusFailed(MaskedUri tokenKeysUri, int statusCode); + + [LoggerMessage(LogLevel.Warning, "Fetch keys from '{TokenKeysUri}' failed because the returned JSON is invalid.")] + private partial void LogFetchTokenKeysParseFailed(Exception exception, MaskedUri tokenKeysUri); + + [LoggerMessage(LogLevel.Information, "Disabled fetch for key '{KeyId}' for {RetryAfterSeconds}s because the HTTP request failed.")] + private partial void LogDisableFetchAfterServerError(string keyId, int retryAfterSeconds); + + [LoggerMessage(LogLevel.Information, "Disabled fetch for key '{KeyId}' for {RetryAfterSeconds}s because the key was not found in the HTTP response.")] + private partial void LogDisableFetchAfterKeyNotFound(string keyId, int retryAfterSeconds); + + private sealed class TimeProviderSystemClock : ISystemClock + { + private readonly TimeProvider _timeProvider; + + public DateTimeOffset UtcNow => _timeProvider.GetUtcNow(); + + public TimeProviderSystemClock(TimeProvider timeProvider) { - return null; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } - - string result = await response.Content.ReadAsStringAsync(cancellationToken); - return JsonWebKeySet.Create(result); } } diff --git a/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs b/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs index 2431888d87..abf71aeb1f 100644 --- a/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs +++ b/src/Security/src/Authorization.Certificate/ApplicationInstanceCertificate.cs @@ -7,17 +7,9 @@ namespace Steeltoe.Security.Authorization.Certificate; -internal sealed class ApplicationInstanceCertificate +internal sealed partial class ApplicationInstanceCertificate { - // This pattern is found on certificates issued by Diego - private static readonly Regex CloudFoundryInstanceCertificateSubjectRegex = - new(@"^CN=(?[0-9a-f-]+),\sOU=organization:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=app:(?[0-9a-f-]+)$", - RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); - - // This pattern is found on certificates created by Steeltoe - private static readonly Regex SteeltoeInstanceCertificateSubjectRegex = - new(@"^CN=(?[0-9a-f-]+),\sOU=app:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=organization:(?[0-9a-f-]+)$", - RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); + private const int RegexMatchTimeoutInMilliseconds = 1_000; public string OrgId { get; } public string SpaceId { get; } @@ -32,16 +24,26 @@ private ApplicationInstanceCertificate(string orgId, string spaceId, string appl InstanceId = instanceId; } + // This pattern is found on certificates issued by Diego. + [GeneratedRegex(@"^CN=(?[0-9a-f-]+),\sOU=organization:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=app:(?[0-9a-f-]+)$", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)] + private static partial Regex CloudFoundryInstanceCertificateSubjectRegex(); + + // This pattern is found on certificates created by Steeltoe. + [GeneratedRegex(@"^CN=(?[0-9a-f-]+),\sOU=app:(?[0-9a-f-]+)\s\+\sOU=space:(?[0-9a-f-]+)\s\+\sOU=organization:(?[0-9a-f-]+)$", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, RegexMatchTimeoutInMilliseconds)] + private static partial Regex SteeltoeInstanceCertificateSubjectRegex(); + public static bool TryParse(string certificateSubject, [NotNullWhen(true)] out ApplicationInstanceCertificate? instanceCertificate) { instanceCertificate = null; certificateSubject = certificateSubject.Replace("\"", string.Empty, StringComparison.OrdinalIgnoreCase); - Match instanceMatch = CloudFoundryInstanceCertificateSubjectRegex.Match(certificateSubject); + Match instanceMatch = CloudFoundryInstanceCertificateSubjectRegex().Match(certificateSubject); if (!instanceMatch.Success) { - instanceMatch = SteeltoeInstanceCertificateSubjectRegex.Match(certificateSubject); + instanceMatch = SteeltoeInstanceCertificateSubjectRegex().Match(certificateSubject); } if (instanceMatch.Success) diff --git a/src/Security/src/Authorization.Certificate/CertificateAuthorizationHandler.cs b/src/Security/src/Authorization.Certificate/CertificateAuthorizationHandler.cs index 7e1fb81fea..f569d803a8 100644 --- a/src/Security/src/Authorization.Certificate/CertificateAuthorizationHandler.cs +++ b/src/Security/src/Authorization.Certificate/CertificateAuthorizationHandler.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Security.Authorization.Certificate; -internal sealed class CertificateAuthorizationHandler : IAuthorizationHandler +internal sealed partial class CertificateAuthorizationHandler : IAuthorizationHandler { private readonly ILogger _logger; private ApplicationInstanceCertificate? _applicationInstanceCertificate; @@ -48,8 +48,7 @@ private void OnCertificateRefresh(CertificateOptions certificateOptions) } else { - _logger.LogError("Identity certificate did not match an expected pattern. Subject was: {CertificateSubject}", - certificateOptions.Certificate.Subject); + LogIdentityCertificateMismatch(certificateOptions.Certificate.Subject); } } @@ -75,8 +74,23 @@ private void HandleCertificateAuthorizationRequirement(AuthorizationHandlerCo } else { - _logger.LogDebug("User has the required claim, but the value doesn't match. Expected {ExpectedClaimValue} but got {ActualClaimValue}", claimValue, - context.User.FindFirstValue(claimType)); + ExpensiveLogClaimValueMismatch(context, claimType, claimValue); } } + + private void ExpensiveLogClaimValueMismatch(AuthorizationHandlerContext context, string claimType, string claimValue) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + string? actualClaimValue = context.User.FindFirstValue(claimType); + LogClaimValueMismatch(claimValue, actualClaimValue); + } + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Identity certificate did not match an expected pattern. Subject was '{CertificateSubject}'.")] + private partial void LogIdentityCertificateMismatch(string certificateSubject); + + [LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true, + Message = "User has the required claim, but the value doesn't match. Expected '{ExpectedClaimValue}' but got '{ActualClaimValue}'.")] + private partial void LogClaimValueMismatch(string expectedClaimValue, string? actualClaimValue); } diff --git a/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs b/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs index d9b497519b..ba2f19982f 100644 --- a/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs +++ b/src/Security/src/Authorization.Certificate/CertificateHttpClientBuilderExtensions.cs @@ -10,7 +10,7 @@ namespace Steeltoe.Security.Authorization.Certificate; -public static class CertificateHttpClientBuilderExtensions +public static partial class CertificateHttpClientBuilderExtensions { /// /// Binds certificate paths in configuration to representing the application instance and attaches the certificate to @@ -94,18 +94,24 @@ public static IHttpClientBuilder AddClientCertificate(this IHttpClientBuilder bu if (certificate != null) { - logger.LogTrace("Adding certificate with subject {CertificateSubject} to outbound requests in header {CertificateHeaderName}", - certificate.Subject, certificateHeaderName); + LogAddingCertificate(logger, certificate.Subject, certificateHeaderName); string b64 = Convert.ToBase64String(certificate.Export(X509ContentType.Cert)); client.DefaultRequestHeaders.Add(certificateHeaderName, b64); } else { - logger.LogError("Failed to find a certificate under the name {CertificateOptionsName}", certificateName); + LogCertificateNotFound(logger, certificateName); } }); return builder; } + + [LoggerMessage(Level = LogLevel.Trace, + Message = "Adding certificate with subject '{CertificateSubject}' to outbound requests in header {CertificateHeaderName}.")] + private static partial void LogAddingCertificate(ILogger logger, string certificateSubject, string certificateHeaderName); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to find a certificate under the name {CertificateOptionsName}.")] + private static partial void LogCertificateNotFound(ILogger logger, string certificateOptionsName); } diff --git a/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs b/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs index 924ed389c7..2085265d8c 100644 --- a/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs +++ b/src/Security/src/Authorization.Certificate/PostConfigureCertificateAuthenticationOptions.cs @@ -11,7 +11,7 @@ namespace Steeltoe.Security.Authorization.Certificate; -internal sealed class PostConfigureCertificateAuthenticationOptions : IPostConfigureOptions +internal sealed partial class PostConfigureCertificateAuthenticationOptions : IPostConfigureOptions { private readonly IOptionsMonitor _certificateOptionsMonitor; private readonly ILogger _logger; @@ -39,8 +39,10 @@ public void PostConfigure(string? name, CertificateAuthenticationOptions options if (!string.IsNullOrEmpty(systemCertPath)) { +#pragma warning disable SYSLIB0057 // Type or member is obsolete X509Certificate2[] systemCertificates = Directory.GetFiles(systemCertPath).Select(certificateFilename => new X509Certificate2(certificateFilename)).ToArray(); +#pragma warning restore SYSLIB0057 // Type or member is obsolete options.CustomTrustStore.AddRange(systemCertificates); } @@ -75,8 +77,7 @@ public void PostConfigure(string? name, CertificateAuthenticationOptions options } else { - _logger.LogError("Identity certificate did not match an expected pattern. Subject was: {CertificateSubject}", - context.ClientCertificate.Subject); + LogIdentityCertificateMismatch(context.ClientCertificate.Subject); } var identity = new ClaimsIdentity(claims, CertificateAuthenticationDefaults.AuthenticationScheme); @@ -87,4 +88,7 @@ public void PostConfigure(string? name, CertificateAuthenticationOptions options } }; } + + [LoggerMessage(Level = LogLevel.Error, Message = "Identity certificate did not match an expected pattern. Subject was '{CertificateSubject}'.")] + private partial void LogIdentityCertificateMismatch(string certificateSubject); } diff --git a/src/Security/src/Authorization.Certificate/Steeltoe.Security.Authorization.Certificate.csproj b/src/Security/src/Authorization.Certificate/Steeltoe.Security.Authorization.Certificate.csproj index d132e72847..5f75794484 100644 --- a/src/Security/src/Authorization.Certificate/Steeltoe.Security.Authorization.Certificate.csproj +++ b/src/Security/src/Authorization.Certificate/Steeltoe.Security.Authorization.Certificate.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Provides support for authorization with client certificates. authorization;security;x509;certificate;mutualtls;CloudFoundry;tanzu;aspnetcore true diff --git a/src/Security/src/DataProtection.Redis/ConfigurationSchema.json b/src/Security/src/DataProtection.Redis/ConfigurationSchema.json new file mode 100644 index 0000000000..1ccb69c5e4 --- /dev/null +++ b/src/Security/src/DataProtection.Redis/ConfigurationSchema.json @@ -0,0 +1,20 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Steeltoe": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.DataProtection": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Steeltoe.Security.DataProtection.Redis": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + } +} diff --git a/src/Security/src/DataProtection.Redis/Properties/AssemblyInfo.cs b/src/Security/src/DataProtection.Redis/Properties/AssemblyInfo.cs index 022305bba9..ce180d3c47 100644 --- a/src/Security/src/DataProtection.Redis/Properties/AssemblyInfo.cs +++ b/src/Security/src/DataProtection.Redis/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.Security", "Steeltoe.Security.DataProtection", "Steeltoe.Security.DataProtection.Redis")] [assembly: InternalsVisibleTo("Steeltoe.Security.DataProtection.Redis.Test")] diff --git a/src/Security/src/DataProtection.Redis/Steeltoe.Security.DataProtection.Redis.csproj b/src/Security/src/DataProtection.Redis/Steeltoe.Security.DataProtection.Redis.csproj index 312dff9d2b..6d7a61d9fe 100644 --- a/src/Security/src/DataProtection.Redis/Steeltoe.Security.DataProtection.Redis.csproj +++ b/src/Security/src/DataProtection.Redis/Steeltoe.Security.DataProtection.Redis.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0;net8.0 Support for storing data protection keys in Redis. CloudFoundry;security;dataprotection;redis;aspnetcore true diff --git a/src/Security/test/Authentication.JwtBearer.Test/PostConfigureJwtBearerOptionsTest.cs b/src/Security/test/Authentication.JwtBearer.Test/PostConfigureJwtBearerOptionsTest.cs index dd118fa349..19439d5eef 100644 --- a/src/Security/test/Authentication.JwtBearer.Test/PostConfigureJwtBearerOptionsTest.cs +++ b/src/Security/test/Authentication.JwtBearer.Test/PostConfigureJwtBearerOptionsTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; @@ -27,7 +28,8 @@ public void PostConfigure_AddsClientIdToValidAudiences() }; IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); - var postConfigurer = new PostConfigureJwtBearerOptions(configuration); + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + var postConfigurer = new PostConfigureJwtBearerOptions(configuration, resolver); postConfigurer.PostConfigure(null, jwtBearerOptions); @@ -63,9 +65,10 @@ public async Task PostConfigure_ConfiguresForCloudFoundry() """; using var servicesScope = new EnvironmentVariableScope("VCAP_SERVICES", vcapServices); - IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings().Build(); + IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Identity).Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); + services.AddLogging(); services.AddAuthentication().AddJwtBearer().ConfigureJwtBearerForCloudFoundry(); await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); @@ -111,9 +114,10 @@ public async Task PostConfigure_ConfiguresForCloudFoundry_AllowMultipleIssuers() using var applicationScope = new EnvironmentVariableScope("VCAP_APPLICATION", "{}"); using var servicesScope = new EnvironmentVariableScope("VCAP_SERVICES", vcapServices); - IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings().Build(); + IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Identity).Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); + services.AddLogging(); services.AddAuthentication().AddJwtBearer().ConfigureJwtBearerForCloudFoundry(); await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); diff --git a/src/Security/test/Authentication.JwtBearer.Test/Steeltoe.Security.Authentication.JwtBearer.Test.csproj b/src/Security/test/Authentication.JwtBearer.Test/Steeltoe.Security.Authentication.JwtBearer.Test.csproj index d738e78d77..abdce38f96 100644 --- a/src/Security/test/Authentication.JwtBearer.Test/Steeltoe.Security.Authentication.JwtBearer.Test.csproj +++ b/src/Security/test/Authentication.JwtBearer.Test/Steeltoe.Security.Authentication.JwtBearer.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Security/test/Authentication.JwtBearer.Test/TokenKeyResolverTest.cs b/src/Security/test/Authentication.JwtBearer.Test/TokenKeyResolverTest.cs index 80b1525758..c457583ed5 100644 --- a/src/Security/test/Authentication.JwtBearer.Test/TokenKeyResolverTest.cs +++ b/src/Security/test/Authentication.JwtBearer.Test/TokenKeyResolverTest.cs @@ -3,17 +3,69 @@ // See the LICENSE file in the project root for more information. using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; +using Steeltoe.Common.TestResources; namespace Steeltoe.Security.Authentication.JwtBearer.Test; public sealed class TokenKeyResolverTest { - private const string KeySet = """ + private const string EmptyKeySet = """ + { + "keys": [] + } + """; + + private const string KeySetWithKeyA = """ + { + "keys": [ + { + "kid": "key-a", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + } + ] + } + """; + + private const string KeySetWithKeyB = """ + { + "keys": [ + { + "kid": "key-b", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + } + ] + } + """; + + private const string KeySetWithBothKeys = """ { "keys": [ { - "kid": "legacy-token-key", + "kid": "key-a", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + }, + { + "kid": "key-b", "alg": "SHA256withRSA", "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", "kty": "RSA", @@ -25,111 +77,439 @@ public sealed class TokenKeyResolverTest } """; + public static TheoryData ServerUnreachableExceptions => + [ + new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused)), + new TaskCanceledException("The request timed out.", new TimeoutException()) + ]; + [Fact] - public void ResolveSigningKey_FindsExistingKey() + public void Fetches_existing_key_and_returns_it_from_cache() { - var keys = JsonWebKeySet.Create(KeySet); - JsonWebKey webKey = keys.Keys[0]; - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); - using var httpClient = new HttpClient(); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - TokenKeyResolver.ResolvedSecurityKeysById["legacy-token-key"] = webKey; + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); - result.Should().ContainSingle().Which.Should().Be(webKey); + result1.Should().NotBeNull(); + result1.KeyId.Should().Be("key-a"); + + timeProvider.Advance(TimeSpan.FromHours(11)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().NotBeNull(); + result2.ToString().Should().Be(result1.ToString()); + + handler.RequestCount.Should().Be(1); + + loggerProvider.GetAll().Should().BeEmpty(); } [Fact] - public void ResolveSigningKey_IssuesHttpRequest_ResolvesKey() + public void Refetches_existing_key_after_expired_from_cache() { - using var handler = new TestMessageHandler(); + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(KeySet) - }; + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + timeProvider.Advance(TimeSpan.FromHours(13)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result.Should().NotBeNull(); + result.KeyId.Should().Be("key-a"); + + handler.RequestCount.Should().Be(2); + + loggerProvider.GetAll().Should().BeEmpty(); + } + + [Fact] + public void Returns_null_when_key_no_longer_present_after_refetch() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerBoth = new TestMessageHandler(KeySetWithBothKeys); + using var httpClientBoth = new HttpClient(handlerBoth); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientBoth); + + result1.Should().NotBeNull(); + + timeProvider.Advance(TimeSpan.FromHours(13)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientB); + + result2.Should().BeNull(); + + handlerBoth.RequestCount.Should().Be(1); + handlerB.RequestCount.Should().Be(1); + } + + [Fact] + public void Returns_key_from_refetch_after_it_became_available() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + using var handlerA = new TestMessageHandler(KeySetWithKeyA); + using var httpClientA = new HttpClient(handlerA); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientB); + + result1.Should().BeNull(); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + + result2.Should().NotBeNull(); + result2.KeyId.Should().Be("key-a"); + + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + result3.Should().NotBeNull(); + result3.ToString().Should().Be(result2.ToString()); + + handlerB.RequestCount.Should().Be(1); + handlerA.RequestCount.Should().Be(1); + } + + [Fact] + public void Returns_existing_key_from_cache_if_fetched_other_key_earlier() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + using var resolver = new TokenKeyResolver(TimeProvider.System, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithBothKeys); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClient); - handler.LastRequest.Should().NotBeNull(); - TokenKeyResolver.ResolvedSecurityKeysById.Should().ContainKey("legacy-token-key"); - result.Should().NotBeEmpty(); + result.Should().NotBeNull(); + result.KeyId.Should().Be("key-b"); + + handler.RequestCount.Should().Be(1); + + loggerProvider.GetAll().Should().BeEmpty(); } [Fact] - public void ResolveSigningKey_IssuesHttpRequest_DoesNotResolveKey() + public void Fetches_unknown_key_and_returns_it_from_cache() { - // ReSharper disable StringLiteralTypo - const string alternateKeySet = """ - { - "keys": [ - { - "kid": "foobar", - "alg": "SHA256withRSA", - "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", - "kty": "RSA", - "use": "sig", - "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", - "e": "AQAB" - } - ] - } - """; - // ReSharper restore StringLiteralTypo + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(EmptyKeySet); + using var httpClient = new HttpClient(handler); - using var handler = new TestMessageHandler(); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(alternateKeySet) - }; + result1.Should().BeNull(); + + loggerProvider.GetAll().Should().ContainSingle().Which.Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'unknown-key' for ") + .And.EndWith("s because the key was not found in the HTTP response."); + + loggerProvider.Clear(); + timeProvider.Advance(TimeSpan.FromSeconds(15)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + + result2.Should().BeNull(); + loggerProvider.GetAll().Should().BeEmpty(); + + handler.RequestCount.Should().Be(1); + } - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + [Fact] + public void Refetches_unknown_key_after_expired_from_cache() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + _ = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + loggerProvider.Clear(); + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + + result.Should().BeNull(); - handler.LastRequest.Should().NotBeNull(); - TokenKeyResolver.ResolvedSecurityKeysById.Should().NotContainKey("legacy-token-key"); - result.Should().BeEmpty(); + loggerProvider.GetAll().Should().ContainSingle().Which.Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'unknown-key' for ") + .And.EndWith("s because the key was not found in the HTTP response."); + + handler.RequestCount.Should().Be(2); } [Fact] - public async Task FetchKeySet_IssuesHttpRequest_ReturnsKeySet() + public void Normalizes_trailing_slash_in_authority() { - using var handler = new TestMessageHandler(); + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(KeySet) - }; + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path/", "key-a", httpClient); + + result1.Should().NotBeNull(); + result2.Should().NotBeNull(); + result2.ToString().Should().Be(result1.ToString()); + + handler.RequestCount.Should().Be(1); + handler.LastRequestUrl.Should().Be("https://server.com/path/token_keys"); + } - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + [Fact] + public void Uses_separate_cache_per_authority() + { + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + using var handler = new TestMessageHandler(KeySetWithKeyA); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - JsonWebKeySet? result = await resolver.FetchKeySetAsync(TestContext.Current.CancellationToken); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().NotBeNull(); + result1.KeyId.Should().Be("key-a"); + handler.RequestCount.Should().Be(1); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://other-server.com/alt-path", "key-a", httpClient); + + result2.Should().NotBeNull(); + result2.KeyId.Should().Be("key-a"); + handler.RequestCount.Should().Be(2); + } + + [Fact] + public void Uses_separate_cache_per_keyId() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + + using var handlerA = new TestMessageHandler(KeySetWithKeyA); + using var httpClientA = new HttpClient(handlerA); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + + // t=0: cache A + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + + handlerA.RequestCount.Should().Be(1); + + // t=11: cache B + timeProvider.Advance(TimeSpan.FromHours(11)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerB.RequestCount.Should().Be(1); + + // t=13: A expired while B still cached + timeProvider.Advance(TimeSpan.FromHours(2)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerA.RequestCount.Should().Be(2); + handlerB.RequestCount.Should().Be(1); + } + + [Fact] + public void All_keys_from_response_are_cached() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + using var handlerBoth = new TestMessageHandler(KeySetWithBothKeys); + using var httpClientBoth = new HttpClient(handlerBoth); + + // t=0: cache B + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerB.RequestCount.Should().Be(1); + + // t=11: cache A, re-cache B + timeProvider.Advance(TimeSpan.FromHours(11)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientBoth); + + handlerBoth.RequestCount.Should().Be(1); + + // t=13: A and B still cached + timeProvider.Advance(TimeSpan.FromHours(2)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); result.Should().NotBeNull(); - result.Keys.Should().NotBeEmpty(); + result.KeyId.Should().Be("key-b"); + + handlerB.RequestCount.Should().Be(1); + } + + [Theory] + [MemberData(nameof(ServerUnreachableExceptions))] + public void Caches_shortly_when_server_is_unreachable(Exception exception) + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(exception); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be($"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); + } + + [Theory] + [InlineData(HttpStatusCode.Unauthorized)] + [InlineData(HttpStatusCode.Forbidden)] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.TooManyRequests)] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + public void Caches_shortly_when_server_returns_error(HttpStatusCode statusCode) + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(statusCode); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be( + $"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed with HTTP status {(int)statusCode}."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); + } + + [Fact] + public void Caches_shortly_when_server_returns_broken_JSON() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler("{"); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be( + $"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed because the returned JSON is invalid."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); } private sealed class TestMessageHandler : HttpMessageHandler { - public HttpRequestMessage? LastRequest { get; private set; } + private readonly HttpStatusCode _responseStatusCode = HttpStatusCode.OK; + private readonly string _responseText = string.Empty; + private readonly Exception? _exceptionToThrow; + + public int RequestCount { get; private set; } + public string? LastRequestUrl { get; private set; } + + public TestMessageHandler(string responseText) + { + _responseText = responseText; + } - public HttpResponseMessage Response { get; set; } = new(HttpStatusCode.OK); + public TestMessageHandler(HttpStatusCode statusCode) + { + _responseStatusCode = statusCode; + } + + public TestMessageHandler(Exception exceptionToThrow) + { + _exceptionToThrow = exceptionToThrow; + } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - LastRequest = request; - return Task.FromResult(Response); + RequestCount++; + LastRequestUrl = request.RequestUri?.ToString(); + + if (_exceptionToThrow != null) + { + return Task.FromException(_exceptionToThrow); + } + + var response = new HttpResponseMessage(_responseStatusCode) + { + Content = new StringContent(_responseText) + }; + + return Task.FromResult(response); } } } diff --git a/src/Security/test/Authentication.OpenIdConnect.Test/PostConfigureOpenIdConnectOptionsTest.cs b/src/Security/test/Authentication.OpenIdConnect.Test/PostConfigureOpenIdConnectOptionsTest.cs index f478b0ebb5..6e4e3ccd52 100644 --- a/src/Security/test/Authentication.OpenIdConnect.Test/PostConfigureOpenIdConnectOptionsTest.cs +++ b/src/Security/test/Authentication.OpenIdConnect.Test/PostConfigureOpenIdConnectOptionsTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; @@ -31,7 +32,8 @@ public async Task PostConfigure_AddsClientIdToValidAudiences() var optionsMonitor = serviceProvider.GetRequiredService>(); OpenIdConnectOptions options = optionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme); - var postConfigurer = new PostConfigureOpenIdConnectOptions(); + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + var postConfigurer = new PostConfigureOpenIdConnectOptions(resolver); postConfigurer.PostConfigure(OpenIdConnectDefaults.AuthenticationScheme, options); @@ -67,10 +69,10 @@ public async Task PostConfigure_ConfiguresForCloudFoundry() """; using var servicesScope = new EnvironmentVariableScope("VCAP_SERVICES", vcapServices); - IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings().Build(); + IConfiguration configuration = new ConfigurationBuilder().AddCloudFoundryServiceBindings(CloudFoundryServiceBrokerTypes.Identity).Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); - + services.AddLogging(); services.AddAuthentication().AddOpenIdConnect().ConfigureOpenIdConnectForCloudFoundry(); await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); diff --git a/src/Security/test/Authentication.OpenIdConnect.Test/Steeltoe.Security.Authentication.OpenIdConnect.Test.csproj b/src/Security/test/Authentication.OpenIdConnect.Test/Steeltoe.Security.Authentication.OpenIdConnect.Test.csproj index 4273872dc9..7a11ec9e00 100644 --- a/src/Security/test/Authentication.OpenIdConnect.Test/Steeltoe.Security.Authentication.OpenIdConnect.Test.csproj +++ b/src/Security/test/Authentication.OpenIdConnect.Test/Steeltoe.Security.Authentication.OpenIdConnect.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Security/test/Authentication.OpenIdConnect.Test/TokenKeyResolverTest.cs b/src/Security/test/Authentication.OpenIdConnect.Test/TokenKeyResolverTest.cs index 9e373ec604..e9c1b20597 100644 --- a/src/Security/test/Authentication.OpenIdConnect.Test/TokenKeyResolverTest.cs +++ b/src/Security/test/Authentication.OpenIdConnect.Test/TokenKeyResolverTest.cs @@ -3,17 +3,69 @@ // See the LICENSE file in the project root for more information. using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; +using Steeltoe.Common.TestResources; namespace Steeltoe.Security.Authentication.OpenIdConnect.Test; public sealed class TokenKeyResolverTest { - private const string KeySet = """ + private const string EmptyKeySet = """ + { + "keys": [] + } + """; + + private const string KeySetWithKeyA = """ + { + "keys": [ + { + "kid": "key-a", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + } + ] + } + """; + + private const string KeySetWithKeyB = """ + { + "keys": [ + { + "kid": "key-b", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + } + ] + } + """; + + private const string KeySetWithBothKeys = """ { "keys": [ { - "kid": "legacy-token-key", + "kid": "key-a", + "alg": "SHA256withRSA", + "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", + "kty": "RSA", + "use": "sig", + "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", + "e": "AQAB" + }, + { + "kid": "key-b", "alg": "SHA256withRSA", "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", "kty": "RSA", @@ -25,111 +77,439 @@ public sealed class TokenKeyResolverTest } """; + public static TheoryData ServerUnreachableExceptions => + [ + new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused)), + new TaskCanceledException("The request timed out.", new TimeoutException()) + ]; + [Fact] - public void ResolveSigningKey_FindsExistingKey() + public void Fetches_existing_key_and_returns_it_from_cache() { - var keys = JsonWebKeySet.Create(KeySet); - JsonWebKey webKey = keys.Keys[0]; - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); - using var httpClient = new HttpClient(); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - TokenKeyResolver.ResolvedSecurityKeysById["legacy-token-key"] = webKey; + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); - result.Should().ContainSingle().Which.Should().Be(webKey); + result1.Should().NotBeNull(); + result1.KeyId.Should().Be("key-a"); + + timeProvider.Advance(TimeSpan.FromHours(11)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().NotBeNull(); + result2.ToString().Should().Be(result1.ToString()); + + handler.RequestCount.Should().Be(1); + + loggerProvider.GetAll().Should().BeEmpty(); } [Fact] - public void ResolveSigningKey_IssuesHttpRequest_ResolvesKey() + public void Refetches_existing_key_after_expired_from_cache() { - using var handler = new TestMessageHandler(); + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(KeySet) - }; + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + timeProvider.Advance(TimeSpan.FromHours(13)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result.Should().NotBeNull(); + result.KeyId.Should().Be("key-a"); + + handler.RequestCount.Should().Be(2); + + loggerProvider.GetAll().Should().BeEmpty(); + } + + [Fact] + public void Returns_null_when_key_no_longer_present_after_refetch() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerBoth = new TestMessageHandler(KeySetWithBothKeys); + using var httpClientBoth = new HttpClient(handlerBoth); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientBoth); + + result1.Should().NotBeNull(); + + timeProvider.Advance(TimeSpan.FromHours(13)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientB); + + result2.Should().BeNull(); + + handlerBoth.RequestCount.Should().Be(1); + handlerB.RequestCount.Should().Be(1); + } + + [Fact] + public void Returns_key_from_refetch_after_it_became_available() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + using var handlerA = new TestMessageHandler(KeySetWithKeyA); + using var httpClientA = new HttpClient(handlerA); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientB); + + result1.Should().BeNull(); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + + result2.Should().NotBeNull(); + result2.KeyId.Should().Be("key-a"); + + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + result3.Should().NotBeNull(); + result3.ToString().Should().Be(result2.ToString()); + + handlerB.RequestCount.Should().Be(1); + handlerA.RequestCount.Should().Be(1); + } + + [Fact] + public void Returns_existing_key_from_cache_if_fetched_other_key_earlier() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + using var resolver = new TokenKeyResolver(TimeProvider.System, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithBothKeys); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClient); - handler.LastRequest.Should().NotBeNull(); - TokenKeyResolver.ResolvedSecurityKeysById.Should().ContainKey("legacy-token-key"); - result.Should().NotBeEmpty(); + result.Should().NotBeNull(); + result.KeyId.Should().Be("key-b"); + + handler.RequestCount.Should().Be(1); + + loggerProvider.GetAll().Should().BeEmpty(); } [Fact] - public void ResolveSigningKey_IssuesHttpRequest_DoesNotResolveKey() + public void Fetches_unknown_key_and_returns_it_from_cache() { - // ReSharper disable StringLiteralTypo - const string alternateKeySet = """ - { - "keys": [ - { - "kid": "foobar", - "alg": "SHA256withRSA", - "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk+7xH35bYBppsn54cBW+\nFlrveTe+3L4xl7ix13XK8eBcCmNOyBhNzhks6toDiRjrgw5QW76cFirVRFIVQkiZ\nsUwDyGOax3q8NOJyBFXiplIUScrx8aI0jkY/Yd6ixAc5yBSBfXThy4EF9T0xCyt4\nxWLYNXMRwe88Y+i+MEoLNXWRbhjJm76LN7rsdIxALbS0vJNWUDALWjtE6FeYX6uU\nL9msAzlCQkdnSvwMmr8Ij2O3IVMxHDJXOZinFqt9zVfXwO11o7ZmiskZnRz1/V0f\nvbUQAadkcDEUt1gk9cbrAhiipg8VWDMsC7VUXuekJZjme5f8oWTwpsgP6cTUzwSS\n6wIDAQAB\n-----END PUBLIC KEY-----", - "kty": "RSA", - "use": "sig", - "n": "AJPu8R9+W2AaabJ+eHAVvhZa73k3vty+MZe4sdd1yvHgXApjTsgYTc4ZLOraA4kY64MOUFu+nBYq1URSFUJImbFMA8hjmsd6vDTicgRV4qZSFEnK8fGiNI5GP2HeosQHOcgUgX104cuBBfU9MQsreMVi2DVzEcHvPGPovjBKCzV1kW4YyZu+ize67HSMQC20tLyTVlAwC1o7ROhXmF+rlC/ZrAM5QkJHZ0r8DJq/CI9jtyFTMRwyVzmYpxarfc1X18DtdaO2ZorJGZ0c9f1dH721EAGnZHAxFLdYJPXG6wIYoqYPFVgzLAu1VF7npCWY5nuX/KFk8KbID+nE1M8Ekus=", - "e": "AQAB" - } - ] - } - """; - // ReSharper restore StringLiteralTypo + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(EmptyKeySet); + using var httpClient = new HttpClient(handler); - using var handler = new TestMessageHandler(); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(alternateKeySet) - }; + result1.Should().BeNull(); + + loggerProvider.GetAll().Should().ContainSingle().Which.Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'unknown-key' for ") + .And.EndWith("s because the key was not found in the HTTP response."); + + loggerProvider.Clear(); + timeProvider.Advance(TimeSpan.FromSeconds(15)); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + + result2.Should().BeNull(); + loggerProvider.GetAll().Should().BeEmpty(); + + handler.RequestCount.Should().Be(1); + } - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + [Fact] + public void Refetches_unknown_key_after_expired_from_cache() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(KeySetWithKeyA); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - SecurityKey[] result = resolver.ResolveSigningKey("legacy-token-key"); + _ = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + loggerProvider.Clear(); + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "unknown-key", httpClient); + + result.Should().BeNull(); - handler.LastRequest.Should().NotBeNull(); - TokenKeyResolver.ResolvedSecurityKeysById.Should().NotContainKey("legacy-token-key"); - result.Should().BeEmpty(); + loggerProvider.GetAll().Should().ContainSingle().Which.Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'unknown-key' for ") + .And.EndWith("s because the key was not found in the HTTP response."); + + handler.RequestCount.Should().Be(2); } [Fact] - public async Task FetchKeySet_IssuesHttpRequest_ReturnsKeySet() + public void Normalizes_trailing_slash_in_authority() { - using var handler = new TestMessageHandler(); + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + using var handler = new TestMessageHandler(KeySetWithKeyA); + using var httpClient = new HttpClient(handler); - handler.Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(KeySet) - }; + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path/", "key-a", httpClient); + + result1.Should().NotBeNull(); + result2.Should().NotBeNull(); + result2.ToString().Should().Be(result1.ToString()); + + handler.RequestCount.Should().Be(1); + handler.LastRequestUrl.Should().Be("https://server.com/path/token_keys"); + } - TokenKeyResolver.ResolvedSecurityKeysById.Clear(); + [Fact] + public void Uses_separate_cache_per_authority() + { + using var resolver = new TokenKeyResolver(TimeProvider.System, NullLoggerFactory.Instance); + using var handler = new TestMessageHandler(KeySetWithKeyA); using var httpClient = new HttpClient(handler); - var resolver = new TokenKeyResolver("https://foo.bar", httpClient); - JsonWebKeySet? result = await resolver.FetchKeySetAsync(TestContext.Current.CancellationToken); + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().NotBeNull(); + result1.KeyId.Should().Be("key-a"); + handler.RequestCount.Should().Be(1); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://other-server.com/alt-path", "key-a", httpClient); + + result2.Should().NotBeNull(); + result2.KeyId.Should().Be("key-a"); + handler.RequestCount.Should().Be(2); + } + + [Fact] + public void Uses_separate_cache_per_keyId() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + + using var handlerA = new TestMessageHandler(KeySetWithKeyA); + using var httpClientA = new HttpClient(handlerA); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + + // t=0: cache A + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + + handlerA.RequestCount.Should().Be(1); + + // t=11: cache B + timeProvider.Advance(TimeSpan.FromHours(11)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerB.RequestCount.Should().Be(1); + + // t=13: A expired while B still cached + timeProvider.Advance(TimeSpan.FromHours(2)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientA); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerA.RequestCount.Should().Be(2); + handlerB.RequestCount.Should().Be(1); + } + + [Fact] + public void All_keys_from_response_are_cached() + { + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, NullLoggerFactory.Instance); + using var handlerB = new TestMessageHandler(KeySetWithKeyB); + using var httpClientB = new HttpClient(handlerB); + using var handlerBoth = new TestMessageHandler(KeySetWithBothKeys); + using var httpClientBoth = new HttpClient(handlerBoth); + + // t=0: cache B + _ = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); + + handlerB.RequestCount.Should().Be(1); + + // t=11: cache A, re-cache B + timeProvider.Advance(TimeSpan.FromHours(11)); + _ = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClientBoth); + + handlerBoth.RequestCount.Should().Be(1); + + // t=13: A and B still cached + timeProvider.Advance(TimeSpan.FromHours(2)); + JsonWebKey? result = resolver.ResolveSigningKey("https://server.com/path", "key-b", httpClientB); result.Should().NotBeNull(); - result.Keys.Should().NotBeEmpty(); + result.KeyId.Should().Be("key-b"); + + handlerB.RequestCount.Should().Be(1); + } + + [Theory] + [MemberData(nameof(ServerUnreachableExceptions))] + public void Caches_shortly_when_server_is_unreachable(Exception exception) + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(exception); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be($"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); + } + + [Theory] + [InlineData(HttpStatusCode.Unauthorized)] + [InlineData(HttpStatusCode.Forbidden)] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.TooManyRequests)] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + public void Caches_shortly_when_server_returns_error(HttpStatusCode statusCode) + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler(statusCode); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be( + $"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed with HTTP status {(int)statusCode}."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); + } + + [Fact] + public void Caches_shortly_when_server_returns_broken_JSON() + { + using var loggerProvider = new CapturingLoggerProvider((_, level) => level >= LogLevel.Information); + using var loggerFactory = new LoggerFactory([loggerProvider]); + var timeProvider = new FakeTimeProvider(); + using var resolver = new TokenKeyResolver(timeProvider, loggerFactory); + using var handler = new TestMessageHandler("{"); + using var httpClient = new HttpClient(handler); + + JsonWebKey? result1 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result1.Should().BeNull(); + + JsonWebKey? result2 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result2.Should().BeNull(); + + handler.RequestCount.Should().Be(1); + + IList logLines = loggerProvider.GetAll(); + logLines.Should().HaveCount(2); + + logLines[0].Should().Be( + $"WARN {typeof(TokenKeyResolver)}: Fetch keys from 'https://server.com/path/token_keys' failed because the returned JSON is invalid."); + + logLines[1].Should().StartWith($"INFO {typeof(TokenKeyResolver)}: Disabled fetch for key 'key-a' for ").And + .EndWith("s because the HTTP request failed."); + + timeProvider.Advance(TimeSpan.FromSeconds(90)); + JsonWebKey? result3 = resolver.ResolveSigningKey("https://server.com/path", "key-a", httpClient); + + result3.Should().BeNull(); + + handler.RequestCount.Should().Be(2); } private sealed class TestMessageHandler : HttpMessageHandler { - public HttpRequestMessage? LastRequest { get; private set; } + private readonly HttpStatusCode _responseStatusCode = HttpStatusCode.OK; + private readonly string _responseText = string.Empty; + private readonly Exception? _exceptionToThrow; + + public int RequestCount { get; private set; } + public string? LastRequestUrl { get; private set; } + + public TestMessageHandler(string responseText) + { + _responseText = responseText; + } - public HttpResponseMessage Response { get; set; } = new(HttpStatusCode.OK); + public TestMessageHandler(HttpStatusCode statusCode) + { + _responseStatusCode = statusCode; + } + + public TestMessageHandler(Exception exceptionToThrow) + { + _exceptionToThrow = exceptionToThrow; + } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - LastRequest = request; - return Task.FromResult(Response); + RequestCount++; + LastRequestUrl = request.RequestUri?.ToString(); + + if (_exceptionToThrow != null) + { + return Task.FromException(_exceptionToThrow); + } + + var response = new HttpResponseMessage(_responseStatusCode) + { + Content = new StringContent(_responseText) + }; + + return Task.FromResult(response); } } } diff --git a/src/Security/test/Authorization.Certificate.Test/CertificateAuthorizationTest.cs b/src/Security/test/Authorization.Certificate.Test/CertificateAuthorizationTest.cs index 5cafd5a32d..806ca83c7e 100644 --- a/src/Security/test/Authorization.Certificate.Test/CertificateAuthorizationTest.cs +++ b/src/Security/test/Authorization.Certificate.Test/CertificateAuthorizationTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Net; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -81,8 +82,7 @@ public async Task CertificateAuth_RejectsSpaceMismatch() response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task CertificateAuth_AcceptsSameSpace_DiegoCert() { var requestUri = new Uri($"https://localhost/{CertificateAuthorizationPolicies.SameSpace}"); @@ -98,8 +98,7 @@ public async Task CertificateAuth_AcceptsSameSpace_DiegoCert() response.StatusCode.Should().Be(HttpStatusCode.OK); } - [Fact] - [Trait("Category", "SkipOnMacOS")] + [FactSkippedOnPlatform(nameof(OSPlatform.OSX))] public async Task CertificateAuth_AcceptsSameOrg_DiegoCert() { var requestUri = new Uri($"https://localhost/{CertificateAuthorizationPolicies.SameOrg}"); diff --git a/src/Security/test/Authorization.Certificate.Test/Steeltoe.Security.Authorization.Certificate.Test.csproj b/src/Security/test/Authorization.Certificate.Test/Steeltoe.Security.Authorization.Certificate.Test.csproj index c06e4381d5..9235a977d6 100644 --- a/src/Security/test/Authorization.Certificate.Test/Steeltoe.Security.Authorization.Certificate.Test.csproj +++ b/src/Security/test/Authorization.Certificate.Test/Steeltoe.Security.Authorization.Certificate.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.cs b/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.cs index 814e741ea1..6814ee9fe2 100644 --- a/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.cs +++ b/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.cs @@ -20,7 +20,6 @@ namespace Steeltoe.Security.DataProtection.Redis.Test; public sealed partial class RedisDataProtectionBuilderExtensionsTest { [Fact] - [Trait("Category", "SkipOnMacOS")] public async Task Stores_session_state_in_Redis() { const string appName = "SHARED-APP-NAME"; diff --git a/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.net90.cs b/src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.other.cs similarity index 100% rename from src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.net90.cs rename to src/Security/test/DataProtection.Redis.Test/RedisDataProtectionBuilderExtensionsTest.other.cs diff --git a/src/Security/test/DataProtection.Redis.Test/Steeltoe.Security.DataProtection.Redis.Test.csproj b/src/Security/test/DataProtection.Redis.Test/Steeltoe.Security.DataProtection.Redis.Test.csproj index 71284f6de5..5de5a0bade 100644 --- a/src/Security/test/DataProtection.Redis.Test/Steeltoe.Security.DataProtection.Redis.Test.csproj +++ b/src/Security/test/DataProtection.Redis.Test/Steeltoe.Security.DataProtection.Redis.Test.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 @@ -19,8 +19,8 @@ - - + + diff --git a/src/Steeltoe.All.sln b/src/Steeltoe.All.sln deleted file mode 100644 index 39dd55ab9c..0000000000 --- a/src/Steeltoe.All.sln +++ /dev/null @@ -1,952 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32112.339 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common", "Common\src\Common\Steeltoe.Common.csproj", "{61812938-5132-4AB6-B48D-2DF4189B3E37}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Test", "Common\test\Common.Test\Steeltoe.Common.Test.csproj", "{12BB796A-63C5-40D0-A2DE-80D9996DE571}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Http", "Common\src\Http\Steeltoe.Common.Http.csproj", "{E58EB8CA-2389-4A28-B0EF-34C2B57BB270}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Net", "Common\src\Net\Steeltoe.Common.Net.csproj", "{8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Certificates", "Common\src\Certificates\Steeltoe.Common.Certificates.csproj", "{DFE642E6-1CD0-4485-AC86-43CEBC451484}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{59874241-E276-4035-B31D-14924889A1C9}" - ProjectSection(SolutionItems) = preProject - Common\README.md = Common\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Http.Test", "Common\test\Http.Test\Steeltoe.Common.Http.Test.csproj", "{C67D2028-D637-4D81-84A2-5D64F2E389DA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Net.Test", "Common\test\Net.Test\Steeltoe.Common.Net.Test.csproj", "{1ACC6991-7D4C-48B2-A41C-4B179B19A85C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Certificates.Test", "Common\test\Certificates.Test\Steeltoe.Common.Certificates.Test.csproj", "{880208DF-05C6-4763-A447-744D6C8DBDEA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_shared", "_shared", "{DC1BC61A-E0FA-4CF9-9F24-D4C564A07836}" - ProjectSection(SolutionItems) = preProject - ..\.editorconfig = ..\.editorconfig - ..\.gitattributes = ..\.gitattributes - ..\.gitignore = ..\.gitignore - ..\cleanupcode.ps1 = ..\cleanupcode.ps1 - ..\coverlet.runsettings = ..\coverlet.runsettings - ..\Directory.Build.targets = ..\Directory.Build.targets - ..\nuget.config = ..\nuget.config - ..\PackageReadme.md = ..\PackageReadme.md - ..\shared-package.props = ..\shared-package.props - ..\shared-project.props = ..\shared-project.props - ..\shared-test.props = ..\shared-test.props - ..\shared.props = ..\shared.props - ..\Steeltoe.Debug.ruleset = ..\Steeltoe.Debug.ruleset - ..\Steeltoe.Release.ruleset = ..\Steeltoe.Release.ruleset - ..\stylecop.json = ..\stylecop.json - ..\versions.props = ..\versions.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{4AB95F47-0C93-4C88-B87F-231262CD0E89}" - ProjectSection(SolutionItems) = preProject - Configuration\README.md = Configuration\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Connectors", "Connectors", "{3CBE0336-C3C8-4DC2-AE45-86FC1D1B3C25}" - ProjectSection(SolutionItems) = preProject - Connectors\README.md = Connectors\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Discovery", "Discovery", "{31BAEBB1-696E-44A1-B1EF-0D150E6D2559}" - ProjectSection(SolutionItems) = preProject - Discovery\README.md = Discovery\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logging", "Logging", "{DDB99068-891F-4937-98B0-E72CC3F4964B}" - ProjectSection(SolutionItems) = preProject - Logging\README.md = Logging\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Management", "Management", "{8BD7D5E5-D887-4BD7-9F42-725A8714F7BC}" - ProjectSection(SolutionItems) = preProject - Management\README.md = Management\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Security", "Security", "{5128206A-242E-4069-AD30-910EDC40B165}" - ProjectSection(SolutionItems) = preProject - Security\README.md = Security\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.CloudFoundry", "Configuration\src\CloudFoundry\Steeltoe.Configuration.CloudFoundry.csproj", "{3084B070-6F72-4673-889F-EF00DADB468B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.ConfigServer", "Configuration\src\ConfigServer\Steeltoe.Configuration.ConfigServer.csproj", "{727CC05F-A99D-474F-9C66-A9E57D6B25CC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Placeholder", "Configuration\src\Placeholder\Steeltoe.Configuration.Placeholder.csproj", "{EBFDDFDE-BF5D-4607-AADA-7039445E2AB7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.RandomValue", "Configuration\src\RandomValue\Steeltoe.Configuration.RandomValue.csproj", "{C51FBF31-BFEE-460B-B182-49CD65F8F8EA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.CloudFoundry.Test", "Configuration\test\CloudFoundry.Test\Steeltoe.Configuration.CloudFoundry.Test.csproj", "{0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.ConfigServer.Integration.Test", "Configuration\test\ConfigServer.Integration.Test\Steeltoe.Configuration.ConfigServer.Integration.Test.csproj", "{25F1BF2F-1B6A-466A-A3BF-8963B0D040F4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.ConfigServer.Test", "Configuration\test\ConfigServer.Test\Steeltoe.Configuration.ConfigServer.Test.csproj", "{5802F580-3E70-45F4-AF60-4F4E5F8FAADA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Placeholder.Test", "Configuration\test\Placeholder.Test\Steeltoe.Configuration.Placeholder.Test.csproj", "{1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.RandomValue.Test", "Configuration\test\RandomValue.Test\Steeltoe.Configuration.RandomValue.Test.csproj", "{98489BC4-B03B-4703-B25D-A89D617E761F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Connectors.EntityFrameworkCore", "Connectors\src\EntityFrameworkCore\Steeltoe.Connectors.EntityFrameworkCore.csproj", "{B1B7522F-6EF2-4935-BE9F-66B6DB974303}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Connectors", "Connectors\src\Connectors\Steeltoe.Connectors.csproj", "{34891C2A-FAF2-466A-90A6-F25DBD8283E8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Connectors.EntityFrameworkCore.Test", "Connectors\test\EntityFrameworkCore.Test\Steeltoe.Connectors.EntityFrameworkCore.Test.csproj", "{72C7AD35-98E4-478E-B594-06A3DB2E61D6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Consul", "Discovery\src\Consul\Steeltoe.Discovery.Consul.csproj", "{C3098547-C3B5-4136-9CEB-30466EE1FA53}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Eureka", "Discovery\src\Eureka\Steeltoe.Discovery.Eureka.csproj", "{E6DBF77D-04A3-4422-A7D2-D3131DF5B829}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Consul.Test", "Discovery\test\Consul.Test\Steeltoe.Discovery.Consul.Test.csproj", "{B5D0C674-1958-43BF-B644-0865E9DDCCBD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Eureka.Test", "Discovery\test\Eureka.Test\Steeltoe.Discovery.Eureka.Test.csproj", "{23B95817-4183-4B42-BF27-EA575184DB2D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Logging.DynamicConsole", "Logging\src\DynamicConsole\Steeltoe.Logging.DynamicConsole.csproj", "{43DC1B99-588C-4984-AEAC-43AA5B47D84C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Logging.DynamicConsole.Test", "Logging\test\DynamicConsole.Test\Steeltoe.Logging.DynamicConsole.Test.csproj", "{04BC571A-0055-4B9B-805E-D2B0823C9D8D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Tasks", "Management\src\Tasks\Steeltoe.Management.Tasks.csproj", "{990DCEE9-D88F-4B9F-8298-678B524AC4D6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Endpoint", "Management\src\Endpoint\Steeltoe.Management.Endpoint.csproj", "{557AFE26-B89D-497D-92B6-8268D04396E5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Tracing", "Management\src\Tracing\Steeltoe.Management.Tracing.csproj", "{4EFBD262-86B2-4E21-8608-B1894E04EA99}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Endpoint.Test", "Management\test\Endpoint.Test\Steeltoe.Management.Endpoint.Test.csproj", "{C2EFCD80-C96D-4A09-BFD6-02CB4603961C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Tracing.Test", "Management\test\Tracing.Test\Steeltoe.Management.Tracing.Test.csproj", "{C437628B-43EE-4AAD-83ED-DDE7499E705B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Tasks.Test", "Management\test\Tasks.Test\Steeltoe.Management.Tasks.Test.csproj", "{A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.DataProtection.Redis", "Security\src\DataProtection.Redis\Steeltoe.Security.DataProtection.Redis.csproj", "{31E52250-3422-49E9-9605-EBE786CC1BE3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.DataProtection.Redis.Test", "Security\test\DataProtection.Redis.Test\Steeltoe.Security.DataProtection.Redis.Test.csproj", "{6D09B0D3-AF28-4FC4-A67D-424E7746682B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.TestResources", "Common\test\TestResources\Steeltoe.Common.TestResources.csproj", "{65CF716D-9215-475C-B37F-CA943F5CD6A3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Hosting", "Common\src\Hosting\Steeltoe.Common.Hosting.csproj", "{BEB156E9-4E06-403A-B778-E7B092B79962}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Hosting.Test", "Common\test\Hosting.Test\Steeltoe.Common.Hosting.Test.csproj", "{BA80CEAD-02A2-480D-93F4-907B01BDB219}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Abstractions", "Management\src\Abstractions\Steeltoe.Management.Abstractions.csproj", "{F9095412-B7FB-4A8A-A291-141458A988AF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Abstractions", "Configuration\src\Abstractions\Steeltoe.Configuration.Abstractions.csproj", "{920BDA8F-094B-4D81-A03C-664C0F808DC7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Logging.Abstractions", "Logging\src\Abstractions\Steeltoe.Logging.Abstractions.csproj", "{86FBBCC7-97F5-4795-A61E-FA7E0CE0514D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Connectors.Test", "Connectors\test\Connectors.Test\Steeltoe.Connectors.Test.csproj", "{C3232459-EE86-430B-B310-EB32C991A11A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Logging.DynamicSerilog", "Logging\src\DynamicSerilog\Steeltoe.Logging.DynamicSerilog.csproj", "{E0B1415D-8D97-4BA5-9315-82E7D33E3933}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Logging.DynamicSerilog.Test", "Logging\test\DynamicSerilog.Test\Steeltoe.Logging.DynamicSerilog.Test.csproj", "{6C116C3E-177A-44C0-A149-8DDB62812DC3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.HttpClients", "Discovery\src\HttpClients\Steeltoe.Discovery.HttpClients.csproj", "{E2BE4A3F-1C35-41ED-99D1-7630B5369942}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.HttpClients.Test", "Discovery\test\HttpClients.Test\Steeltoe.Discovery.HttpClients.Test.csproj", "{C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.SpringBoot", "Configuration\src\SpringBoot\Steeltoe.Configuration.SpringBoot.csproj", "{D94706F7-7622-4D4C-87BD-84FB40D4E6BD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.SpringBoot.Test", "Configuration\test\SpringBoot.Test\Steeltoe.Configuration.SpringBoot.Test.csproj", "{BA38127A-DA5A-437F-9C84-5997FBE0B6A0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bootstrap", "Bootstrap", "{EA9C3A73-3F31-4DC9-982C-963CE613E119}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Bootstrap.AutoConfiguration", "Bootstrap\src\AutoConfiguration\Steeltoe.Bootstrap.AutoConfiguration.csproj", "{38EFC635-0C56-442D-8CA3-20AEC1930D7B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Bootstrap.AutoConfiguration.Test", "Bootstrap\test\AutoConfiguration.Test\Steeltoe.Bootstrap.AutoConfiguration.Test.csproj", "{6499285A-88CF-426A-909D-307382B91AA3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Kubernetes.ServiceBindings", "Configuration\src\Kubernetes.ServiceBindings\Steeltoe.Configuration.Kubernetes.ServiceBindings.csproj", "{6A064B5A-21F7-452A-AAFF-3C0A68286FDF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Kubernetes.ServiceBindings.Test", "Configuration\test\Kubernetes.ServiceBindings.Test\Steeltoe.Configuration.Kubernetes.ServiceBindings.Test.csproj", "{0D68FC03-7FD4-4280-852B-0E620872CE22}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Encryption.Test", "Configuration\test\Encryption.Test\Steeltoe.Configuration.Encryption.Test.csproj", "{BCC3F7DC-080D-4A7E-8060-40ED65073EB3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.Encryption", "Configuration\src\Encryption\Steeltoe.Configuration.Encryption.csproj", "{764B3355-FD20-42E3-B452-357FCC20A27D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Prometheus", "Management\src\Prometheus\Steeltoe.Management.Prometheus.csproj", "{7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Bootstrap.EmptyAutoConfiguration.Test", "Bootstrap\test\EmptyAutoConfiguration.Test\Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj", "{C821DCC5-880F-4F85-97B1-6D9A56DAD6F0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Management.Prometheus.Test", "Management\test\Prometheus.Test\Steeltoe.Management.Prometheus.Test.csproj", "{7866263B-936E-4604-A821-73C46BAB1657}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Configuration", "Discovery\src\Configuration\Steeltoe.Discovery.Configuration.csproj", "{EA3AFC13-2399-411E-AD20-C6677505615A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Discovery.Configuration.Test", "Discovery\test\Configuration.Test\Steeltoe.Discovery.Configuration.Test.csproj", "{5031A608-281E-4FAF-9501-1DBDAB645907}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Configuration.ConfigServer.Discovery.Test", "Configuration\test\ConfigServer.Discovery.Test\Steeltoe.Configuration.ConfigServer.Discovery.Test.csproj", "{10EC7705-BE9E-4E2A-B174-E74D1CC9852F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.OpenIdConnect", "Security\src\Authentication.OpenIdConnect\Steeltoe.Security.Authentication.OpenIdConnect.csproj", "{AF26B4AC-35B5-4954-ADE8-97BA2DC31250}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.JwtBearer", "Security\src\Authentication.JwtBearer\Steeltoe.Security.Authentication.JwtBearer.csproj", "{5EBE664D-D071-43A9-9B53-346BE3DCCB41}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.JwtBearer.Test", "Security\test\Authentication.JwtBearer.Test\Steeltoe.Security.Authentication.JwtBearer.Test.csproj", "{196A7EF1-56CE-481E-BB49-97BD0FF096BE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authorization.Certificate", "Security\src\Authorization.Certificate\Steeltoe.Security.Authorization.Certificate.csproj", "{B0AC88EC-EB6C-4E73-B9D5-51DD213931B4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authorization.Certificate.Test", "Security\test\Authorization.Certificate.Test\Steeltoe.Security.Authorization.Certificate.Test.csproj", "{AAA51F15-E5BE-4E77-92DF-1992434D557A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.OpenIdConnect.Test", "Security\test\Authentication.OpenIdConnect.Test\Steeltoe.Security.Authentication.OpenIdConnect.Test.csproj", "{9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_tools", "_tools", "{DF26D677-72A3-442D-B556-46E3D0EF4A77}" - ProjectSection(SolutionItems) = preProject - Tools\src\package.targets = Tools\src\package.targets - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationSchemaGenerator", "Tools\src\ConfigurationSchemaGenerator\ConfigurationSchemaGenerator.csproj", "{2A975FB7-401B-41BB-96A4-1DF0036888A9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationSchemaGenerator.Tests", "Tools\test\ConfigurationSchemaGenerator.Tests\ConfigurationSchemaGenerator.Tests.csproj", "{C4C38F83-8410-443C-9599-ACFB5FA7CD2D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Logging", "Common\src\Logging\Steeltoe.Common.Logging.csproj", "{738AFF97-B4C4-4EAC-B9C5-C405D481C92B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Common.Logging.Test", "Common\test\Logging.Test\Steeltoe.Common.Logging.Test.csproj", "{A9CCC214-212A-4296-98F5-65ADDB2BB8B4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.Endpoint.RazorPagesTestWebApp", "Management\test\RazorPagesTestWebApp\Steeltoe.Management.Endpoint.RazorPagesTestWebApp.csproj", "{51DD3135-1EAA-4640-82F1-9FBECA421708}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Obsolete", "Obsolete", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Bootstrap.Autoconfig", "Obsolete\Steeltoe.Bootstrap.Autoconfig\Steeltoe.Bootstrap.Autoconfig.csproj", "{6607EBEE-8668-E1D3-C1CB-ED11A91DC100}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.CircuitBreaker.Abstractions", "Obsolete\Steeltoe.CircuitBreaker.Abstractions\Steeltoe.CircuitBreaker.Abstractions.csproj", "{5D16D6CC-2BBF-3E53-A932-7E6FCD64E123}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore", "Obsolete\Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore\Steeltoe.CircuitBreaker.Hystrix.MetricsEventsCore.csproj", "{77459A1A-63B4-F7CE-5B20-79091A4BC6C8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore", "Obsolete\Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore\Steeltoe.CircuitBreaker.Hystrix.MetricsStreamCore.csproj", "{7A06889E-2503-38D9-FE44-36527A9FD0C9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.CircuitBreaker.HystrixBase", "Obsolete\Steeltoe.CircuitBreaker.HystrixBase\Steeltoe.CircuitBreaker.HystrixBase.csproj", "{5159B5AC-6354-B394-93DD-2621DB302EDF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.CircuitBreaker.HystrixCore", "Obsolete\Steeltoe.CircuitBreaker.HystrixCore\Steeltoe.CircuitBreaker.HystrixCore.csproj", "{44D3B2AE-DA53-A134-182B-E4301BB856A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Abstractions", "Obsolete\Steeltoe.Common.Abstractions\Steeltoe.Common.Abstractions.csproj", "{55DDF80C-42C8-A046-883C-954049FCB811}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Expression", "Obsolete\Steeltoe.Common.Expression\Steeltoe.Common.Expression.csproj", "{98E31A4F-30D0-0F7B-2E9D-D8F1AEB53DA5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Kubernetes", "Obsolete\Steeltoe.Common.Kubernetes\Steeltoe.Common.Kubernetes.csproj", "{3F841B6E-2AEA-23B1-C141-CC67A8FB33E0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Retry", "Obsolete\Steeltoe.Common.Retry\Steeltoe.Common.Retry.csproj", "{8057DA4A-FF84-72D3-54BE-A17B47760608}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Security", "Obsolete\Steeltoe.Common.Security\Steeltoe.Common.Security.csproj", "{5A179D89-95C6-F103-DA7D-6FD33FDF151B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Common.Utils", "Obsolete\Steeltoe.Common.Utils\Steeltoe.Common.Utils.csproj", "{A4F19C30-3624-86C4-F533-D88C9DC2A972}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.Abstractions", "Obsolete\Steeltoe.Connector.Abstractions\Steeltoe.Connector.Abstractions.csproj", "{3E41717A-789B-D213-A4A8-56BFED2E8D17}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.CloudFoundry", "Obsolete\Steeltoe.Connector.CloudFoundry\Steeltoe.Connector.CloudFoundry.csproj", "{63CAD818-CAFA-41ED-E389-C7553AD813FD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.ConnectorBase", "Obsolete\Steeltoe.Connector.ConnectorBase\Steeltoe.Connector.ConnectorBase.csproj", "{C6277231-D388-0D78-CC9F-973172F92585}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.ConnectorCore", "Obsolete\Steeltoe.Connector.ConnectorCore\Steeltoe.Connector.ConnectorCore.csproj", "{981F5916-AB63-4E99-7762-1BC03CDD8D00}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.EF6Core", "Obsolete\Steeltoe.Connector.EF6Core\Steeltoe.Connector.EF6Core.csproj", "{3105ADCB-D81E-25F2-8390-986E50E3A88B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Connector.EFCore", "Obsolete\Steeltoe.Connector.EFCore\Steeltoe.Connector.EFCore.csproj", "{2F785E38-6E2A-834A-0C9A-CBE3B307DE01}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Discovery.Abstractions", "Obsolete\Steeltoe.Discovery.Abstractions\Steeltoe.Discovery.Abstractions.csproj", "{4C3111B7-D8EC-0538-2C30-952EA4D9FC94}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Discovery.ClientBase", "Obsolete\Steeltoe.Discovery.ClientBase\Steeltoe.Discovery.ClientBase.csproj", "{2388CA32-2573-C097-0B04-B67C58BDB7CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Discovery.ClientCore", "Obsolete\Steeltoe.Discovery.ClientCore\Steeltoe.Discovery.ClientCore.csproj", "{C5824CE4-D590-20E7-0565-6BDE1D809A19}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Discovery.Kubernetes", "Obsolete\Steeltoe.Discovery.Kubernetes\Steeltoe.Discovery.Kubernetes.csproj", "{14E1A30B-2E8A-794C-CF13-F47FDEF4ACD3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.Abstractions", "Obsolete\Steeltoe.Extensions.Configuration.Abstractions\Steeltoe.Extensions.Configuration.Abstractions.csproj", "{644C6F28-AEC7-AD30-1D65-F459E0F21DC6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.CloudFoundryBase", "Obsolete\Steeltoe.Extensions.Configuration.CloudFoundryBase\Steeltoe.Extensions.Configuration.CloudFoundryBase.csproj", "{18B7AC0F-16B9-B612-A6AA-E092EE68DA1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.CloudFoundryCore", "Obsolete\Steeltoe.Extensions.Configuration.CloudFoundryCore\Steeltoe.Extensions.Configuration.CloudFoundryCore.csproj", "{5B7C359B-81DB-8BF9-D806-6DCEAC1A51D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.ConfigServerBase", "Obsolete\Steeltoe.Extensions.Configuration.ConfigServerBase\Steeltoe.Extensions.Configuration.ConfigServerBase.csproj", "{EEDC48F4-14C6-4017-0CB4-1A7E2315C685}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.ConfigServerCore", "Obsolete\Steeltoe.Extensions.Configuration.ConfigServerCore\Steeltoe.Extensions.Configuration.ConfigServerCore.csproj", "{B1965821-5021-E09B-0107-9BF12D674AB1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding", "Obsolete\Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding\Steeltoe.Extensions.Configuration.Kubernetes.ServiceBinding.csproj", "{C11055B8-9185-2BF4-35E7-65C977DD5DEC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.KubernetesBase", "Obsolete\Steeltoe.Extensions.Configuration.KubernetesBase\Steeltoe.Extensions.Configuration.KubernetesBase.csproj", "{F44A6978-521A-1E7B-8841-C1BF15F80CCB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.KubernetesCore", "Obsolete\Steeltoe.Extensions.Configuration.KubernetesCore\Steeltoe.Extensions.Configuration.KubernetesCore.csproj", "{CD01D081-2759-DBD2-A98F-BE5ECE04A403}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.PlaceholderBase", "Obsolete\Steeltoe.Extensions.Configuration.PlaceholderBase\Steeltoe.Extensions.Configuration.PlaceholderBase.csproj", "{F7A34F42-9092-1D8D-BEA7-146968B3EE3A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.PlaceholderCore", "Obsolete\Steeltoe.Extensions.Configuration.PlaceholderCore\Steeltoe.Extensions.Configuration.PlaceholderCore.csproj", "{73DB7209-1957-2295-FB91-3FBC82442B01}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.RandomValueBase", "Obsolete\Steeltoe.Extensions.Configuration.RandomValueBase\Steeltoe.Extensions.Configuration.RandomValueBase.csproj", "{135C853D-B3A2-8F7C-4926-B52E9392002F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.SpringBootBase", "Obsolete\Steeltoe.Extensions.Configuration.SpringBootBase\Steeltoe.Extensions.Configuration.SpringBootBase.csproj", "{E87C0BA6-E772-68FA-34C6-64227DD9F3EE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Configuration.SpringBootCore", "Obsolete\Steeltoe.Extensions.Configuration.SpringBootCore\Steeltoe.Extensions.Configuration.SpringBootCore.csproj", "{5FFC2953-2430-8AC9-0096-3FDBC1CA5140}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Logging.Abstractions", "Obsolete\Steeltoe.Extensions.Logging.Abstractions\Steeltoe.Extensions.Logging.Abstractions.csproj", "{192D3A1C-8554-3D05-DA6E-A57030ECA124}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Logging.DynamicLogger", "Obsolete\Steeltoe.Extensions.Logging.DynamicLogger\Steeltoe.Extensions.Logging.DynamicLogger.csproj", "{31EC11DD-755D-FECE-4678-D88E17BFDBB6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Logging.DynamicSerilogBase", "Obsolete\Steeltoe.Extensions.Logging.DynamicSerilogBase\Steeltoe.Extensions.Logging.DynamicSerilogBase.csproj", "{CB988CB6-A9A2-0365-354E-1464906386FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Extensions.Logging.DynamicSerilogCore", "Obsolete\Steeltoe.Extensions.Logging.DynamicSerilogCore\Steeltoe.Extensions.Logging.DynamicSerilogCore.csproj", "{5064A781-B3B0-585F-B856-E294556180F1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Integration.Abstractions", "Obsolete\Steeltoe.Integration.Abstractions\Steeltoe.Integration.Abstractions.csproj", "{97D530C8-97AC-CEBE-9657-CE02557D6ED9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Integration.IntegrationBase", "Obsolete\Steeltoe.Integration.IntegrationBase\Steeltoe.Integration.IntegrationBase.csproj", "{942F9B69-02CD-469B-C00C-3454E93BA2AE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Integration.RabbitMQ", "Obsolete\Steeltoe.Integration.RabbitMQ\Steeltoe.Integration.RabbitMQ.csproj", "{48145FD7-B653-CFE3-B30A-7EF2E0120B21}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.CloudFoundryCore", "Obsolete\Steeltoe.Management.CloudFoundryCore\Steeltoe.Management.CloudFoundryCore.csproj", "{05B5D7F9-FE77-1519-2023-61CD1FAD90B1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.Diagnostics", "Obsolete\Steeltoe.Management.Diagnostics\Steeltoe.Management.Diagnostics.csproj", "{1BB203B2-65E0-3834-3EB4-0BEC2F75FEBC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.EndpointBase", "Obsolete\Steeltoe.Management.EndpointBase\Steeltoe.Management.EndpointBase.csproj", "{E974FA60-F02F-FA1F-BA84-C25972F8DD4F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.EndpointCore", "Obsolete\Steeltoe.Management.EndpointCore\Steeltoe.Management.EndpointCore.csproj", "{A857F14B-45A2-8244-554B-C3205FC38F08}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.KubernetesCore", "Obsolete\Steeltoe.Management.KubernetesCore\Steeltoe.Management.KubernetesCore.csproj", "{A12F0AE9-DCB4-9D54-E586-FBBF10CA93F6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.OpenTelemetryBase", "Obsolete\Steeltoe.Management.OpenTelemetryBase\Steeltoe.Management.OpenTelemetryBase.csproj", "{56589499-4FA9-EC26-7F7E-D187D035D4DA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.TaskCore", "Obsolete\Steeltoe.Management.TaskCore\Steeltoe.Management.TaskCore.csproj", "{0F613620-3071-DF98-3506-F37F016C07E1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.TracingBase", "Obsolete\Steeltoe.Management.TracingBase\Steeltoe.Management.TracingBase.csproj", "{C45ED108-A68D-B447-00D1-BCB45EEC0D15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Management.TracingCore", "Obsolete\Steeltoe.Management.TracingCore\Steeltoe.Management.TracingCore.csproj", "{1AFEBB7F-BFB8-2C11-28BF-68AA6376479B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Messaging.Abstractions", "Obsolete\Steeltoe.Messaging.Abstractions\Steeltoe.Messaging.Abstractions.csproj", "{63AA9B32-84B1-BE5F-4507-8E7DF73880F2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Messaging.MessagingBase", "Obsolete\Steeltoe.Messaging.MessagingBase\Steeltoe.Messaging.MessagingBase.csproj", "{83653A8C-7946-338C-F42C-AB714F49991D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Messaging.RabbitMQ", "Obsolete\Steeltoe.Messaging.RabbitMQ\Steeltoe.Messaging.RabbitMQ.csproj", "{32481813-3CE4-F4C8-086F-15FED097835F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.Authentication.CloudFoundryBase", "Obsolete\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj", "{4F6C59C7-0311-8ADF-017D-E64F46510A70}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.Authentication.CloudFoundryCore", "Obsolete\Steeltoe.Security.Authentication.CloudFoundryCore\Steeltoe.Security.Authentication.CloudFoundryCore.csproj", "{FF8B81BD-63BA-BEE6-2466-B1A3EE553494}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.Authentication.MtlsCore", "Obsolete\Steeltoe.Security.Authentication.MtlsCore\Steeltoe.Security.Authentication.MtlsCore.csproj", "{9D7D20F4-F986-4194-9D18-4F28654EDE7D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.DataProtection.CredHubBase", "Obsolete\Steeltoe.Security.DataProtection.CredHubBase\Steeltoe.Security.DataProtection.CredHubBase.csproj", "{A90ADACF-3BC4-3C34-F6D6-D02D99C799D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.DataProtection.CredHubCore", "Obsolete\Steeltoe.Security.DataProtection.CredHubCore\Steeltoe.Security.DataProtection.CredHubCore.csproj", "{75087953-E456-EA40-857F-B38A2BF4DD1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.DataProtection.RedisCore", "Obsolete\Steeltoe.Security.DataProtection.RedisCore\Steeltoe.Security.DataProtection.RedisCore.csproj", "{CF879664-186D-AE54-434E-F0FC3B5D27DC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Stream.Abstractions", "Obsolete\Steeltoe.Stream.Abstractions\Steeltoe.Stream.Abstractions.csproj", "{D377B3DB-B229-DC7A-D651-320B5904BC29}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Stream.Binder.RabbitMQ", "Obsolete\Steeltoe.Stream.Binder.RabbitMQ\Steeltoe.Stream.Binder.RabbitMQ.csproj", "{F42A9BC5-DA3E-A1CE-B48E-7EBF59D8D161}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Stream.StreamBase", "Obsolete\Steeltoe.Stream.StreamBase\Steeltoe.Stream.StreamBase.csproj", "{51FBF981-91B2-407E-7A10-D28E5FB717F4}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {61812938-5132-4AB6-B48D-2DF4189B3E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {61812938-5132-4AB6-B48D-2DF4189B3E37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {61812938-5132-4AB6-B48D-2DF4189B3E37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {61812938-5132-4AB6-B48D-2DF4189B3E37}.Release|Any CPU.Build.0 = Release|Any CPU - {12BB796A-63C5-40D0-A2DE-80D9996DE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12BB796A-63C5-40D0-A2DE-80D9996DE571}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12BB796A-63C5-40D0-A2DE-80D9996DE571}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12BB796A-63C5-40D0-A2DE-80D9996DE571}.Release|Any CPU.Build.0 = Release|Any CPU - {E58EB8CA-2389-4A28-B0EF-34C2B57BB270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E58EB8CA-2389-4A28-B0EF-34C2B57BB270}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E58EB8CA-2389-4A28-B0EF-34C2B57BB270}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E58EB8CA-2389-4A28-B0EF-34C2B57BB270}.Release|Any CPU.Build.0 = Release|Any CPU - {8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431}.Release|Any CPU.Build.0 = Release|Any CPU - {DFE642E6-1CD0-4485-AC86-43CEBC451484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFE642E6-1CD0-4485-AC86-43CEBC451484}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFE642E6-1CD0-4485-AC86-43CEBC451484}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFE642E6-1CD0-4485-AC86-43CEBC451484}.Release|Any CPU.Build.0 = Release|Any CPU - {C67D2028-D637-4D81-84A2-5D64F2E389DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C67D2028-D637-4D81-84A2-5D64F2E389DA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C67D2028-D637-4D81-84A2-5D64F2E389DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C67D2028-D637-4D81-84A2-5D64F2E389DA}.Release|Any CPU.Build.0 = Release|Any CPU - {1ACC6991-7D4C-48B2-A41C-4B179B19A85C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1ACC6991-7D4C-48B2-A41C-4B179B19A85C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1ACC6991-7D4C-48B2-A41C-4B179B19A85C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1ACC6991-7D4C-48B2-A41C-4B179B19A85C}.Release|Any CPU.Build.0 = Release|Any CPU - {880208DF-05C6-4763-A447-744D6C8DBDEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {880208DF-05C6-4763-A447-744D6C8DBDEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {880208DF-05C6-4763-A447-744D6C8DBDEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {880208DF-05C6-4763-A447-744D6C8DBDEA}.Release|Any CPU.Build.0 = Release|Any CPU - {3084B070-6F72-4673-889F-EF00DADB468B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3084B070-6F72-4673-889F-EF00DADB468B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3084B070-6F72-4673-889F-EF00DADB468B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3084B070-6F72-4673-889F-EF00DADB468B}.Release|Any CPU.Build.0 = Release|Any CPU - {727CC05F-A99D-474F-9C66-A9E57D6B25CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {727CC05F-A99D-474F-9C66-A9E57D6B25CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {727CC05F-A99D-474F-9C66-A9E57D6B25CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {727CC05F-A99D-474F-9C66-A9E57D6B25CC}.Release|Any CPU.Build.0 = Release|Any CPU - {EBFDDFDE-BF5D-4607-AADA-7039445E2AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EBFDDFDE-BF5D-4607-AADA-7039445E2AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBFDDFDE-BF5D-4607-AADA-7039445E2AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EBFDDFDE-BF5D-4607-AADA-7039445E2AB7}.Release|Any CPU.Build.0 = Release|Any CPU - {C51FBF31-BFEE-460B-B182-49CD65F8F8EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C51FBF31-BFEE-460B-B182-49CD65F8F8EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C51FBF31-BFEE-460B-B182-49CD65F8F8EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C51FBF31-BFEE-460B-B182-49CD65F8F8EA}.Release|Any CPU.Build.0 = Release|Any CPU - {0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A}.Release|Any CPU.Build.0 = Release|Any CPU - {25F1BF2F-1B6A-466A-A3BF-8963B0D040F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {25F1BF2F-1B6A-466A-A3BF-8963B0D040F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {25F1BF2F-1B6A-466A-A3BF-8963B0D040F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {25F1BF2F-1B6A-466A-A3BF-8963B0D040F4}.Release|Any CPU.Build.0 = Release|Any CPU - {5802F580-3E70-45F4-AF60-4F4E5F8FAADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5802F580-3E70-45F4-AF60-4F4E5F8FAADA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5802F580-3E70-45F4-AF60-4F4E5F8FAADA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5802F580-3E70-45F4-AF60-4F4E5F8FAADA}.Release|Any CPU.Build.0 = Release|Any CPU - {1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450}.Release|Any CPU.Build.0 = Release|Any CPU - {98489BC4-B03B-4703-B25D-A89D617E761F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {98489BC4-B03B-4703-B25D-A89D617E761F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {98489BC4-B03B-4703-B25D-A89D617E761F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {98489BC4-B03B-4703-B25D-A89D617E761F}.Release|Any CPU.Build.0 = Release|Any CPU - {B1B7522F-6EF2-4935-BE9F-66B6DB974303}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1B7522F-6EF2-4935-BE9F-66B6DB974303}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1B7522F-6EF2-4935-BE9F-66B6DB974303}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1B7522F-6EF2-4935-BE9F-66B6DB974303}.Release|Any CPU.Build.0 = Release|Any CPU - {34891C2A-FAF2-466A-90A6-F25DBD8283E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34891C2A-FAF2-466A-90A6-F25DBD8283E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34891C2A-FAF2-466A-90A6-F25DBD8283E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34891C2A-FAF2-466A-90A6-F25DBD8283E8}.Release|Any CPU.Build.0 = Release|Any CPU - {72C7AD35-98E4-478E-B594-06A3DB2E61D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72C7AD35-98E4-478E-B594-06A3DB2E61D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72C7AD35-98E4-478E-B594-06A3DB2E61D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72C7AD35-98E4-478E-B594-06A3DB2E61D6}.Release|Any CPU.Build.0 = Release|Any CPU - {C3098547-C3B5-4136-9CEB-30466EE1FA53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3098547-C3B5-4136-9CEB-30466EE1FA53}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3098547-C3B5-4136-9CEB-30466EE1FA53}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3098547-C3B5-4136-9CEB-30466EE1FA53}.Release|Any CPU.Build.0 = Release|Any CPU - {E6DBF77D-04A3-4422-A7D2-D3131DF5B829}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E6DBF77D-04A3-4422-A7D2-D3131DF5B829}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E6DBF77D-04A3-4422-A7D2-D3131DF5B829}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E6DBF77D-04A3-4422-A7D2-D3131DF5B829}.Release|Any CPU.Build.0 = Release|Any CPU - {B5D0C674-1958-43BF-B644-0865E9DDCCBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5D0C674-1958-43BF-B644-0865E9DDCCBD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5D0C674-1958-43BF-B644-0865E9DDCCBD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5D0C674-1958-43BF-B644-0865E9DDCCBD}.Release|Any CPU.Build.0 = Release|Any CPU - {23B95817-4183-4B42-BF27-EA575184DB2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23B95817-4183-4B42-BF27-EA575184DB2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23B95817-4183-4B42-BF27-EA575184DB2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23B95817-4183-4B42-BF27-EA575184DB2D}.Release|Any CPU.Build.0 = Release|Any CPU - {43DC1B99-588C-4984-AEAC-43AA5B47D84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43DC1B99-588C-4984-AEAC-43AA5B47D84C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43DC1B99-588C-4984-AEAC-43AA5B47D84C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43DC1B99-588C-4984-AEAC-43AA5B47D84C}.Release|Any CPU.Build.0 = Release|Any CPU - {04BC571A-0055-4B9B-805E-D2B0823C9D8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04BC571A-0055-4B9B-805E-D2B0823C9D8D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04BC571A-0055-4B9B-805E-D2B0823C9D8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04BC571A-0055-4B9B-805E-D2B0823C9D8D}.Release|Any CPU.Build.0 = Release|Any CPU - {990DCEE9-D88F-4B9F-8298-678B524AC4D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {990DCEE9-D88F-4B9F-8298-678B524AC4D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {990DCEE9-D88F-4B9F-8298-678B524AC4D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {990DCEE9-D88F-4B9F-8298-678B524AC4D6}.Release|Any CPU.Build.0 = Release|Any CPU - {557AFE26-B89D-497D-92B6-8268D04396E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {557AFE26-B89D-497D-92B6-8268D04396E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {557AFE26-B89D-497D-92B6-8268D04396E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {557AFE26-B89D-497D-92B6-8268D04396E5}.Release|Any CPU.Build.0 = Release|Any CPU - {4EFBD262-86B2-4E21-8608-B1894E04EA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4EFBD262-86B2-4E21-8608-B1894E04EA99}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4EFBD262-86B2-4E21-8608-B1894E04EA99}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4EFBD262-86B2-4E21-8608-B1894E04EA99}.Release|Any CPU.Build.0 = Release|Any CPU - {C2EFCD80-C96D-4A09-BFD6-02CB4603961C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2EFCD80-C96D-4A09-BFD6-02CB4603961C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2EFCD80-C96D-4A09-BFD6-02CB4603961C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2EFCD80-C96D-4A09-BFD6-02CB4603961C}.Release|Any CPU.Build.0 = Release|Any CPU - {C437628B-43EE-4AAD-83ED-DDE7499E705B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C437628B-43EE-4AAD-83ED-DDE7499E705B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C437628B-43EE-4AAD-83ED-DDE7499E705B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C437628B-43EE-4AAD-83ED-DDE7499E705B}.Release|Any CPU.Build.0 = Release|Any CPU - {A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C}.Release|Any CPU.Build.0 = Release|Any CPU - {31E52250-3422-49E9-9605-EBE786CC1BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31E52250-3422-49E9-9605-EBE786CC1BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31E52250-3422-49E9-9605-EBE786CC1BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31E52250-3422-49E9-9605-EBE786CC1BE3}.Release|Any CPU.Build.0 = Release|Any CPU - {6D09B0D3-AF28-4FC4-A67D-424E7746682B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D09B0D3-AF28-4FC4-A67D-424E7746682B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D09B0D3-AF28-4FC4-A67D-424E7746682B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D09B0D3-AF28-4FC4-A67D-424E7746682B}.Release|Any CPU.Build.0 = Release|Any CPU - {65CF716D-9215-475C-B37F-CA943F5CD6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {65CF716D-9215-475C-B37F-CA943F5CD6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {65CF716D-9215-475C-B37F-CA943F5CD6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {65CF716D-9215-475C-B37F-CA943F5CD6A3}.Release|Any CPU.Build.0 = Release|Any CPU - {BEB156E9-4E06-403A-B778-E7B092B79962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BEB156E9-4E06-403A-B778-E7B092B79962}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BEB156E9-4E06-403A-B778-E7B092B79962}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BEB156E9-4E06-403A-B778-E7B092B79962}.Release|Any CPU.Build.0 = Release|Any CPU - {BA80CEAD-02A2-480D-93F4-907B01BDB219}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA80CEAD-02A2-480D-93F4-907B01BDB219}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA80CEAD-02A2-480D-93F4-907B01BDB219}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA80CEAD-02A2-480D-93F4-907B01BDB219}.Release|Any CPU.Build.0 = Release|Any CPU - {F9095412-B7FB-4A8A-A291-141458A988AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F9095412-B7FB-4A8A-A291-141458A988AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F9095412-B7FB-4A8A-A291-141458A988AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F9095412-B7FB-4A8A-A291-141458A988AF}.Release|Any CPU.Build.0 = Release|Any CPU - {920BDA8F-094B-4D81-A03C-664C0F808DC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {920BDA8F-094B-4D81-A03C-664C0F808DC7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {920BDA8F-094B-4D81-A03C-664C0F808DC7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {920BDA8F-094B-4D81-A03C-664C0F808DC7}.Release|Any CPU.Build.0 = Release|Any CPU - {86FBBCC7-97F5-4795-A61E-FA7E0CE0514D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {86FBBCC7-97F5-4795-A61E-FA7E0CE0514D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {86FBBCC7-97F5-4795-A61E-FA7E0CE0514D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {86FBBCC7-97F5-4795-A61E-FA7E0CE0514D}.Release|Any CPU.Build.0 = Release|Any CPU - {C3232459-EE86-430B-B310-EB32C991A11A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3232459-EE86-430B-B310-EB32C991A11A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3232459-EE86-430B-B310-EB32C991A11A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3232459-EE86-430B-B310-EB32C991A11A}.Release|Any CPU.Build.0 = Release|Any CPU - {E0B1415D-8D97-4BA5-9315-82E7D33E3933}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E0B1415D-8D97-4BA5-9315-82E7D33E3933}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E0B1415D-8D97-4BA5-9315-82E7D33E3933}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E0B1415D-8D97-4BA5-9315-82E7D33E3933}.Release|Any CPU.Build.0 = Release|Any CPU - {6C116C3E-177A-44C0-A149-8DDB62812DC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C116C3E-177A-44C0-A149-8DDB62812DC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C116C3E-177A-44C0-A149-8DDB62812DC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C116C3E-177A-44C0-A149-8DDB62812DC3}.Release|Any CPU.Build.0 = Release|Any CPU - {E2BE4A3F-1C35-41ED-99D1-7630B5369942}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2BE4A3F-1C35-41ED-99D1-7630B5369942}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2BE4A3F-1C35-41ED-99D1-7630B5369942}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2BE4A3F-1C35-41ED-99D1-7630B5369942}.Release|Any CPU.Build.0 = Release|Any CPU - {C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A}.Release|Any CPU.Build.0 = Release|Any CPU - {D94706F7-7622-4D4C-87BD-84FB40D4E6BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D94706F7-7622-4D4C-87BD-84FB40D4E6BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D94706F7-7622-4D4C-87BD-84FB40D4E6BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D94706F7-7622-4D4C-87BD-84FB40D4E6BD}.Release|Any CPU.Build.0 = Release|Any CPU - {BA38127A-DA5A-437F-9C84-5997FBE0B6A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA38127A-DA5A-437F-9C84-5997FBE0B6A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA38127A-DA5A-437F-9C84-5997FBE0B6A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA38127A-DA5A-437F-9C84-5997FBE0B6A0}.Release|Any CPU.Build.0 = Release|Any CPU - {38EFC635-0C56-442D-8CA3-20AEC1930D7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38EFC635-0C56-442D-8CA3-20AEC1930D7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38EFC635-0C56-442D-8CA3-20AEC1930D7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38EFC635-0C56-442D-8CA3-20AEC1930D7B}.Release|Any CPU.Build.0 = Release|Any CPU - {6499285A-88CF-426A-909D-307382B91AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6499285A-88CF-426A-909D-307382B91AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6499285A-88CF-426A-909D-307382B91AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6499285A-88CF-426A-909D-307382B91AA3}.Release|Any CPU.Build.0 = Release|Any CPU - {6A064B5A-21F7-452A-AAFF-3C0A68286FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A064B5A-21F7-452A-AAFF-3C0A68286FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A064B5A-21F7-452A-AAFF-3C0A68286FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A064B5A-21F7-452A-AAFF-3C0A68286FDF}.Release|Any CPU.Build.0 = Release|Any CPU - {0D68FC03-7FD4-4280-852B-0E620872CE22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D68FC03-7FD4-4280-852B-0E620872CE22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D68FC03-7FD4-4280-852B-0E620872CE22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D68FC03-7FD4-4280-852B-0E620872CE22}.Release|Any CPU.Build.0 = Release|Any CPU - {BCC3F7DC-080D-4A7E-8060-40ED65073EB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BCC3F7DC-080D-4A7E-8060-40ED65073EB3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BCC3F7DC-080D-4A7E-8060-40ED65073EB3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BCC3F7DC-080D-4A7E-8060-40ED65073EB3}.Release|Any CPU.Build.0 = Release|Any CPU - {764B3355-FD20-42E3-B452-357FCC20A27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {764B3355-FD20-42E3-B452-357FCC20A27D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {764B3355-FD20-42E3-B452-357FCC20A27D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {764B3355-FD20-42E3-B452-357FCC20A27D}.Release|Any CPU.Build.0 = Release|Any CPU - {7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B}.Release|Any CPU.Build.0 = Release|Any CPU - {C821DCC5-880F-4F85-97B1-6D9A56DAD6F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C821DCC5-880F-4F85-97B1-6D9A56DAD6F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C821DCC5-880F-4F85-97B1-6D9A56DAD6F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C821DCC5-880F-4F85-97B1-6D9A56DAD6F0}.Release|Any CPU.Build.0 = Release|Any CPU - {7866263B-936E-4604-A821-73C46BAB1657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7866263B-936E-4604-A821-73C46BAB1657}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7866263B-936E-4604-A821-73C46BAB1657}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7866263B-936E-4604-A821-73C46BAB1657}.Release|Any CPU.Build.0 = Release|Any CPU - {EA3AFC13-2399-411E-AD20-C6677505615A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA3AFC13-2399-411E-AD20-C6677505615A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3AFC13-2399-411E-AD20-C6677505615A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA3AFC13-2399-411E-AD20-C6677505615A}.Release|Any CPU.Build.0 = Release|Any CPU - {5031A608-281E-4FAF-9501-1DBDAB645907}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5031A608-281E-4FAF-9501-1DBDAB645907}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5031A608-281E-4FAF-9501-1DBDAB645907}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5031A608-281E-4FAF-9501-1DBDAB645907}.Release|Any CPU.Build.0 = Release|Any CPU - {10EC7705-BE9E-4E2A-B174-E74D1CC9852F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10EC7705-BE9E-4E2A-B174-E74D1CC9852F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {10EC7705-BE9E-4E2A-B174-E74D1CC9852F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10EC7705-BE9E-4E2A-B174-E74D1CC9852F}.Release|Any CPU.Build.0 = Release|Any CPU - {AF26B4AC-35B5-4954-ADE8-97BA2DC31250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF26B4AC-35B5-4954-ADE8-97BA2DC31250}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF26B4AC-35B5-4954-ADE8-97BA2DC31250}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF26B4AC-35B5-4954-ADE8-97BA2DC31250}.Release|Any CPU.Build.0 = Release|Any CPU - {5EBE664D-D071-43A9-9B53-346BE3DCCB41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EBE664D-D071-43A9-9B53-346BE3DCCB41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EBE664D-D071-43A9-9B53-346BE3DCCB41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EBE664D-D071-43A9-9B53-346BE3DCCB41}.Release|Any CPU.Build.0 = Release|Any CPU - {196A7EF1-56CE-481E-BB49-97BD0FF096BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {196A7EF1-56CE-481E-BB49-97BD0FF096BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {196A7EF1-56CE-481E-BB49-97BD0FF096BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {196A7EF1-56CE-481E-BB49-97BD0FF096BE}.Release|Any CPU.Build.0 = Release|Any CPU - {B0AC88EC-EB6C-4E73-B9D5-51DD213931B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B0AC88EC-EB6C-4E73-B9D5-51DD213931B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B0AC88EC-EB6C-4E73-B9D5-51DD213931B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B0AC88EC-EB6C-4E73-B9D5-51DD213931B4}.Release|Any CPU.Build.0 = Release|Any CPU - {AAA51F15-E5BE-4E77-92DF-1992434D557A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAA51F15-E5BE-4E77-92DF-1992434D557A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAA51F15-E5BE-4E77-92DF-1992434D557A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAA51F15-E5BE-4E77-92DF-1992434D557A}.Release|Any CPU.Build.0 = Release|Any CPU - {9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D}.Release|Any CPU.Build.0 = Release|Any CPU - {2A975FB7-401B-41BB-96A4-1DF0036888A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A975FB7-401B-41BB-96A4-1DF0036888A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A975FB7-401B-41BB-96A4-1DF0036888A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A975FB7-401B-41BB-96A4-1DF0036888A9}.Release|Any CPU.Build.0 = Release|Any CPU - {C4C38F83-8410-443C-9599-ACFB5FA7CD2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4C38F83-8410-443C-9599-ACFB5FA7CD2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4C38F83-8410-443C-9599-ACFB5FA7CD2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4C38F83-8410-443C-9599-ACFB5FA7CD2D}.Release|Any CPU.Build.0 = Release|Any CPU - {738AFF97-B4C4-4EAC-B9C5-C405D481C92B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {738AFF97-B4C4-4EAC-B9C5-C405D481C92B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {738AFF97-B4C4-4EAC-B9C5-C405D481C92B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {738AFF97-B4C4-4EAC-B9C5-C405D481C92B}.Release|Any CPU.Build.0 = Release|Any CPU - {A9CCC214-212A-4296-98F5-65ADDB2BB8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9CCC214-212A-4296-98F5-65ADDB2BB8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9CCC214-212A-4296-98F5-65ADDB2BB8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9CCC214-212A-4296-98F5-65ADDB2BB8B4}.Release|Any CPU.Build.0 = Release|Any CPU - {51DD3135-1EAA-4640-82F1-9FBECA421708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51DD3135-1EAA-4640-82F1-9FBECA421708}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51DD3135-1EAA-4640-82F1-9FBECA421708}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51DD3135-1EAA-4640-82F1-9FBECA421708}.Release|Any CPU.Build.0 = Release|Any CPU - {6607EBEE-8668-E1D3-C1CB-ED11A91DC100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6607EBEE-8668-E1D3-C1CB-ED11A91DC100}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6607EBEE-8668-E1D3-C1CB-ED11A91DC100}.Release|Any CPU.Build.0 = Release|Any CPU - {5D16D6CC-2BBF-3E53-A932-7E6FCD64E123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D16D6CC-2BBF-3E53-A932-7E6FCD64E123}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D16D6CC-2BBF-3E53-A932-7E6FCD64E123}.Release|Any CPU.Build.0 = Release|Any CPU - {77459A1A-63B4-F7CE-5B20-79091A4BC6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {77459A1A-63B4-F7CE-5B20-79091A4BC6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {77459A1A-63B4-F7CE-5B20-79091A4BC6C8}.Release|Any CPU.Build.0 = Release|Any CPU - {7A06889E-2503-38D9-FE44-36527A9FD0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A06889E-2503-38D9-FE44-36527A9FD0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A06889E-2503-38D9-FE44-36527A9FD0C9}.Release|Any CPU.Build.0 = Release|Any CPU - {5159B5AC-6354-B394-93DD-2621DB302EDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5159B5AC-6354-B394-93DD-2621DB302EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5159B5AC-6354-B394-93DD-2621DB302EDF}.Release|Any CPU.Build.0 = Release|Any CPU - {44D3B2AE-DA53-A134-182B-E4301BB856A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44D3B2AE-DA53-A134-182B-E4301BB856A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44D3B2AE-DA53-A134-182B-E4301BB856A3}.Release|Any CPU.Build.0 = Release|Any CPU - {55DDF80C-42C8-A046-883C-954049FCB811}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55DDF80C-42C8-A046-883C-954049FCB811}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55DDF80C-42C8-A046-883C-954049FCB811}.Release|Any CPU.Build.0 = Release|Any CPU - {98E31A4F-30D0-0F7B-2E9D-D8F1AEB53DA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {98E31A4F-30D0-0F7B-2E9D-D8F1AEB53DA5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {98E31A4F-30D0-0F7B-2E9D-D8F1AEB53DA5}.Release|Any CPU.Build.0 = Release|Any CPU - {3F841B6E-2AEA-23B1-C141-CC67A8FB33E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F841B6E-2AEA-23B1-C141-CC67A8FB33E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F841B6E-2AEA-23B1-C141-CC67A8FB33E0}.Release|Any CPU.Build.0 = Release|Any CPU - {8057DA4A-FF84-72D3-54BE-A17B47760608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8057DA4A-FF84-72D3-54BE-A17B47760608}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8057DA4A-FF84-72D3-54BE-A17B47760608}.Release|Any CPU.Build.0 = Release|Any CPU - {5A179D89-95C6-F103-DA7D-6FD33FDF151B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A179D89-95C6-F103-DA7D-6FD33FDF151B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A179D89-95C6-F103-DA7D-6FD33FDF151B}.Release|Any CPU.Build.0 = Release|Any CPU - {A4F19C30-3624-86C4-F533-D88C9DC2A972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4F19C30-3624-86C4-F533-D88C9DC2A972}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4F19C30-3624-86C4-F533-D88C9DC2A972}.Release|Any CPU.Build.0 = Release|Any CPU - {3E41717A-789B-D213-A4A8-56BFED2E8D17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E41717A-789B-D213-A4A8-56BFED2E8D17}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E41717A-789B-D213-A4A8-56BFED2E8D17}.Release|Any CPU.Build.0 = Release|Any CPU - {63CAD818-CAFA-41ED-E389-C7553AD813FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63CAD818-CAFA-41ED-E389-C7553AD813FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63CAD818-CAFA-41ED-E389-C7553AD813FD}.Release|Any CPU.Build.0 = Release|Any CPU - {C6277231-D388-0D78-CC9F-973172F92585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6277231-D388-0D78-CC9F-973172F92585}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6277231-D388-0D78-CC9F-973172F92585}.Release|Any CPU.Build.0 = Release|Any CPU - {981F5916-AB63-4E99-7762-1BC03CDD8D00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {981F5916-AB63-4E99-7762-1BC03CDD8D00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {981F5916-AB63-4E99-7762-1BC03CDD8D00}.Release|Any CPU.Build.0 = Release|Any CPU - {3105ADCB-D81E-25F2-8390-986E50E3A88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3105ADCB-D81E-25F2-8390-986E50E3A88B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3105ADCB-D81E-25F2-8390-986E50E3A88B}.Release|Any CPU.Build.0 = Release|Any CPU - {2F785E38-6E2A-834A-0C9A-CBE3B307DE01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F785E38-6E2A-834A-0C9A-CBE3B307DE01}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F785E38-6E2A-834A-0C9A-CBE3B307DE01}.Release|Any CPU.Build.0 = Release|Any CPU - {4C3111B7-D8EC-0538-2C30-952EA4D9FC94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C3111B7-D8EC-0538-2C30-952EA4D9FC94}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C3111B7-D8EC-0538-2C30-952EA4D9FC94}.Release|Any CPU.Build.0 = Release|Any CPU - {2388CA32-2573-C097-0B04-B67C58BDB7CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2388CA32-2573-C097-0B04-B67C58BDB7CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2388CA32-2573-C097-0B04-B67C58BDB7CA}.Release|Any CPU.Build.0 = Release|Any CPU - {C5824CE4-D590-20E7-0565-6BDE1D809A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5824CE4-D590-20E7-0565-6BDE1D809A19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5824CE4-D590-20E7-0565-6BDE1D809A19}.Release|Any CPU.Build.0 = Release|Any CPU - {14E1A30B-2E8A-794C-CF13-F47FDEF4ACD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14E1A30B-2E8A-794C-CF13-F47FDEF4ACD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14E1A30B-2E8A-794C-CF13-F47FDEF4ACD3}.Release|Any CPU.Build.0 = Release|Any CPU - {644C6F28-AEC7-AD30-1D65-F459E0F21DC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {644C6F28-AEC7-AD30-1D65-F459E0F21DC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {644C6F28-AEC7-AD30-1D65-F459E0F21DC6}.Release|Any CPU.Build.0 = Release|Any CPU - {18B7AC0F-16B9-B612-A6AA-E092EE68DA1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {18B7AC0F-16B9-B612-A6AA-E092EE68DA1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {18B7AC0F-16B9-B612-A6AA-E092EE68DA1D}.Release|Any CPU.Build.0 = Release|Any CPU - {5B7C359B-81DB-8BF9-D806-6DCEAC1A51D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B7C359B-81DB-8BF9-D806-6DCEAC1A51D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B7C359B-81DB-8BF9-D806-6DCEAC1A51D5}.Release|Any CPU.Build.0 = Release|Any CPU - {EEDC48F4-14C6-4017-0CB4-1A7E2315C685}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEDC48F4-14C6-4017-0CB4-1A7E2315C685}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEDC48F4-14C6-4017-0CB4-1A7E2315C685}.Release|Any CPU.Build.0 = Release|Any CPU - {B1965821-5021-E09B-0107-9BF12D674AB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1965821-5021-E09B-0107-9BF12D674AB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1965821-5021-E09B-0107-9BF12D674AB1}.Release|Any CPU.Build.0 = Release|Any CPU - {C11055B8-9185-2BF4-35E7-65C977DD5DEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C11055B8-9185-2BF4-35E7-65C977DD5DEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C11055B8-9185-2BF4-35E7-65C977DD5DEC}.Release|Any CPU.Build.0 = Release|Any CPU - {F44A6978-521A-1E7B-8841-C1BF15F80CCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F44A6978-521A-1E7B-8841-C1BF15F80CCB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F44A6978-521A-1E7B-8841-C1BF15F80CCB}.Release|Any CPU.Build.0 = Release|Any CPU - {CD01D081-2759-DBD2-A98F-BE5ECE04A403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD01D081-2759-DBD2-A98F-BE5ECE04A403}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD01D081-2759-DBD2-A98F-BE5ECE04A403}.Release|Any CPU.Build.0 = Release|Any CPU - {F7A34F42-9092-1D8D-BEA7-146968B3EE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7A34F42-9092-1D8D-BEA7-146968B3EE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F7A34F42-9092-1D8D-BEA7-146968B3EE3A}.Release|Any CPU.Build.0 = Release|Any CPU - {73DB7209-1957-2295-FB91-3FBC82442B01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73DB7209-1957-2295-FB91-3FBC82442B01}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73DB7209-1957-2295-FB91-3FBC82442B01}.Release|Any CPU.Build.0 = Release|Any CPU - {135C853D-B3A2-8F7C-4926-B52E9392002F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {135C853D-B3A2-8F7C-4926-B52E9392002F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {135C853D-B3A2-8F7C-4926-B52E9392002F}.Release|Any CPU.Build.0 = Release|Any CPU - {E87C0BA6-E772-68FA-34C6-64227DD9F3EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E87C0BA6-E772-68FA-34C6-64227DD9F3EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E87C0BA6-E772-68FA-34C6-64227DD9F3EE}.Release|Any CPU.Build.0 = Release|Any CPU - {5FFC2953-2430-8AC9-0096-3FDBC1CA5140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FFC2953-2430-8AC9-0096-3FDBC1CA5140}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FFC2953-2430-8AC9-0096-3FDBC1CA5140}.Release|Any CPU.Build.0 = Release|Any CPU - {192D3A1C-8554-3D05-DA6E-A57030ECA124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {192D3A1C-8554-3D05-DA6E-A57030ECA124}.Release|Any CPU.ActiveCfg = Release|Any CPU - {192D3A1C-8554-3D05-DA6E-A57030ECA124}.Release|Any CPU.Build.0 = Release|Any CPU - {31EC11DD-755D-FECE-4678-D88E17BFDBB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31EC11DD-755D-FECE-4678-D88E17BFDBB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31EC11DD-755D-FECE-4678-D88E17BFDBB6}.Release|Any CPU.Build.0 = Release|Any CPU - {CB988CB6-A9A2-0365-354E-1464906386FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB988CB6-A9A2-0365-354E-1464906386FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB988CB6-A9A2-0365-354E-1464906386FF}.Release|Any CPU.Build.0 = Release|Any CPU - {5064A781-B3B0-585F-B856-E294556180F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5064A781-B3B0-585F-B856-E294556180F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5064A781-B3B0-585F-B856-E294556180F1}.Release|Any CPU.Build.0 = Release|Any CPU - {97D530C8-97AC-CEBE-9657-CE02557D6ED9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97D530C8-97AC-CEBE-9657-CE02557D6ED9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97D530C8-97AC-CEBE-9657-CE02557D6ED9}.Release|Any CPU.Build.0 = Release|Any CPU - {942F9B69-02CD-469B-C00C-3454E93BA2AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {942F9B69-02CD-469B-C00C-3454E93BA2AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {942F9B69-02CD-469B-C00C-3454E93BA2AE}.Release|Any CPU.Build.0 = Release|Any CPU - {48145FD7-B653-CFE3-B30A-7EF2E0120B21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48145FD7-B653-CFE3-B30A-7EF2E0120B21}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48145FD7-B653-CFE3-B30A-7EF2E0120B21}.Release|Any CPU.Build.0 = Release|Any CPU - {05B5D7F9-FE77-1519-2023-61CD1FAD90B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05B5D7F9-FE77-1519-2023-61CD1FAD90B1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05B5D7F9-FE77-1519-2023-61CD1FAD90B1}.Release|Any CPU.Build.0 = Release|Any CPU - {1BB203B2-65E0-3834-3EB4-0BEC2F75FEBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BB203B2-65E0-3834-3EB4-0BEC2F75FEBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BB203B2-65E0-3834-3EB4-0BEC2F75FEBC}.Release|Any CPU.Build.0 = Release|Any CPU - {E974FA60-F02F-FA1F-BA84-C25972F8DD4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E974FA60-F02F-FA1F-BA84-C25972F8DD4F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E974FA60-F02F-FA1F-BA84-C25972F8DD4F}.Release|Any CPU.Build.0 = Release|Any CPU - {A857F14B-45A2-8244-554B-C3205FC38F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A857F14B-45A2-8244-554B-C3205FC38F08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A857F14B-45A2-8244-554B-C3205FC38F08}.Release|Any CPU.Build.0 = Release|Any CPU - {A12F0AE9-DCB4-9D54-E586-FBBF10CA93F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A12F0AE9-DCB4-9D54-E586-FBBF10CA93F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A12F0AE9-DCB4-9D54-E586-FBBF10CA93F6}.Release|Any CPU.Build.0 = Release|Any CPU - {56589499-4FA9-EC26-7F7E-D187D035D4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56589499-4FA9-EC26-7F7E-D187D035D4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56589499-4FA9-EC26-7F7E-D187D035D4DA}.Release|Any CPU.Build.0 = Release|Any CPU - {0F613620-3071-DF98-3506-F37F016C07E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F613620-3071-DF98-3506-F37F016C07E1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0F613620-3071-DF98-3506-F37F016C07E1}.Release|Any CPU.Build.0 = Release|Any CPU - {C45ED108-A68D-B447-00D1-BCB45EEC0D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C45ED108-A68D-B447-00D1-BCB45EEC0D15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C45ED108-A68D-B447-00D1-BCB45EEC0D15}.Release|Any CPU.Build.0 = Release|Any CPU - {1AFEBB7F-BFB8-2C11-28BF-68AA6376479B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1AFEBB7F-BFB8-2C11-28BF-68AA6376479B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1AFEBB7F-BFB8-2C11-28BF-68AA6376479B}.Release|Any CPU.Build.0 = Release|Any CPU - {63AA9B32-84B1-BE5F-4507-8E7DF73880F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63AA9B32-84B1-BE5F-4507-8E7DF73880F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63AA9B32-84B1-BE5F-4507-8E7DF73880F2}.Release|Any CPU.Build.0 = Release|Any CPU - {83653A8C-7946-338C-F42C-AB714F49991D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83653A8C-7946-338C-F42C-AB714F49991D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83653A8C-7946-338C-F42C-AB714F49991D}.Release|Any CPU.Build.0 = Release|Any CPU - {32481813-3CE4-F4C8-086F-15FED097835F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32481813-3CE4-F4C8-086F-15FED097835F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32481813-3CE4-F4C8-086F-15FED097835F}.Release|Any CPU.Build.0 = Release|Any CPU - {4F6C59C7-0311-8ADF-017D-E64F46510A70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F6C59C7-0311-8ADF-017D-E64F46510A70}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F6C59C7-0311-8ADF-017D-E64F46510A70}.Release|Any CPU.Build.0 = Release|Any CPU - {FF8B81BD-63BA-BEE6-2466-B1A3EE553494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF8B81BD-63BA-BEE6-2466-B1A3EE553494}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF8B81BD-63BA-BEE6-2466-B1A3EE553494}.Release|Any CPU.Build.0 = Release|Any CPU - {9D7D20F4-F986-4194-9D18-4F28654EDE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D7D20F4-F986-4194-9D18-4F28654EDE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D7D20F4-F986-4194-9D18-4F28654EDE7D}.Release|Any CPU.Build.0 = Release|Any CPU - {A90ADACF-3BC4-3C34-F6D6-D02D99C799D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A90ADACF-3BC4-3C34-F6D6-D02D99C799D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A90ADACF-3BC4-3C34-F6D6-D02D99C799D5}.Release|Any CPU.Build.0 = Release|Any CPU - {75087953-E456-EA40-857F-B38A2BF4DD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {75087953-E456-EA40-857F-B38A2BF4DD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {75087953-E456-EA40-857F-B38A2BF4DD1D}.Release|Any CPU.Build.0 = Release|Any CPU - {CF879664-186D-AE54-434E-F0FC3B5D27DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF879664-186D-AE54-434E-F0FC3B5D27DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF879664-186D-AE54-434E-F0FC3B5D27DC}.Release|Any CPU.Build.0 = Release|Any CPU - {D377B3DB-B229-DC7A-D651-320B5904BC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D377B3DB-B229-DC7A-D651-320B5904BC29}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D377B3DB-B229-DC7A-D651-320B5904BC29}.Release|Any CPU.Build.0 = Release|Any CPU - {F42A9BC5-DA3E-A1CE-B48E-7EBF59D8D161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F42A9BC5-DA3E-A1CE-B48E-7EBF59D8D161}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F42A9BC5-DA3E-A1CE-B48E-7EBF59D8D161}.Release|Any CPU.Build.0 = Release|Any CPU - {51FBF981-91B2-407E-7A10-D28E5FB717F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51FBF981-91B2-407E-7A10-D28E5FB717F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51FBF981-91B2-407E-7A10-D28E5FB717F4}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {61812938-5132-4AB6-B48D-2DF4189B3E37} = {59874241-E276-4035-B31D-14924889A1C9} - {12BB796A-63C5-40D0-A2DE-80D9996DE571} = {59874241-E276-4035-B31D-14924889A1C9} - {E58EB8CA-2389-4A28-B0EF-34C2B57BB270} = {59874241-E276-4035-B31D-14924889A1C9} - {8F27A2D4-FEF2-4783-99C4-6B2ABA3D9431} = {59874241-E276-4035-B31D-14924889A1C9} - {DFE642E6-1CD0-4485-AC86-43CEBC451484} = {59874241-E276-4035-B31D-14924889A1C9} - {C67D2028-D637-4D81-84A2-5D64F2E389DA} = {59874241-E276-4035-B31D-14924889A1C9} - {1ACC6991-7D4C-48B2-A41C-4B179B19A85C} = {59874241-E276-4035-B31D-14924889A1C9} - {880208DF-05C6-4763-A447-744D6C8DBDEA} = {59874241-E276-4035-B31D-14924889A1C9} - {3084B070-6F72-4673-889F-EF00DADB468B} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {727CC05F-A99D-474F-9C66-A9E57D6B25CC} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {EBFDDFDE-BF5D-4607-AADA-7039445E2AB7} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {C51FBF31-BFEE-460B-B182-49CD65F8F8EA} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {0E5746A9-C13B-4BB9-BF3B-C524F4E5BC3A} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {25F1BF2F-1B6A-466A-A3BF-8963B0D040F4} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {5802F580-3E70-45F4-AF60-4F4E5F8FAADA} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {1E9A51CC-1E5E-4BB1-9E47-58D6C2DB8450} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {98489BC4-B03B-4703-B25D-A89D617E761F} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {B1B7522F-6EF2-4935-BE9F-66B6DB974303} = {3CBE0336-C3C8-4DC2-AE45-86FC1D1B3C25} - {34891C2A-FAF2-466A-90A6-F25DBD8283E8} = {3CBE0336-C3C8-4DC2-AE45-86FC1D1B3C25} - {72C7AD35-98E4-478E-B594-06A3DB2E61D6} = {3CBE0336-C3C8-4DC2-AE45-86FC1D1B3C25} - {C3098547-C3B5-4136-9CEB-30466EE1FA53} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {E6DBF77D-04A3-4422-A7D2-D3131DF5B829} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {B5D0C674-1958-43BF-B644-0865E9DDCCBD} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {23B95817-4183-4B42-BF27-EA575184DB2D} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {43DC1B99-588C-4984-AEAC-43AA5B47D84C} = {DDB99068-891F-4937-98B0-E72CC3F4964B} - {04BC571A-0055-4B9B-805E-D2B0823C9D8D} = {DDB99068-891F-4937-98B0-E72CC3F4964B} - {990DCEE9-D88F-4B9F-8298-678B524AC4D6} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {557AFE26-B89D-497D-92B6-8268D04396E5} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {4EFBD262-86B2-4E21-8608-B1894E04EA99} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {C2EFCD80-C96D-4A09-BFD6-02CB4603961C} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {C437628B-43EE-4AAD-83ED-DDE7499E705B} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {A4D6D08A-BC0C-4CAD-97E6-F4BAAA03928C} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {31E52250-3422-49E9-9605-EBE786CC1BE3} = {5128206A-242E-4069-AD30-910EDC40B165} - {6D09B0D3-AF28-4FC4-A67D-424E7746682B} = {5128206A-242E-4069-AD30-910EDC40B165} - {65CF716D-9215-475C-B37F-CA943F5CD6A3} = {59874241-E276-4035-B31D-14924889A1C9} - {BEB156E9-4E06-403A-B778-E7B092B79962} = {59874241-E276-4035-B31D-14924889A1C9} - {BA80CEAD-02A2-480D-93F4-907B01BDB219} = {59874241-E276-4035-B31D-14924889A1C9} - {F9095412-B7FB-4A8A-A291-141458A988AF} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {920BDA8F-094B-4D81-A03C-664C0F808DC7} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {86FBBCC7-97F5-4795-A61E-FA7E0CE0514D} = {DDB99068-891F-4937-98B0-E72CC3F4964B} - {C3232459-EE86-430B-B310-EB32C991A11A} = {3CBE0336-C3C8-4DC2-AE45-86FC1D1B3C25} - {E0B1415D-8D97-4BA5-9315-82E7D33E3933} = {DDB99068-891F-4937-98B0-E72CC3F4964B} - {6C116C3E-177A-44C0-A149-8DDB62812DC3} = {DDB99068-891F-4937-98B0-E72CC3F4964B} - {E2BE4A3F-1C35-41ED-99D1-7630B5369942} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {C5DDEE8C-E3EF-49BC-AEEE-B35271FC512A} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {D94706F7-7622-4D4C-87BD-84FB40D4E6BD} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {BA38127A-DA5A-437F-9C84-5997FBE0B6A0} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {38EFC635-0C56-442D-8CA3-20AEC1930D7B} = {EA9C3A73-3F31-4DC9-982C-963CE613E119} - {6499285A-88CF-426A-909D-307382B91AA3} = {EA9C3A73-3F31-4DC9-982C-963CE613E119} - {6A064B5A-21F7-452A-AAFF-3C0A68286FDF} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {0D68FC03-7FD4-4280-852B-0E620872CE22} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {BCC3F7DC-080D-4A7E-8060-40ED65073EB3} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {764B3355-FD20-42E3-B452-357FCC20A27D} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {7603B7DF-0297-4CF7-BB31-D5B8BABF7D5B} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {C821DCC5-880F-4F85-97B1-6D9A56DAD6F0} = {EA9C3A73-3F31-4DC9-982C-963CE613E119} - {7866263B-936E-4604-A821-73C46BAB1657} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {EA3AFC13-2399-411E-AD20-C6677505615A} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {5031A608-281E-4FAF-9501-1DBDAB645907} = {31BAEBB1-696E-44A1-B1EF-0D150E6D2559} - {10EC7705-BE9E-4E2A-B174-E74D1CC9852F} = {4AB95F47-0C93-4C88-B87F-231262CD0E89} - {AF26B4AC-35B5-4954-ADE8-97BA2DC31250} = {5128206A-242E-4069-AD30-910EDC40B165} - {5EBE664D-D071-43A9-9B53-346BE3DCCB41} = {5128206A-242E-4069-AD30-910EDC40B165} - {196A7EF1-56CE-481E-BB49-97BD0FF096BE} = {5128206A-242E-4069-AD30-910EDC40B165} - {B0AC88EC-EB6C-4E73-B9D5-51DD213931B4} = {5128206A-242E-4069-AD30-910EDC40B165} - {AAA51F15-E5BE-4E77-92DF-1992434D557A} = {5128206A-242E-4069-AD30-910EDC40B165} - {9E83FDF7-D838-4E7E-A767-6FEAC6BC1C5D} = {5128206A-242E-4069-AD30-910EDC40B165} - {2A975FB7-401B-41BB-96A4-1DF0036888A9} = {DF26D677-72A3-442D-B556-46E3D0EF4A77} - {C4C38F83-8410-443C-9599-ACFB5FA7CD2D} = {DF26D677-72A3-442D-B556-46E3D0EF4A77} - {738AFF97-B4C4-4EAC-B9C5-C405D481C92B} = {59874241-E276-4035-B31D-14924889A1C9} - {A9CCC214-212A-4296-98F5-65ADDB2BB8B4} = {59874241-E276-4035-B31D-14924889A1C9} - {51DD3135-1EAA-4640-82F1-9FBECA421708} = {8BD7D5E5-D887-4BD7-9F42-725A8714F7BC} - {6607EBEE-8668-E1D3-C1CB-ED11A91DC100} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5D16D6CC-2BBF-3E53-A932-7E6FCD64E123} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {77459A1A-63B4-F7CE-5B20-79091A4BC6C8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {7A06889E-2503-38D9-FE44-36527A9FD0C9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5159B5AC-6354-B394-93DD-2621DB302EDF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {44D3B2AE-DA53-A134-182B-E4301BB856A3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {55DDF80C-42C8-A046-883C-954049FCB811} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {98E31A4F-30D0-0F7B-2E9D-D8F1AEB53DA5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {3F841B6E-2AEA-23B1-C141-CC67A8FB33E0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {8057DA4A-FF84-72D3-54BE-A17B47760608} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5A179D89-95C6-F103-DA7D-6FD33FDF151B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {A4F19C30-3624-86C4-F533-D88C9DC2A972} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {3E41717A-789B-D213-A4A8-56BFED2E8D17} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {63CAD818-CAFA-41ED-E389-C7553AD813FD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {C6277231-D388-0D78-CC9F-973172F92585} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {981F5916-AB63-4E99-7762-1BC03CDD8D00} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {3105ADCB-D81E-25F2-8390-986E50E3A88B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {2F785E38-6E2A-834A-0C9A-CBE3B307DE01} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {4C3111B7-D8EC-0538-2C30-952EA4D9FC94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {2388CA32-2573-C097-0B04-B67C58BDB7CA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {C5824CE4-D590-20E7-0565-6BDE1D809A19} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {14E1A30B-2E8A-794C-CF13-F47FDEF4ACD3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {644C6F28-AEC7-AD30-1D65-F459E0F21DC6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {18B7AC0F-16B9-B612-A6AA-E092EE68DA1D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5B7C359B-81DB-8BF9-D806-6DCEAC1A51D5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {EEDC48F4-14C6-4017-0CB4-1A7E2315C685} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {B1965821-5021-E09B-0107-9BF12D674AB1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {C11055B8-9185-2BF4-35E7-65C977DD5DEC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {F44A6978-521A-1E7B-8841-C1BF15F80CCB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {CD01D081-2759-DBD2-A98F-BE5ECE04A403} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {F7A34F42-9092-1D8D-BEA7-146968B3EE3A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {73DB7209-1957-2295-FB91-3FBC82442B01} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {135C853D-B3A2-8F7C-4926-B52E9392002F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {E87C0BA6-E772-68FA-34C6-64227DD9F3EE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5FFC2953-2430-8AC9-0096-3FDBC1CA5140} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {192D3A1C-8554-3D05-DA6E-A57030ECA124} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {31EC11DD-755D-FECE-4678-D88E17BFDBB6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {CB988CB6-A9A2-0365-354E-1464906386FF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {5064A781-B3B0-585F-B856-E294556180F1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {97D530C8-97AC-CEBE-9657-CE02557D6ED9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {942F9B69-02CD-469B-C00C-3454E93BA2AE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {48145FD7-B653-CFE3-B30A-7EF2E0120B21} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {05B5D7F9-FE77-1519-2023-61CD1FAD90B1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {1BB203B2-65E0-3834-3EB4-0BEC2F75FEBC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {E974FA60-F02F-FA1F-BA84-C25972F8DD4F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {A857F14B-45A2-8244-554B-C3205FC38F08} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {A12F0AE9-DCB4-9D54-E586-FBBF10CA93F6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {56589499-4FA9-EC26-7F7E-D187D035D4DA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {0F613620-3071-DF98-3506-F37F016C07E1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {C45ED108-A68D-B447-00D1-BCB45EEC0D15} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {1AFEBB7F-BFB8-2C11-28BF-68AA6376479B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {63AA9B32-84B1-BE5F-4507-8E7DF73880F2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {83653A8C-7946-338C-F42C-AB714F49991D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {32481813-3CE4-F4C8-086F-15FED097835F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {4F6C59C7-0311-8ADF-017D-E64F46510A70} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {FF8B81BD-63BA-BEE6-2466-B1A3EE553494} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {9D7D20F4-F986-4194-9D18-4F28654EDE7D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {A90ADACF-3BC4-3C34-F6D6-D02D99C799D5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {75087953-E456-EA40-857F-B38A2BF4DD1D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {CF879664-186D-AE54-434E-F0FC3B5D27DC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {D377B3DB-B229-DC7A-D651-320B5904BC29} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {F42A9BC5-DA3E-A1CE-B48E-7EBF59D8D161} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {51FBF981-91B2-407E-7A10-D28E5FB717F4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {AB0245B9-2464-47F8-BE15-D80A7A2FA965} - EndGlobalSection -EndGlobal diff --git a/src/Steeltoe.All.slnx b/src/Steeltoe.All.slnx new file mode 100644 index 0000000000..dbe0f1cafb --- /dev/null +++ b/src/Steeltoe.All.slnx @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Steeltoe.All.sln.DotSettings b/src/Steeltoe.All.slnx.DotSettings similarity index 86% rename from src/Steeltoe.All.sln.DotSettings rename to src/Steeltoe.All.slnx.DotSettings index b57f8f9c6d..1eb9393ada 100644 --- a/src/Steeltoe.All.sln.DotSettings +++ b/src/Steeltoe.All.slnx.DotSettings @@ -3,20 +3,16 @@ 2000 3000 False - ExplicitlyExcluded - ExplicitlyExcluded - ExplicitlyExcluded - ExplicitlyExcluded - ExplicitlyExcluded - ExplicitlyExcluded - 2A975FB7-401B-41BB-96A4-1DF0036888A9 - 61812938-5132-4AB6-B48D-2DF4189B3E37/f:ConfigurationSchemaAttributes.cs - C4C38F83-8410-443C-9599-ACFB5FA7CD2D - C4C38F83-8410-443C-9599-ACFB5FA7CD2D/f:ConfigurationSchemaAttributes.cs/l:..?..?..?Common?src?Abstractions?ConfigurationSchemaAttributes.cs - DC1BC61A-E0FA-4CF9-9F24-D4C564A07836/f:Directory.Build.targets/l:..?Directory.Build.targets + ExplicitlyExcluded + ExplicitlyExcluded + ExplicitlyExcluded + 2C62D385-0462-A9A1-B49F-11B2CA4C133B/f:Directory.Build.targets/l:..?Directory.Build.targets + 790DD63C-8905-7556-AEBC-3CA429B0A44A + 790DD63C-8905-7556-AEBC-3CA429B0A44A/f:ConfigurationSchemaAttributes.cs/l:..?..?..?Common?src?Common?ConfigurationSchemaAttributes.cs + 9D8CC586-97FA-57A6-1702-125D9D03645D/f:ConfigurationSchemaAttributes.cs + CFB09DDD-8A94-3860-BFC2-111EC05320C3 SOLUTION True - True SUGGESTION SUGGESTION SUGGESTION @@ -24,6 +20,7 @@ SUGGESTION SUGGESTION SUGGESTION + WARNING SUGGESTION SUGGESTION SUGGESTION @@ -58,7 +55,9 @@ WARNING SUGGESTION DO_NOT_SHOW + DO_NOT_SHOW WARNING + SUGGESTION DO_NOT_SHOW HINT SUGGESTION @@ -78,11 +77,14 @@ WARNING SUGGESTION SUGGESTION + WARNING SUGGESTION WARNING WARNING WARNING + WARNING DO_NOT_SHOW + DO_NOT_SHOW WARNING WARNING WARNING @@ -90,6 +92,7 @@ WARNING SUGGESTION WARNING + SUGGESTION HINT WARNING WARNING @@ -100,14 +103,16 @@ SUGGESTION WARNING True + ShowAndRun SUGGESTION False - <?xml version="1.0" encoding="utf-16"?><Profile name="Steeltoe Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSReformatInactiveBranches>True</CSReformatInactiveBranches><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> + <?xml version="1.0" encoding="utf-16"?><Profile name="Steeltoe Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" ArrangeAccessors="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSReformatInactiveBranches>True</CSReformatInactiveBranches><CSharpReformatComments>True</CSharpReformatComments><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> Steeltoe Full Cleanup Required Required Required Required + StringEmpty Conditional False False @@ -164,6 +169,7 @@ WRAP_IF_LONG True True + 2 True 2 @@ -193,8 +199,7 @@ OnSingleLine False 160 - <?xml version="1.0" encoding="utf-16"?> -<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> <TypePattern DisplayName="Non-reorderable types" RemoveRegions="All"> <TypePattern.Match> <Or> @@ -218,7 +223,7 @@ <HasMember> <And> <Kind Is="Method" /> - <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + <HasAttribute Inherited="True" Name="Xunit.FactAttribute" /> </And> </HasMember> </And> @@ -230,7 +235,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Group DisplayName="Fields"> @@ -243,7 +248,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Static"> @@ -257,7 +262,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance Readonly"> @@ -271,7 +276,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -287,7 +292,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -300,7 +305,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -313,7 +318,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -324,7 +329,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Group DisplayName="Events"> @@ -336,7 +341,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -349,7 +354,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -364,7 +369,7 @@ </Or> </Entry.Match> <Entry.SortBy> - <Kind Order="Constructor" /> + <Kind Is="0" Order="Constructor" /> </Entry.SortBy> </Entry> <Entry DisplayName="Test methods" Priority="100"> @@ -374,6 +379,8 @@ <Or> <HasAttribute Name="Xunit.FactAttribute" /> <HasAttribute Name="Xunit.TheoryAttribute" /> + <HasAttribute Name="FactSkippedOnPlatform" /> + <HasAttribute Name="TheorySkippedOnPlatform" /> </Or> </And> </Entry.Match> @@ -395,7 +402,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Nested types"> @@ -412,7 +419,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Group DisplayName="Fields"> @@ -425,7 +432,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Static"> @@ -439,7 +446,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance Readonly"> @@ -453,7 +460,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -469,7 +476,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -482,7 +489,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -495,7 +502,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -506,7 +513,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Group DisplayName="Events"> @@ -518,7 +525,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Instance"> @@ -531,7 +538,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> </Group> @@ -554,7 +561,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access /> + <Access Is="0" /> </Entry.SortBy> </Entry> </Group> @@ -575,7 +582,7 @@ </And> </Entry.Match> <Entry.SortBy> - <Access Order="Private Internal Protected ProtectedInternal Public" /> + <Access Is="0" Order="Private Internal Protected ProtectedInternal Public" /> </Entry.SortBy> </Entry> <Entry DisplayName="Nested types"> @@ -601,20 +608,18 @@ See the LICENSE file in the project root for more information. IO IP MQ + OS OSX UAA False <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> - <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> - <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, PrivateProtected" Description="Protected fields"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> - <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, PrivateProtected" Description="Protected fields"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="True" Prefix="" Suffix="" Style="aaBb" /></Policy> True True True True True - True True True True @@ -637,7 +642,6 @@ See the LICENSE file in the project root for more information. True CSHARP False - Replace argument null check with Guard clause System.ArgumentNullException.ThrowIfNull($argument$); $left$ = $right$; $left$ = $right$ ?? throw new ArgumentNullException(nameof($argument$)); @@ -652,7 +656,6 @@ $left$ = $right$; True CSHARP False - Replace argument null check with Guard clause System.ArgumentNullException.ThrowIfNull($argument$); if ($argument$ == null) throw new ArgumentNullException(nameof($argument$)); WARNING diff --git a/src/Steeltoe.Common.slnf b/src/Steeltoe.Common.slnf index 66c2b96d6b..99f121b7cc 100644 --- a/src/Steeltoe.Common.slnf +++ b/src/Steeltoe.Common.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Certificates\\Steeltoe.Common.Certificates.csproj", "Common\\src\\Common\\Steeltoe.Common.csproj", diff --git a/src/Steeltoe.Configuration.slnf b/src/Steeltoe.Configuration.slnf index 0eb8a847ae..e471516cf7 100644 --- a/src/Steeltoe.Configuration.slnf +++ b/src/Steeltoe.Configuration.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Certificates\\Steeltoe.Common.Certificates.csproj", "Common\\src\\Common\\Steeltoe.Common.csproj", diff --git a/src/Steeltoe.Connectors.slnf b/src/Steeltoe.Connectors.slnf index 68b3607897..c631a0bea4 100644 --- a/src/Steeltoe.Connectors.slnf +++ b/src/Steeltoe.Connectors.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Common\\Steeltoe.Common.csproj", "Common\\src\\Hosting\\Steeltoe.Common.Hosting.csproj", diff --git a/src/Steeltoe.Discovery.slnf b/src/Steeltoe.Discovery.slnf index 4b0800bfa3..206ca6c486 100644 --- a/src/Steeltoe.Discovery.slnf +++ b/src/Steeltoe.Discovery.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Certificates\\Steeltoe.Common.Certificates.csproj", "Common\\src\\Common\\Steeltoe.Common.csproj", diff --git a/src/Steeltoe.Logging.slnf b/src/Steeltoe.Logging.slnf index ddb9815af3..9b8554a5d1 100644 --- a/src/Steeltoe.Logging.slnf +++ b/src/Steeltoe.Logging.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Common\\Steeltoe.Common.csproj", "Common\\src\\Hosting\\Steeltoe.Common.Hosting.csproj", diff --git a/src/Steeltoe.Management.slnf b/src/Steeltoe.Management.slnf index ef0472875f..121eea921c 100644 --- a/src/Steeltoe.Management.slnf +++ b/src/Steeltoe.Management.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Certificates\\Steeltoe.Common.Certificates.csproj", "Common\\src\\Common\\Steeltoe.Common.csproj", diff --git a/src/Steeltoe.Security.slnf b/src/Steeltoe.Security.slnf index 072a044572..7efff59059 100644 --- a/src/Steeltoe.Security.slnf +++ b/src/Steeltoe.Security.slnf @@ -1,6 +1,6 @@ { "solution": { - "path": "Steeltoe.All.sln", + "path": "Steeltoe.All.slnx", "projects": [ "Common\\src\\Certificates\\Steeltoe.Common.Certificates.csproj", "Common\\src\\Common\\Steeltoe.Common.csproj", diff --git a/src/Tools/src/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs b/src/Tools/src/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs index be41d09a7f..aa1b9ece30 100644 --- a/src/Tools/src/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs +++ b/src/Tools/src/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs @@ -721,18 +721,13 @@ private static string[] CreateExclusionPaths(List? exclusionPaths) private static void ReplaceNodeWithKeyCasingChange(JsonObject jsonObject, string key, JsonNode value) { - // In System.Text.Json v9, the casing of the new key is not adapted. See https://github.com/dotnet/runtime/issues/108790. - // So instead, remove the existing node and insert a new one with the updated key. - var index = jsonObject.IndexOf(key); - if (index != -1) + if (!jsonObject.TryAdd(key, value, out var index)) { + // Starting from System.Text.Json v9, the casing of the new key is not adapted. See https://github.com/dotnet/runtime/issues/108790. + // So instead, remove the existing node and insert a new one with the updated key. jsonObject.RemoveAt(index); jsonObject.Insert(index, key, value); } - else - { - jsonObject[key] = value; - } } private sealed class SchemaOrderJsonNodeConverter : JsonConverter diff --git a/src/Tools/src/ConfigurationSchemaGenerator/Program.cs b/src/Tools/src/ConfigurationSchemaGenerator/Program.cs index 9bd9ca1f6c..0a11766498 100644 --- a/src/Tools/src/ConfigurationSchemaGenerator/Program.cs +++ b/src/Tools/src/ConfigurationSchemaGenerator/Program.cs @@ -15,4 +15,4 @@ #endif var rootCommand = RootGenerateCommand.GetCommand(); -return await rootCommand.Parse(args).InvokeAsync(CancellationToken.None).ConfigureAwait(false); +return await rootCommand.Parse(args).InvokeAsync().ConfigureAwait(false); diff --git a/src/Tools/src/ConfigurationSchemaGenerator/RootGenerateCommand.cs b/src/Tools/src/ConfigurationSchemaGenerator/RootGenerateCommand.cs index 534db7d6be..e2403188cb 100644 --- a/src/Tools/src/ConfigurationSchemaGenerator/RootGenerateCommand.cs +++ b/src/Tools/src/ConfigurationSchemaGenerator/RootGenerateCommand.cs @@ -6,56 +6,49 @@ #pragma warning disable using System.CommandLine; -using System.CommandLine.Invocation; namespace ConfigurationSchemaGenerator; internal static class RootGenerateCommand { - private static readonly GenerateCommandDefaultHandler s_formatCommandHandler = new(); - - private static readonly CliOption s_inputOption = new("--input") + private static readonly Option s_inputOption = new("--input") { Required = true, Description = "The assembly to generate a ConfigurationSchema.json file for.", }; - private static readonly CliOption s_referencesOption = new("--reference") + private static readonly Option s_referencesOption = new("--reference") { AllowMultipleArgumentsPerToken = true, Required = true, Description = "The assemblies referenced by the input assembly.", }; - private static readonly CliOption s_outputOption = new("--output") + private static readonly Option s_outputOption = new("--output") { Required = true, Description = "The FilePath assembly to generate a ConfigurationSchema.json file for.", }; - public static CliRootCommand GetCommand() + public static RootCommand GetCommand() { - var formatCommand = new CliRootCommand("Generates ConfigurationSchema.json files.") + var formatCommand = new RootCommand("Generates ConfigurationSchema.json files.") { s_inputOption, s_referencesOption, s_outputOption, }; - formatCommand.Action = s_formatCommandHandler; - return formatCommand; - } - private sealed class GenerateCommandDefaultHandler : SynchronousCliAction - { - public override int Invoke(ParseResult parseResult) + formatCommand.SetAction(static parseResult => { - var inputAssembly = parseResult.GetValue(s_inputOption); - var references = parseResult.GetValue(s_referencesOption); - var outputFile = parseResult.GetValue(s_outputOption); + var inputAssembly = parseResult.GetValue(s_inputOption); + var references = parseResult.GetValue(s_referencesOption); + var outputFile = parseResult.GetValue(s_outputOption); ConfigSchemaGenerator.GenerateSchema(inputAssembly, references, outputFile); - return 0; - } + }); + + return formatCommand; } } diff --git a/src/Tools/test/ConfigurationSchemaGenerator.Tests/ConfigurationSchemaGenerator.Tests.csproj b/src/Tools/test/ConfigurationSchemaGenerator.Tests/ConfigurationSchemaGenerator.Tests.csproj index b3b0ca4194..eeb1f5cf99 100644 --- a/src/Tools/test/ConfigurationSchemaGenerator.Tests/ConfigurationSchemaGenerator.Tests.csproj +++ b/src/Tools/test/ConfigurationSchemaGenerator.Tests/ConfigurationSchemaGenerator.Tests.csproj @@ -1,14 +1,13 @@ - net9.0;net8.0 - false - enable + net10.0;net9.0;net8.0 true true true + $(NoWarn);S104;SA1636 - + @@ -23,12 +22,8 @@ - - - - diff --git a/src/Tools/test/ConfigurationSchemaGenerator.Tests/GeneratorTests.cs b/src/Tools/test/ConfigurationSchemaGenerator.Tests/GeneratorTests.cs index 480464db8a..73ce367b66 100644 --- a/src/Tools/test/ConfigurationSchemaGenerator.Tests/GeneratorTests.cs +++ b/src/Tools/test/ConfigurationSchemaGenerator.Tests/GeneratorTests.cs @@ -29,6 +29,7 @@ namespace ConfigurationSchemaGenerator.Tests; +/// public partial class GeneratorTests { private static readonly SyntaxTree s_implicitUsingsSyntaxTree = SyntaxFactory.ParseSyntaxTree(SourceText.From( diff --git a/src/testenvironments.json b/src/testenvironments.json index 539ead6b04..ebd64f1380 100644 --- a/src/testenvironments.json +++ b/src/testenvironments.json @@ -1,10 +1,15 @@ { - "version": "1", - "environments": [ - { - "name": "Ubuntu", - "type": "wsl", - "wslDistribution": "Ubuntu" - } - ] + "version": "1", + "environments": [ + { + "name": "Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu" + }, + { + "name": "Ubuntu-22.04", + "type": "wsl", + "wslDistribution": "Ubuntu-22.04" + } + ] } diff --git a/versions.props b/versions.props index a11c13fea6..c81daec6e1 100644 --- a/versions.props +++ b/versions.props @@ -6,37 +6,47 @@ --> 9.0.* - 6.0.* + 10.0.* 7.2.* - 3.51.* - 4.14.* - 6.0.* + 3.58.* + 5.0.* + 7.0.* 7.0.* - 3.4.* + 3.8.* 4.20.69 - 2.4.* - 9.3.* + 2.5.* + 9.7.* 13.0.* - 4.14.* - 7.1.* + 3.3.* + 7.2.* 4.0.* 8.4.* - 10.9.0.115408 + 10.25.0.139117 1.2.0-beta.556 - 2.0.0-beta4.24324.3 - 8.12.* + 2.0.* + 8.15.* 4.9.* - 17.14.* - 2.0.* + 18.5.* + 3.2.* 3.1.* 8.0.* + $(EntityFrameworkCoreTestVersion) - 9.0.*-* + 9.0.* + $(EntityFrameworkCoreTestVersion) + + + + 10.0.* + + + 9.0.* + @@ -49,29 +59,29 @@ 2.2.* 1.7.14.* - 9.0.621003 + 9.0.652701 8.0.* - 9.0.* + 10.0.* - 8.12.* - 0.2.621003 - 3.1.16 - 1.12.*-* - 1.12.* + 8.15.* + 0.2.652701 + 3.1.23 + 1.15.*-* + 1.15.* 9.0.* - 6.0.* + 6.1.* 8.0.* @@ -82,10 +92,21 @@ 9.0.* + + + + + 10.0.* + +