├── icon.png ├── logo.png ├── public.snk ├── NOTICE ├── samples ├── Samples │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Settings.cs │ ├── Samples.csproj │ ├── Controllers │ │ └── ValuesController.cs │ ├── Properties │ │ └── launchSettings.json │ └── Program.cs └── Samples.sln ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation.yml │ ├── feature-request.yml │ └── bug-report.yml ├── dependabot.yml ├── workflows │ ├── handle-stale-discussions.yml │ ├── closed-issue-message.yml │ ├── semgrep-analysis.yml │ ├── issue-regression-labeler.yml │ ├── change-file-in-pr.yml │ ├── aws-ci.yml │ ├── stale_issues.yml │ ├── create-release-pr.yml │ └── sync-main-dev.yml └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── .autover └── autover.json ├── src ├── ruleset.xml └── Amazon.Extensions.Configuration.SystemsManager │ ├── Internal │ ├── ISystemsManagerProcessor.cs │ ├── DictionaryExtensions.cs │ ├── AwsOptionsProvider.cs │ ├── ServiceClientAppender.cs │ ├── JsonConfigurationParser.cs │ └── SystemsManagerProcessor.cs │ ├── SystemsManagerExceptionContext.cs │ ├── IParameterProcessor.cs │ ├── JsonOrStringParameterProcessor.cs │ ├── JsonParameterProcessor.cs │ ├── ISystemsManagerConfigurationSource.cs │ ├── DuplicateParameterException.cs │ ├── ConfigurationExtensions.cs │ ├── SystemsManagerConfigurationSource.cs │ ├── DefaultParameterProcessor.cs │ ├── AppConfig │ ├── AppConfigConfigurationSource.cs │ ├── AppConfigProcessor.cs │ └── AppConfigForLambdaExtensions.cs │ ├── Amazon.Extensions.Configuration.SystemsManager.csproj │ ├── Utils │ └── ParameterProcessorUtil.cs │ ├── SystemsManagerConfigurationProvider.cs │ └── SystemsManagerExtensions.cs ├── test ├── Amazon.Extensions.Configuration.SystemsManager.Integ │ ├── Amazon.Extensions.Configuration.SystemsManager.Integ.csproj │ ├── ConfigurationBuilderIntegrationTests.cs │ ├── ConfigurtaionBuilderIntegrationTestFixture.cs │ └── AppConfigEndToEndTests.cs └── Amazon.Extensions.Configuration.SystemsManager.Tests │ ├── Amazon.Extensions.Configuration.SystemsManager.Tests.csproj │ ├── SystemsManagerProcessorTests.cs │ ├── AppConfigConfigurationSourceTests.cs │ ├── SystemsManagerConfigurationSourceTests.cs │ ├── DictionaryExtensionsTests.cs │ ├── JsonParameterProcessorTests.cs │ ├── AppConfigExtensionsTests.cs │ ├── AppConfigForLambdaExtensionsTests.cs │ ├── Utils │ └── ParameterProcessorUtilTests.cs │ ├── SystemsManagerConfigurationProviderTests.cs │ ├── DefaultParameterProcessorTests.cs │ ├── SystemsManagerExtensionsTests.cs │ ├── JsonOrStringParameterProcessorTests.cs │ └── AppConfigProcessorTests.cs ├── .gitignore ├── Amazon.Extensions.Configuration.sln ├── CONTRIBUTING.md ├── CHANGELOG.md └── LICENSE /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-dotnet-extensions-configuration/HEAD/icon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-dotnet-extensions-configuration/HEAD/logo.png -------------------------------------------------------------------------------- /public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-dotnet-extensions-configuration/HEAD/public.snk -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Dotnet Extensions Configuration 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /samples/Samples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /samples/Samples/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWS": { 3 | "Region": "us-east-1" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Debug", 8 | "System": "Information", 9 | "Microsoft": "Information" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: 💬 General Question 5 | url: https://github.com/aws/aws-dotnet-extensions-configuration/discussions/categories/q-a 6 | about: Please ask and answer questions as a discussion thread -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /samples/Samples/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Samples 4 | { 5 | public class Settings 6 | { 7 | public string StringValue { get; set; } 8 | public int IntegerValue { get; set; } 9 | public DateTime DateTimeValue { get; set; } 10 | public bool BooleanValue { get; set; } 11 | public TimeSpan TimeSpanValue { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.autover/autover.json: -------------------------------------------------------------------------------- 1 | { 2 | "Projects": [ 3 | { 4 | "Name": "Amazon.Extensions.Configuration.SystemsManager", 5 | "Path": "src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj" 6 | } 7 | ], 8 | "UseCommitsForChangelog": false, 9 | "UseSameVersionForAllProjects": false, 10 | "DefaultIncrementType": "Patch", 11 | "ChangeFilesDetermineIncrementType": true 12 | } -------------------------------------------------------------------------------- /samples/Samples/Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every quarter 8 | interval: "quarterly" 9 | labels: 10 | - "Release Not Needed" 11 | target-branch: "dev" 12 | # Group all github-actions updates into a single PR 13 | groups: 14 | all-github-actions: 15 | applies-to: "version-updates" 16 | patterns: 17 | - "*" 18 | -------------------------------------------------------------------------------- /src/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/handle-stale-discussions.yml: -------------------------------------------------------------------------------- 1 | name: HandleStaleDiscussions 2 | on: 3 | schedule: 4 | - cron: '0 */4 * * *' 5 | discussion_comment: 6 | types: [created] 7 | 8 | jobs: 9 | handle-stale-discussions: 10 | name: Handle stale discussions 11 | runs-on: ubuntu-latest 12 | permissions: 13 | discussions: write 14 | steps: 15 | - name: Stale discussions action 16 | uses: aws-github-ops/handle-stale-discussions@c0beee451a5d33d9c8f048a6d4e7c856b5422544 #v1.6.0 17 | env: 18 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📕 Documentation Issue" 3 | description: Report an issue in the API Reference documentation or Developer Guide 4 | title: "(short issue description)" 5 | labels: [documentation, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the issue 12 | description: A clear and concise description of the issue. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: links 18 | attributes: 19 | label: Links 20 | description: | 21 | Include links to affected documentation page(s). 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /samples/Samples/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace Samples.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class ValuesController : ControllerBase 9 | { 10 | private readonly IOptions _settings; 11 | 12 | public ValuesController(IOptions settings) 13 | { 14 | _settings = settings; 15 | } 16 | 17 | // GET api/values 18 | [HttpGet] 19 | public ActionResult Get() 20 | { 21 | return Ok(new 22 | { 23 | Settings = _settings.Value 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/closed-issue-message.yml: -------------------------------------------------------------------------------- 1 | name: Closed Issue Message 2 | on: 3 | issues: 4 | types: [closed] 5 | permissions: 6 | issues: write 7 | 8 | jobs: 9 | auto_comment: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: aws-actions/closed-issue-message@10aaf6366131b673a7c8b7742f8b3849f1d44f18 #v2 13 | with: 14 | # These inputs are both required 15 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 16 | message: | 17 | ### ⚠️COMMENT VISIBILITY WARNING⚠️ 18 | Comments on closed issues are hard for our team to see. 19 | If you need more assistance, please either tag a team member or open a new issue that references this one. 20 | If you wish to keep having a conversation with other community members under this issue feel free to do so. 21 | -------------------------------------------------------------------------------- /samples/Samples/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5766", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Samples": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/values", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/ISystemsManagerProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Threading.Tasks; 18 | 19 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 20 | { 21 | public interface ISystemsManagerProcessor 22 | { 23 | Task> GetDataAsync(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Integ/Amazon.Extensions.Configuration.SystemsManager.Integ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/semgrep-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | # Scan changed files in PRs, block on new issues only (existing issues ignored) 5 | pull_request: 6 | 7 | push: 8 | branches: ["dev", "main"] 9 | 10 | schedule: 11 | - cron: '23 20 * * 1' 12 | 13 | # Manually trigger the workflow 14 | workflow_dispatch: 15 | 16 | jobs: 17 | semgrep: 18 | name: Scan 19 | permissions: 20 | security-events: write 21 | runs-on: ubuntu-latest 22 | container: 23 | image: returntocorp/semgrep 24 | # Skip any PR created by dependabot to avoid permission issues 25 | if: (github.actor != 'dependabot[bot]') 26 | steps: 27 | # Fetch project source 28 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 29 | 30 | - run: semgrep ci --sarif > semgrep.sarif 31 | env: 32 | SEMGREP_RULES: >- # more at semgrep.dev/explore 33 | p/security-audit 34 | p/secrets 35 | p/owasp-top-ten 36 | 37 | - name: Upload SARIF file for GitHub Advanced Security Dashboard 38 | uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 #v3.30.5 39 | with: 40 | sarif_file: semgrep.sarif 41 | if: always() 42 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/Amazon.Extensions.Configuration.SystemsManager.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | false 7 | true 8 | ..\..\public.snk 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/issue-regression-labeler.yml: -------------------------------------------------------------------------------- 1 | # Apply potential regression label on issues 2 | name: issue-regression-label 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | jobs: 7 | add-regression-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - name: Fetch template body 13 | id: check_regression 14 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | TEMPLATE_BODY: ${{ github.event.issue.body }} 18 | with: 19 | script: | 20 | const regressionPattern = /\[x\] Select this option if this issue appears to be a regression\./i; 21 | const template = `${process.env.TEMPLATE_BODY}` 22 | const match = regressionPattern.test(template); 23 | core.setOutput('is_regression', match); 24 | - name: Manage regression label 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | if [ "${{ steps.check_regression.outputs.is_regression }}" == "true" ]; then 29 | gh issue edit ${{ github.event.issue.number }} --add-label "potential-regression" -R ${{ github.repository }} 30 | else 31 | gh issue edit ${{ github.event.issue.number }} --remove-label "potential-regression" -R ${{ github.repository }} 32 | fi 33 | -------------------------------------------------------------------------------- /.github/workflows/change-file-in-pr.yml: -------------------------------------------------------------------------------- 1 | name: Change File Included in PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, labeled] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | check-files-in-directory: 12 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'Release Not Needed') && !contains(github.event.pull_request.labels.*.name, 'Release PR') }} 13 | name: Change File Included in PR 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout PR code 18 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 19 | 20 | - name: Get List of Changed Files 21 | id: changed-files 22 | uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c #v45 23 | 24 | - name: Check for Change File(s) in .autover/changes/ 25 | run: | 26 | DIRECTORY=".autover/changes/" 27 | if echo "${{ steps.changed-files.outputs.all_changed_files }}" | grep -q "$DIRECTORY"; then 28 | echo "✅ One or more change files in '$DIRECTORY' are included in this PR." 29 | else 30 | echo "❌ No change files in '$DIRECTORY' are included in this PR." 31 | echo "Refer to the 'Adding a change file to your contribution branch' section of https://github.com/aws/aws-dotnet-extensions-configuration/blob/master/CONTRIBUTING.md" 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/SystemsManagerProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 4 | using Amazon.SimpleSystemsManagement.Model; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 9 | { 10 | public class SystemsManagerProcessorTests 11 | { 12 | [Theory] 13 | [InlineData("/aws/reference/secretsmanager/", true)] 14 | [InlineData("/not-sm-path/", false)] 15 | public void IsSecretsManagerPathTest(string path, bool expected) 16 | { 17 | Assert.Equal(expected, SystemsManagerProcessor.IsSecretsManagerPath(path)); 18 | } 19 | 20 | [Theory] 21 | [InlineData(null)] 22 | [InlineData("prefix")] 23 | public void AddPrefixTest(string prefix) 24 | { 25 | var data = new Dictionary { { "Key", "Value" } }; 26 | var output = SystemsManagerProcessor.AddPrefix(data, prefix); 27 | 28 | if (prefix == null) 29 | { 30 | Assert.Equal(data, output); 31 | } 32 | else 33 | { 34 | foreach (var item in output) 35 | { 36 | Assert.StartsWith($"{prefix}:", item.Key); 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # Visual Studio per-user settings data 3 | ###################################### 4 | *.suo 5 | *.user 6 | 7 | #################### 8 | # Build/Test folders 9 | #################### 10 | 11 | **/bin/ 12 | **/obj/ 13 | **/TestResults/ 14 | **/Temp/ 15 | **/buildlogs/ 16 | **/.vs/ 17 | 18 | **/*project.lock.json 19 | **/project.lock.json 20 | **/*.nuspec 21 | 22 | sdk/packages/** 23 | sdk/test/PCLTests/AndroidTests/packages/** 24 | sdk/.vs/** 25 | 26 | **/fxcop-report-*.xml 27 | 28 | docgenerator/.vs/** 29 | 30 | Deployment/** 31 | DocDeployment/** 32 | DocBuildAssemblies/** 33 | Include/** 34 | 35 | **/*.userprefs 36 | /sdk/test/CrossPlatformTests/CommonTests/Resources/settings.json 37 | packages 38 | /sdk/xamarin-components/**/*.xam 39 | /sdk/test/Unity/Unity3DTests/Assets/Resources/settings.json 40 | /sdk/test/Unity/Unity3DTests/Assets/Resources/awsconfig.xml 41 | /sdk/test/Unity/Unity3DTests/Unity3DTests.CSharp.csproj 42 | /sdk/test/Unity/Unity3DTests/Unity3DTests.CSharp.Editor.csproj 43 | /sdk/test/Unity/Unity3DTests/Unity3DTests.sln 44 | /sdk/test/Unity/Unity3DTests/Library 45 | /sdk/test/Unity/Unity3DTests/Assets/SDK/*.dll 46 | /sdk/test/Unity/Unity3DTests/Assets/SDK/*.pdb 47 | 48 | *.bak 49 | generator/.vs/** 50 | *.mdb 51 | *.apk 52 | *.meta 53 | /sdk/test/Unity/AWSAndroidHelper/.settings 54 | /sdk/test/Unity3DTests/Assets/SDK/*.mdb 55 | 56 | **/*.partial.sln 57 | **/*.partial.FxCop 58 | **/*.partial.csproj 59 | 60 | **/.DS_Store 61 | 62 | #JetBrains Rider user folders 63 | .idea/ -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/SystemsManagerExceptionContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Microsoft.Extensions.Configuration; 18 | 19 | namespace Amazon.Extensions.Configuration.SystemsManager 20 | { 21 | /// Contains information about a Systems Manager load exception. 22 | public class SystemsManagerExceptionContext 23 | { 24 | /// 25 | /// The that caused the exception. 26 | /// 27 | public IConfigurationProvider Provider { get; set; } 28 | 29 | /// The exception that occured in Load. 30 | public Exception Exception { get; set; } 31 | 32 | /// If true, the exception will not be rethrown. 33 | public bool Ignore { get; set; } 34 | 35 | /// If true, the exception was raised on a reload event 36 | public bool Reload { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigConfigurationSourceTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 17 | using Amazon.Extensions.NETCore.Setup; 18 | using Microsoft.Extensions.Configuration; 19 | using Xunit; 20 | 21 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 22 | { 23 | public class AppConfigConfigurationSourceTests 24 | { 25 | [Fact] 26 | public void BuildShouldReturnSystemsManagerConfigurationProvider() 27 | { 28 | var source = new AppConfigConfigurationSource 29 | { 30 | ApplicationId = "appId", 31 | EnvironmentId = "envId", 32 | ConfigProfileId = "profileId", 33 | AwsOptions = new AWSOptions() 34 | }; 35 | var builder = new ConfigurationBuilder(); 36 | 37 | var result = source.Build(builder); 38 | 39 | Assert.IsType(result); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/SystemsManagerConfigurationSourceTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using Amazon.Extensions.NETCore.Setup; 17 | using Microsoft.Extensions.Configuration; 18 | using Xunit; 19 | 20 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 21 | { 22 | public class SystemsManagerConfigurationSourceTests 23 | { 24 | [Fact] 25 | public void BuildSuccessTest() 26 | { 27 | var source = new SystemsManagerConfigurationSource 28 | { 29 | AwsOptions = new AWSOptions(), 30 | Path = "/temp/" 31 | }; 32 | var builder = new ConfigurationBuilder(); 33 | 34 | var result = source.Build(builder); 35 | 36 | Assert.IsType(result); 37 | } 38 | 39 | [Fact] 40 | public void FiltersInitializedTest() 41 | { 42 | var source = new SystemsManagerConfigurationSource(); 43 | 44 | Assert.NotNull(source.Filters); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/IParameterProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using Amazon.SimpleSystemsManagement.Model; 19 | using Microsoft.Extensions.Configuration; 20 | 21 | namespace Amazon.Extensions.Configuration.SystemsManager 22 | { 23 | /// 24 | /// Processor responsible for deciding if a should be included and processing the Key 25 | /// 26 | public interface IParameterProcessor 27 | { 28 | /// 29 | /// Process parameters from AWS Parameter Store into a dictionary for 30 | /// 31 | /// Enumeration of s to be processed 32 | /// Path used when retrieving the 33 | /// Configuration values for 34 | IDictionary ProcessParameters(IEnumerable parameters, string path); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/Samples.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32126.317 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Extensions.Configuration.SystemsManager", "..\src\Amazon.Extensions.Configuration.SystemsManager\Amazon.Extensions.Configuration.SystemsManager.csproj", "{CE965321-158B-47C8-BDA3-748F18745532}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{74F8A828-62EB-4338-A106-F28D91FE4B3A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CE965321-158B-47C8-BDA3-748F18745532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CE965321-158B-47C8-BDA3-748F18745532}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CE965321-158B-47C8-BDA3-748F18745532}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {CE965321-158B-47C8-BDA3-748F18745532}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {74F8A828-62EB-4338-A106-F28D91FE4B3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {74F8A828-62EB-4338-A106-F28D91FE4B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {74F8A828-62EB-4338-A106-F28D91FE4B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {74F8A828-62EB-4338-A106-F28D91FE4B3A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {5B66FEC1-55C5-47A4-8ADF-8A853A41AA42} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | 18 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 19 | { 20 | public static class DictionaryExtensions 21 | { 22 | public static bool EquivalentTo(this IDictionary first, IDictionary second) => EquivalentTo(first, second, null); 23 | 24 | public static bool EquivalentTo(this IDictionary first, IDictionary second, IEqualityComparer valueComparer) 25 | { 26 | if (first == second) return true; 27 | if (first == null || second == null) return false; 28 | if (first.Count != second.Count) return false; 29 | 30 | valueComparer = valueComparer ?? EqualityComparer.Default; 31 | 32 | foreach (var kvp in first) 33 | { 34 | if (!second.TryGetValue(kvp.Key, out var secondValue)) return false; 35 | if (!valueComparer.Equals(kvp.Value, secondValue)) return false; 36 | } 37 | return true; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/DictionaryExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 3 | using Xunit; 4 | 5 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 6 | { 7 | public class DictionaryExtensionsTests 8 | { 9 | [Theory] 10 | [MemberData(nameof(EquivalentToData))] 11 | public void TestEquivalentTo(IDictionary first, IDictionary second, bool equals) 12 | { 13 | Assert.Equal(equals, first.EquivalentTo(second)); 14 | } 15 | 16 | public static TheoryData, IDictionary, bool> EquivalentToData => new TheoryData, IDictionary, bool> 17 | { 18 | {new Dictionary(), new Dictionary(), true}, 19 | {new Dictionary(), null, false}, 20 | {new Dictionary(), new Dictionary {{"a", "a"}}, false}, 21 | {new Dictionary {{"a", "a"}}, new Dictionary {{"a", "a"}}, true}, 22 | {new Dictionary {{"a", "a"}}, new Dictionary {{"a", "a"}, {"b", "b"}}, false}, 23 | {new Dictionary {{"a", "a"}}, new Dictionary {{"b", "b"}}, false}, 24 | {new Dictionary {{"a", "a"}}, new Dictionary {{"a", "b"}}, false}, 25 | {new Dictionary {{"a", "a"}}, new Dictionary {{"b", "a"}}, false}, 26 | {new Dictionary {{"a", "a"},{"b", "b"}}, new Dictionary {{"b", "b"},{"a", "a"}}, true} 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/JsonOrStringParameterProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using Amazon.SimpleSystemsManagement; 6 | using Amazon.SimpleSystemsManagement.Model; 7 | using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil; 8 | 9 | namespace Amazon.Extensions.Configuration.SystemsManager 10 | { 11 | /// 12 | /// 13 | /// A processor that prioritizes JSON parameters but falls back to string parameters, 14 | /// in accordance with Systems Manager's suggested naming conventions 15 | /// 16 | public class JsonOrStringParameterProcessor : DefaultParameterProcessor 17 | { 18 | public override IDictionary ProcessParameters(IEnumerable parameters, string path) 19 | { 20 | var result = new Dictionary(StringComparer.OrdinalIgnoreCase); 21 | 22 | foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path))) 23 | { 24 | var keyPrefix = GetKey(parameter, path); 25 | var value = GetValue(parameter, path); 26 | 27 | if (parameter.Type == ParameterType.StringList) 28 | { 29 | ParseStringListParameter(keyPrefix, value, result); 30 | continue; 31 | } 32 | 33 | try 34 | { 35 | ParseJsonParameter(keyPrefix, value, result); 36 | } 37 | catch (JsonException) 38 | { 39 | ParseStringParameter(keyPrefix, value, result); 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Integ/ConfigurationBuilderIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Microsoft.Extensions.Configuration; 18 | using Xunit; 19 | 20 | namespace Amazon.Extensions.Configuration.SystemsManager.Integ 21 | { 22 | public class ConfigurationBuilderIntegrationTests : IClassFixture 23 | { 24 | private IntegTestFixture fixture; 25 | 26 | public ConfigurationBuilderIntegrationTests(IntegTestFixture fixture) 27 | { 28 | this.fixture = fixture; 29 | } 30 | 31 | [Fact] 32 | public void TestConfigurationBuilder() 33 | { 34 | var configurationBuilder = new ConfigurationBuilder(); 35 | configurationBuilder.AddSystemsManager(IntegTestFixture.ParameterPrefix, fixture.AWSOptions); 36 | var configurations = configurationBuilder.Build(); 37 | 38 | Assert.All(fixture.TestData, (pair) => { 39 | Assert.Equal(pair.Value, configurations[pair.Key]); 40 | }); 41 | 42 | // Since there is no reload going on this should return back immediately. 43 | configurations.WaitForSystemsManagerReloadToComplete(TimeSpan.FromHours(1)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/aws-ci.yml: -------------------------------------------------------------------------------- 1 | name: AWS CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | - dev 9 | - 'feature/**' 10 | 11 | permissions: 12 | id-token: write 13 | 14 | jobs: 15 | run-ci: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Configure AWS Credentials 19 | uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 20 | with: 21 | role-to-assume: ${{ secrets.CI_MAIN_TESTING_ACCOUNT_ROLE_ARN }} 22 | role-duration-seconds: 7200 23 | aws-region: us-west-2 24 | - name: Invoke Load Balancer Lambda 25 | id: lambda 26 | shell: pwsh 27 | run: | 28 | aws lambda invoke response.json --function-name "${{ secrets.CI_TESTING_LOAD_BALANCER_LAMBDA_NAME }}" --cli-binary-format raw-in-base64-out --payload '{"Roles": "${{ secrets.CI_TEST_RUNNER_ACCOUNT_ROLES }}", "ProjectName": "${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }}", "Branch": "${{ github.sha }}"}' 29 | $roleArn=$(cat ./response.json) 30 | "roleArn=$($roleArn -replace '"', '')" >> $env:GITHUB_OUTPUT 31 | - name: Configure Test Runner Credentials 32 | uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 33 | with: 34 | role-to-assume: ${{ steps.lambda.outputs.roleArn }} 35 | role-duration-seconds: 7200 36 | aws-region: us-west-2 37 | - name: Run Tests on AWS 38 | id: codebuild 39 | uses: aws-actions/aws-codebuild-run-build@4d15a47425739ac2296ba5e7eee3bdd4bfbdd767 #v1.0.18 40 | with: 41 | project-name: ${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }} 42 | - name: CodeBuild Link 43 | shell: pwsh 44 | run: | 45 | $buildId = "${{ steps.codebuild.outputs.aws-build-id }}" 46 | echo $buildId 47 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/AwsOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using Amazon.Extensions.NETCore.Setup; 17 | using Microsoft.Extensions.Configuration; 18 | 19 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 20 | { 21 | public static class AwsOptionsProvider 22 | { 23 | private const string AwsOptionsConfigurationKey = "AWS_CONFIGBUILDER_AWSOPTIONS"; 24 | 25 | public static AWSOptions GetAwsOptions(IConfigurationBuilder builder) 26 | { 27 | if (builder.Properties.TryGetValue(AwsOptionsConfigurationKey, out var value) && value is AWSOptions existingOptions) 28 | { 29 | return existingOptions; 30 | } 31 | 32 | var config = builder.Build(); 33 | var newOptions = config.GetAWSOptions(); 34 | 35 | if (builder.Properties.ContainsKey(AwsOptionsConfigurationKey)) 36 | { 37 | builder.Properties[AwsOptionsConfigurationKey] = newOptions; 38 | } 39 | else 40 | { 41 | builder.Properties.Add(AwsOptionsConfigurationKey, newOptions); 42 | } 43 | 44 | return newOptions; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/JsonParameterProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | using Amazon.SimpleSystemsManagement.Model; 20 | using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil; 21 | 22 | namespace Amazon.Extensions.Configuration.SystemsManager 23 | { 24 | /// 25 | /// 26 | /// A processor specifically designed for handling JSON parameters, 27 | /// following Systems Manager's recommended naming conventions 28 | /// 29 | public class JsonParameterProcessor : DefaultParameterProcessor 30 | { 31 | public override IDictionary ProcessParameters(IEnumerable parameters, string path) 32 | { 33 | var result = new Dictionary(StringComparer.OrdinalIgnoreCase); 34 | 35 | foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path))) 36 | { 37 | var keyPrefix = GetKey(parameter, path); 38 | var value = GetValue(parameter, path); 39 | 40 | ParseJsonParameter(keyPrefix, value, result); 41 | } 42 | 43 | return result; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/ISystemsManagerConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Microsoft.Extensions.Configuration; 18 | 19 | namespace Amazon.Extensions.Configuration.SystemsManager 20 | { 21 | /// 22 | /// 23 | /// Represents AWS Systems Manager variables as an . 24 | /// 25 | public interface ISystemsManagerConfigurationSource : IConfigurationSource 26 | { 27 | /// 28 | /// Determines if loading configuration data from AWS Systems Manager Parameter Store is optional. 29 | /// 30 | #pragma warning disable CA1716 // Identifiers should not match keywords: This would be a breaking change to change this name now. 31 | bool Optional { get; set; } 32 | #pragma warning restore CA1716 // Identifiers should not match keywords 33 | 34 | /// 35 | /// Parameters will be reloaded from the AWS Systems Manager Parameter Store after the specified time frame 36 | /// 37 | TimeSpan? ReloadAfter { get; set; } 38 | 39 | /// 40 | /// Will be called if an uncaught exception occurs in .Load. 41 | /// 42 | Action OnLoadException { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/stale_issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | cleanup: 14 | runs-on: ubuntu-latest 15 | name: Stale issue job 16 | steps: 17 | - uses: aws-actions/stale-issue-cleanup@5650b49bcd757a078f6ca06c373d7807b773f9bc #v7.1.0 18 | with: 19 | # Setting messages to an empty string will cause the automation to skip 20 | # that category 21 | ancient-issue-message: We have noticed this issue has not received attention in 1 year. We will close this issue for now. If you think this is in error, please feel free to comment and reopen the issue. 22 | stale-issue-message: This issue has not received a response in 5 days. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled. 23 | 24 | # These labels are required 25 | stale-issue-label: closing-soon 26 | exempt-issue-labels: no-autoclose 27 | stale-pr-label: no-pr-activity 28 | exempt-pr-labels: awaiting-approval 29 | response-requested-label: response-requested 30 | 31 | # Don't set closed-for-staleness label to skip closing very old issues 32 | # regardless of label 33 | closed-for-staleness-label: closed-for-staleness 34 | 35 | # Issue timing 36 | days-before-stale: 5 37 | days-before-close: 2 38 | days-before-ancient: 36500 39 | 40 | # If you don't want to mark a issue as being ancient based on a 41 | # threshold of "upvotes", you can set this here. An "upvote" is 42 | # the total number of +1, heart, hooray, and rocket reactions 43 | # on an issue. 44 | minimum-upvotes-to-exempt: 10 45 | 46 | repo-token: ${{ secrets.GITHUB_TOKEN }} 47 | #loglevel: DEBUG 48 | # Set dry-run to true to not perform label or close actions. 49 | #dry-run: true 50 | 51 | -------------------------------------------------------------------------------- /samples/Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.Extensions.NETCore.Setup; 3 | using Amazon.SimpleSystemsManagement; 4 | using Amazon.SimpleSystemsManagement.Model; 5 | using Samples; 6 | 7 | //populates some sample data to be used by this example project 8 | await PopulateSampleDataForThisProject().ConfigureAwait(false); 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | builder.Configuration.AddSystemsManager($"/dotnet-aws-samples/systems-manager-sample/"); 13 | 14 | builder.Services.Configure(builder.Configuration.GetSection($"common:settings")); 15 | 16 | // Add services to the container. 17 | builder.Services.AddControllers(); 18 | 19 | var app = builder.Build(); 20 | 21 | app.UseHttpsRedirection(); 22 | app.UseAuthorization(); 23 | app.MapControllers(); 24 | 25 | app.Run(); 26 | 27 | 28 | static async Task PopulateSampleDataForThisProject() 29 | { 30 | var awsOptions = new AWSOptions { Region = RegionEndpoint.USEast1 }; 31 | 32 | var root = $"/dotnet-aws-samples/systems-manager-sample/common"; 33 | var parameters = new[] 34 | { 35 | new {Name = "StringValue", Value = "string-value"}, 36 | new {Name = "IntegerValue", Value = "10"}, 37 | new {Name = "DateTimeValue", Value = "2000-01-01"}, 38 | new {Name = "BooleanValue", Value = "True"}, 39 | new {Name = "TimeSpanValue", Value = "00:05:00"}, 40 | }; 41 | 42 | using (var client = awsOptions.CreateServiceClient()) 43 | { 44 | var result = await client.GetParametersByPathAsync(new GetParametersByPathRequest { Path = root, Recursive = true }).ConfigureAwait(false); 45 | if (result.Parameters.Count == parameters.Length) return; 46 | 47 | foreach (var parameter in parameters) 48 | { 49 | var name = $"{root}/settings/{parameter.Name}"; 50 | await client.PutParameterAsync(new PutParameterRequest { Name = name, Value = parameter.Value, Type = ParameterType.String, Overwrite = true }).ConfigureAwait(false); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/ServiceClientAppender.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System.Reflection; 17 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 18 | using Amazon.Runtime; 19 | 20 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 21 | { 22 | public static class ServiceClientAppender 23 | { 24 | private static readonly string AssemblyVersion = typeof(AppConfigProcessor).GetTypeInfo().Assembly.GetName().Version.ToString(); 25 | private static readonly string UserAgentSuffix = $"lib/SSMConfigProvider#{AssemblyVersion}"; 26 | 27 | public static void ServiceClientBeforeRequestEvent(object sender, RequestEventArgs e) 28 | { 29 | WebServiceRequestEventArgs args = e as WebServiceRequestEventArgs; 30 | if (args != null && args.Request is Amazon.Runtime.Internal.IAmazonWebServiceRequest internalRequest && 31 | #if NET8_0_OR_GREATER 32 | !internalRequest.UserAgentDetails.GetCustomUserAgentComponents().Contains(UserAgentSuffix, System.StringComparison.InvariantCulture) 33 | #else 34 | !internalRequest.UserAgentDetails.GetCustomUserAgentComponents().Contains(UserAgentSuffix) 35 | #endif 36 | ) 37 | { 38 | internalRequest.UserAgentDetails.AddUserAgentComponent(UserAgentSuffix); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## Testing 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate) 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 22 | 23 | ## Checklist 24 | 25 | 26 | - [ ] My code follows the code style of this project 27 | - [ ] My change requires a change to the documentation 28 | - [ ] I have updated the documentation accordingly 29 | - [ ] I have read the **README** document 30 | - [ ] I have added tests to cover my changes 31 | - [ ] All new and existing tests passed 32 | 33 | ## License 34 | 35 | 36 | 37 | - [ ] I confirm that this pull request can be released under the Apache 2 license 38 | 39 | [issues]: https://github.com/aws/aws-dotnet-extensions-configuration/issues 40 | [license]: http://aws.amazon.com/apache2.0/ 41 | [cla]: http://en.wikipedia.org/wiki/Contributor_License_Agreement 42 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/DuplicateParameterException.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | 18 | namespace Amazon.Extensions.Configuration.SystemsManager 19 | { 20 | /// 21 | /// This exception is thrown when duplicate Systems Manager parameter keys (case insensitive) are 22 | /// detected irrespective of whether the parameter is optional or not. 23 | /// 24 | /// For example, keys /some-path/some-key and /some-path/SOME-KEY are considered as duplicates. 25 | /// 26 | /// 27 | public class DuplicateParameterException : ArgumentException 28 | { 29 | /// 30 | /// Initializes a new instance of DuplicateParameterException. 31 | /// 32 | public DuplicateParameterException() 33 | { 34 | } 35 | 36 | /// 37 | /// Initializes a new instance of DuplicateParameterException. 38 | /// 39 | /// The message that describes the error. 40 | public DuplicateParameterException(string message) 41 | : base(message) 42 | { 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of DuplicateParameterException. 47 | /// 48 | /// The message that describes the error. 49 | /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. 50 | public DuplicateParameterException(string message, Exception innerException) 51 | : base(message, innerException) 52 | { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Microsoft.Extensions.Configuration; 18 | 19 | namespace Amazon.Extensions.Configuration.SystemsManager 20 | { 21 | /// 22 | /// This extension is an a different namespace to avoid misuse of this method which should only be called when being used from AWS Lambda. 23 | /// 24 | public static class ConfigurationExtensions 25 | { 26 | /// 27 | /// This method blocks while any SystemsManagerConfigurationProvider added to IConfiguration are 28 | /// currently reloading the parameters from Parameter Store. 29 | /// 30 | /// This is generally only needed when the provider is being called from a AWS Lambda function. Without this call 31 | /// in a AWS Lambda environment there is a potential of the background thread doing the refresh never running successfully. 32 | /// This can happen because the AWS Lambda compute environment is frozen after the current AWS Lambda event is complete. 33 | /// 34 | /// 35 | /// Maximum time to wait for reload to be completed 36 | public static void WaitForSystemsManagerReloadToComplete(this IConfiguration configuration, TimeSpan timeout) 37 | { 38 | if (configuration is ConfigurationRoot configRoot) 39 | { 40 | foreach (var provider in configRoot.Providers) 41 | { 42 | if (provider is SystemsManagerConfigurationProvider ssmProvider) 43 | { 44 | ssmProvider.WaitForReloadToComplete(timeout); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Suggest an idea for this project 4 | title: "(short issue description)" 5 | labels: [feature-request, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the feature 12 | description: A clear and concise description of the feature you are proposing. 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: use-case 17 | attributes: 18 | label: Use Case 19 | description: | 20 | Why do you need this feature? For example: "I'm always frustrated when..." 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: Proposed Solution 27 | description: | 28 | Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: other 33 | attributes: 34 | label: Other Information 35 | description: | 36 | Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. 37 | validations: 38 | required: false 39 | - type: checkboxes 40 | id: ack 41 | attributes: 42 | label: Acknowledgements 43 | options: 44 | - label: I may be able to implement this feature request 45 | required: false 46 | - label: This feature might incur a breaking change 47 | required: false 48 | 49 | - type: textarea 50 | id: dotnet-sdk-version 51 | attributes: 52 | label: AWS .NET SDK and/or Package version used 53 | description: NuGet Packages used 54 | placeholder: AWSSDK.S3 3.7.8.13 55 | validations: 56 | required: true 57 | 58 | - type: input 59 | id: platform-used 60 | attributes: 61 | label: Targeted .NET Platform 62 | description: "Example: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc." 63 | placeholder: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc. 64 | validations: 65 | required: true 66 | 67 | - type: input 68 | id: operating-system 69 | attributes: 70 | label: Operating System and version 71 | description: "Example: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc." 72 | placeholder: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc. 73 | validations: 74 | required: true -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/SystemsManagerConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using Amazon.Extensions.NETCore.Setup; 19 | using Amazon.SimpleSystemsManagement.Model; 20 | using Microsoft.Extensions.Configuration; 21 | 22 | namespace Amazon.Extensions.Configuration.SystemsManager 23 | { 24 | /// 25 | /// 26 | /// Represents AWS Systems Manager Parameter Store variables as an . 27 | /// 28 | public class SystemsManagerConfigurationSource : ISystemsManagerConfigurationSource 29 | { 30 | public SystemsManagerConfigurationSource() 31 | { 32 | Filters = new List(); 33 | } 34 | 35 | /// 36 | /// A Path used to filter parameters. 37 | /// 38 | public string Path { get; set; } 39 | 40 | /// 41 | /// used to create an AWS Systems Manager Client />. 42 | /// 43 | public AWSOptions AwsOptions { get; set; } 44 | 45 | /// 46 | public bool Optional { get; set; } 47 | 48 | /// 49 | public TimeSpan? ReloadAfter { get; set; } 50 | 51 | /// 52 | /// Prepends the supplied Prefix to all result keys 53 | /// 54 | public string Prefix { get; set; } 55 | 56 | /// 57 | public Action OnLoadException { get; set; } 58 | 59 | /// 60 | /// Implementation of used to process results. Defaults to . 61 | /// 62 | public IParameterProcessor ParameterProcessor { get; set; } 63 | 64 | /// 65 | /// Filters to limit the request results. 66 | /// You can't filter using the parameter name. 67 | /// 68 | public List Filters { get; } 69 | 70 | /// 71 | public IConfigurationProvider Build(IConfigurationBuilder builder) 72 | { 73 | return new SystemsManagerConfigurationProvider(this); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/JsonParameterProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | using Amazon.SimpleSystemsManagement.Model; 4 | using Xunit; 5 | 6 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 7 | { 8 | public class JsonParameterProcessorTests 9 | { 10 | private readonly IParameterProcessor _parameterProcessor = new JsonParameterProcessor(); 11 | 12 | [Fact] 13 | public void ProcessParametersTest() 14 | { 15 | var parameters = new List 16 | { 17 | new Parameter {Name = "/p1", Value = "{\"p1\": \"p1\"}"}, 18 | new Parameter {Name = "p2", Value = "{\"p2\": \"p2\"}"}, 19 | new Parameter {Name = "/p1/p3", Value = "{\"p3key\": \"p3value\"}"}, 20 | new Parameter {Name = "/p4", Value = "{\"p4key\": { \"p5key\": \"p5value\" } }"}, 21 | new Parameter {Name = "/p6", Value = "{\"p6key\": { \"p7key\": { \"p8key\": \"p8value\" } } }"}, 22 | new Parameter {Name = "/ObjectA", Value = "{\"Bucket\": \"arnA\"}"}, 23 | new Parameter {Name = "/ObjectB", Value = "{\"Bucket\": \"arnB\"}"}, 24 | new Parameter {Name = "/", Value = "{\"testParam\": \"testValue\"}"} 25 | }; 26 | var expected = new Dictionary() { 27 | { "p1:p1", "p1" }, 28 | { "p2:p2", "p2" }, 29 | { "p1:p3:p3key", "p3value" }, 30 | { "p4:p4key:p5key", "p5value" }, 31 | { "p6:p6key:p7key:p8key", "p8value" }, 32 | { "ObjectA:Bucket", "arnA" }, 33 | { "ObjectB:Bucket", "arnB" }, 34 | { "testParam", "testValue" } 35 | }; 36 | 37 | const string path = "/"; 38 | 39 | var data = _parameterProcessor.ProcessParameters(parameters, path); 40 | 41 | Assert.All(expected, item => Assert.Equal(item.Value, data[item.Key])); 42 | } 43 | 44 | [Fact] 45 | public void DuplicateParametersTest() 46 | { 47 | var parameters = new List 48 | { 49 | new Parameter {Name = "/p1", Value = "{\"p1\": \"p1\"}"}, 50 | new Parameter {Name = "p2", Value = "{\"p2\": \"p2\"}"}, 51 | new Parameter {Name = "p1", Value = "{\"P1\": \"p1-1\"}"}, 52 | }; 53 | 54 | const string path = "/"; 55 | Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, path)); 56 | } 57 | 58 | [Fact] 59 | public void InvalidJsonTest() 60 | { 61 | var parameters = new List 62 | { 63 | new Parameter {Name = "/p1", Value = "String value"}, 64 | }; 65 | 66 | const string path = "/"; 67 | Assert.ThrowsAny(() => _parameterProcessor.ProcessParameters(parameters, path)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/DefaultParameterProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | using Amazon.SimpleSystemsManagement; 20 | using Amazon.SimpleSystemsManagement.Model; 21 | using Microsoft.Extensions.Configuration; 22 | using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil; 23 | 24 | namespace Amazon.Extensions.Configuration.SystemsManager 25 | { 26 | /// 27 | /// 28 | /// Default parameter processor based on Systems Manager's suggested naming convention 29 | /// 30 | public class DefaultParameterProcessor : IParameterProcessor 31 | { 32 | /// 33 | public static readonly string KeyDelimiter = ConfigurationPath.KeyDelimiter; 34 | 35 | public virtual bool IncludeParameter(Parameter parameter, string path) => true; 36 | 37 | /// Get the extra prefix if the path is subset of parameter name. 38 | public virtual string GetKey(Parameter parameter, string path) 39 | { 40 | var name = parameter.Name.StartsWith(path, StringComparison.OrdinalIgnoreCase) 41 | ? parameter.Name.Substring(path.Length) 42 | : parameter.Name; 43 | #if NET8_0_OR_GREATER 44 | return name.TrimStart('/').Replace("/", KeyDelimiter, StringComparison.InvariantCulture); 45 | #else 46 | return name.TrimStart('/').Replace("/", KeyDelimiter); 47 | #endif 48 | } 49 | 50 | public virtual string GetValue(Parameter parameter, string path) => parameter.Value; 51 | 52 | public virtual IDictionary ProcessParameters(IEnumerable parameters, string path) 53 | { 54 | var result = new Dictionary(StringComparer.OrdinalIgnoreCase); 55 | 56 | foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path))) 57 | { 58 | var keyPrefix = GetKey(parameter, path); 59 | var value = GetValue(parameter, path); 60 | 61 | if (parameter.Type == ParameterType.StringList) 62 | { 63 | ParseStringListParameter(keyPrefix, value, result); 64 | continue; 65 | } 66 | 67 | ParseStringParameter(keyPrefix, value, result); 68 | } 69 | 70 | return result; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Net.Http; 18 | using Amazon.Extensions.NETCore.Setup; 19 | using Microsoft.Extensions.Configuration; 20 | 21 | namespace Amazon.Extensions.Configuration.SystemsManager.AppConfig 22 | { 23 | /// 24 | /// 25 | /// Represents AWS Systems Manager AppConfig variables as an . 26 | /// 27 | public class AppConfigConfigurationSource : ISystemsManagerConfigurationSource 28 | { 29 | /// 30 | /// AppConfig Application Id. 31 | /// 32 | public string ApplicationId { get; set; } 33 | 34 | /// 35 | /// AppConfig Environment Id. 36 | /// 37 | public string EnvironmentId { get; set; } 38 | 39 | /// 40 | /// AppConfig Configuration Profile Id. 41 | /// 42 | public string ConfigProfileId { get; set; } 43 | 44 | /// 45 | /// used to create an AWS Systems Manager Client />. 46 | /// 47 | public AWSOptions AwsOptions { get; set; } 48 | 49 | /// 50 | public bool Optional { get; set; } 51 | 52 | /// 53 | public TimeSpan? ReloadAfter { get; set; } 54 | 55 | /// 56 | /// Indicates to use configured lambda extension HTTP client to retrieve AppConfig data. 57 | /// 58 | internal bool UseLambdaExtension { get; set; } 59 | 60 | /// 61 | /// Used only when integrating with the AWS AppConfig Lambda extension. 62 | /// 63 | /// This property allows customizing the HttpClient connecting to the AWS AppConfig Lambda extension. This is useful 64 | /// to instrument the HttpClient for telemetry. For example adding the Amazon.XRay.Recorder.Handlers.System.Net.HttpClientXRayTracingHandler handle for AWS X-Ray tracing. 65 | /// 66 | public HttpClient CustomHttpClientForLambdaExtension { get; set; } 67 | 68 | /// 69 | public Action OnLoadException { get; set; } 70 | 71 | /// 72 | public IConfigurationProvider Build(IConfigurationBuilder builder) 73 | { 74 | return new SystemsManagerConfigurationProvider(this); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Amazon.Extensions.Configuration.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32126.317 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00A2C0BE-9179-44B3-B30E-7E64C319AE24}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{31D7A0E9-FAC1-4EC5-8F77-CA21B8A4F9F0}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Extensions.Configuration.SystemsManager", "src\Amazon.Extensions.Configuration.SystemsManager\Amazon.Extensions.Configuration.SystemsManager.csproj", "{EC9A4865-6217-4E8D-98EC-12A0F2061F04}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Extensions.Configuration.SystemsManager.Tests", "test\Amazon.Extensions.Configuration.SystemsManager.Tests\Amazon.Extensions.Configuration.SystemsManager.Tests.csproj", "{E8C9EC1F-E466-4100-B55C-569127918CC3}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Extensions.Configuration.SystemsManager.Integ", "test\Amazon.Extensions.Configuration.SystemsManager.Integ\Amazon.Extensions.Configuration.SystemsManager.Integ.csproj", "{60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{30F7DC8F-EF72-4619-8C48-2EC26B91A530}" 17 | ProjectSection(SolutionItems) = preProject 18 | README.md = README.md 19 | RELEASE.CHANGELOG.md = RELEASE.CHANGELOG.md 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {EC9A4865-6217-4E8D-98EC-12A0F2061F04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {EC9A4865-6217-4E8D-98EC-12A0F2061F04}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {EC9A4865-6217-4E8D-98EC-12A0F2061F04}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {EC9A4865-6217-4E8D-98EC-12A0F2061F04}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E8C9EC1F-E466-4100-B55C-569127918CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E8C9EC1F-E466-4100-B55C-569127918CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E8C9EC1F-E466-4100-B55C-569127918CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E8C9EC1F-E466-4100-B55C-569127918CC3}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {EC9A4865-6217-4E8D-98EC-12A0F2061F04} = {00A2C0BE-9179-44B3-B30E-7E64C319AE24} 46 | {E8C9EC1F-E466-4100-B55C-569127918CC3} = {31D7A0E9-FAC1-4EC5-8F77-CA21B8A4F9F0} 47 | {60C9F6C0-C13E-45AA-BC1F-C7E8BEA2E022} = {31D7A0E9-FAC1-4EC5-8F77-CA21B8A4F9F0} 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {6502AC14-8D11-4D72-88EF-154344F3B98D} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | description: Report a bug 4 | title: "(short issue description)" 5 | labels: [bug, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the bug 12 | description: What is the problem? A clear and concise description of the bug. 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | id: regression 17 | attributes: 18 | label: Regression Issue 19 | description: What is a regression? If it worked in a previous version but doesn't in the latest version, it's considered a regression. In this case, please provide specific version number in the report. 20 | options: 21 | - label: Select this option if this issue appears to be a regression. 22 | required: false 23 | - type: textarea 24 | id: expected 25 | attributes: 26 | label: Expected Behavior 27 | description: | 28 | What did you expect to happen? 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: current 33 | attributes: 34 | label: Current Behavior 35 | description: | 36 | What actually happened? 37 | 38 | Please include full errors, uncaught exceptions, stack traces, and relevant logs. 39 | If service responses are relevant, please include wire logs. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: reproduction 44 | attributes: 45 | label: Reproduction Steps 46 | description: | 47 | Provide a self-contained, concise snippet of code that can be used to reproduce the issue. 48 | For more complex issues provide a repo with the smallest sample that reproduces the bug. 49 | 50 | Avoid including business logic or unrelated code, it makes diagnosis more difficult. 51 | The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: solution 56 | attributes: 57 | label: Possible Solution 58 | description: | 59 | Suggest a fix/reason for the bug 60 | validations: 61 | required: false 62 | - type: textarea 63 | id: context 64 | attributes: 65 | label: Additional Information/Context 66 | description: | 67 | Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. 68 | validations: 69 | required: false 70 | 71 | - type: textarea 72 | id: dotnet-sdk-version 73 | attributes: 74 | label: AWS .NET SDK and/or Package version used 75 | description: NuGet Packages used 76 | placeholder: AWSSDK.S3 3.7.8.13 77 | validations: 78 | required: true 79 | 80 | - type: input 81 | id: platform-used 82 | attributes: 83 | label: Targeted .NET Platform 84 | description: "Example: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc." 85 | placeholder: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc. 86 | validations: 87 | required: true 88 | 89 | - type: input 90 | id: operating-system 91 | attributes: 92 | label: Operating System and version 93 | description: "Example: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc." 94 | placeholder: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc. 95 | validations: 96 | required: true 97 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netstandard2.0;net8.0 6 | Amazon.Extensions.Configuration.SystemsManager 7 | Amazon.Extensions.Configuration.SystemsManager 8 | Library 9 | true 10 | Amazon.Extensions.Configuration.SystemsManager 11 | .NET Configuration Extensions for AWS Systems Manager 12 | Amazon.Extensions.Configuration.SystemsManager 13 | .NET Configuration Extensions for AWS Systems Manager 14 | Amazon Web Services 15 | AWS;Amazon;aws-sdk-v4;SimpleSystemsManagement;configuration 16 | https://github.com/aws/aws-dotnet-extensions-configuration/ 17 | LICENSE 18 | icon.png 19 | https://github.com/aws/aws-dotnet-extensions-configuration/ 20 | Amazon Web Services 21 | true 22 | ../ruleset.xml 23 | 24 | true 25 | true 26 | true 27 | snupkg 28 | 29 | 7.0.1 30 | 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | true 47 | ..\..\public.snk 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | all 62 | 63 | 64 | all 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 18 | using Microsoft.Extensions.Configuration; 19 | using Moq; 20 | using Xunit; 21 | 22 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 23 | { 24 | public class AppConfigExtensionsTests 25 | { 26 | [Fact] 27 | public void AddAppConfigWithProperInputShouldReturnProperConfigurationBuilder() 28 | { 29 | var expectedBuilder = new ConfigurationBuilder(); 30 | expectedBuilder.Sources.Add(new Mock().Object); 31 | const string applicationId = "appId"; 32 | const string environmentId = "envId"; 33 | const string configProfileId = "profId"; 34 | 35 | var builder = new ConfigurationBuilder(); 36 | builder.AddAppConfig(applicationId, environmentId, configProfileId); 37 | 38 | Assert.Contains( 39 | builder.Sources, 40 | source => source is AppConfigConfigurationSource configurationSource 41 | && configurationSource.ApplicationId == applicationId 42 | && configurationSource.EnvironmentId == environmentId 43 | && configurationSource.ConfigProfileId == configProfileId 44 | ); 45 | } 46 | 47 | [Fact] 48 | public void AddAppConfigWithoutAwsOptionsShouldThrowException() 49 | { 50 | var builder = new ConfigurationBuilder(); 51 | Func func = () => builder.AddAppConfig("appId", "envId", "profileId", awsOptions: null); 52 | 53 | var ex = Assert.Throws(func); 54 | Assert.Contains("awsOptions", ex.Message); 55 | } 56 | 57 | [Fact] 58 | public void AddAppConfigWithoutApplicationIdShouldThrowException() 59 | { 60 | var builder = new ConfigurationBuilder(); 61 | Func func = () => builder.AddAppConfig(null, "envId", "profileId"); 62 | 63 | var ex = Assert.Throws(func); 64 | Assert.Contains("applicationId", ex.Message); 65 | } 66 | 67 | [Fact] 68 | public void AddAppConfigWithoutEnvironmentIdShouldThrowException() 69 | { 70 | var builder = new ConfigurationBuilder(); 71 | Func func = () => builder.AddAppConfig("appId", null, "profileId"); 72 | 73 | var ex = Assert.Throws(func); 74 | Assert.Contains("environmentId", ex.Message); 75 | } 76 | 77 | [Fact] 78 | public void AddAppConfigWithoutProfileIdShouldThrowException() 79 | { 80 | var builder = new ConfigurationBuilder(); 81 | Func func = () => builder.AddAppConfig("appId", "envId", null); 82 | 83 | var ex = Assert.Throws(func); 84 | Assert.Contains("configProfileId", ex.Message); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigForLambdaExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Reflection; 18 | using System.Linq; 19 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 20 | using Microsoft.Extensions.Configuration; 21 | using Moq; 22 | using Xunit; 23 | 24 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 25 | { 26 | public class AppConfigForLambdaExtensionsTests 27 | { 28 | [Fact] 29 | public void AddAppConfigForLambdaWithProperInputShouldReturnProperConfigurationBuilder() 30 | { 31 | var expectedBuilder = new ConfigurationBuilder(); 32 | expectedBuilder.Sources.Add(new Mock().Object); 33 | const string applicationId = "appId"; 34 | const string environmentId = "envId"; 35 | const string configProfileId = "profId"; 36 | 37 | var builder = new ConfigurationBuilder(); 38 | builder.AddAppConfigUsingLambdaExtension(applicationId, environmentId, configProfileId); 39 | 40 | var configurationSource = builder.Sources.FirstOrDefault(source => source is AppConfigConfigurationSource) as AppConfigConfigurationSource; 41 | Assert.NotNull(configurationSource); 42 | Assert.Equal(applicationId, configurationSource.ApplicationId); 43 | Assert.Equal(environmentId, configurationSource.EnvironmentId); 44 | Assert.Equal(configProfileId, configurationSource.ConfigProfileId); 45 | 46 | var property = typeof(AppConfigConfigurationSource).GetProperty("UseLambdaExtension", BindingFlags.Instance | BindingFlags.NonPublic); 47 | Assert.NotNull(property); 48 | Assert.True((bool)property.GetValue(configurationSource)); 49 | } 50 | 51 | [Fact] 52 | public void AddAppConfigForLambdaWithoutApplicationIdShouldThrowException() 53 | { 54 | var builder = new ConfigurationBuilder(); 55 | Func func = () => builder.AddAppConfigUsingLambdaExtension(null, "envId", "profileId"); 56 | 57 | var ex = Assert.Throws(func); 58 | Assert.Contains("applicationId", ex.Message); 59 | } 60 | 61 | [Fact] 62 | public void AddAppConfigForLambdaWithoutEnvironmentIdShouldThrowException() 63 | { 64 | var builder = new ConfigurationBuilder(); 65 | Func func = () => builder.AddAppConfigUsingLambdaExtension("appId", null, "profileId"); 66 | 67 | var ex = Assert.Throws(func); 68 | Assert.Contains("environmentId", ex.Message); 69 | } 70 | 71 | [Fact] 72 | public void AddAppConfigForLambdaWithoutProfileIdShouldThrowException() 73 | { 74 | var builder = new ConfigurationBuilder(); 75 | Func func = () => builder.AddAppConfigUsingLambdaExtension("appId", "envId", null); 76 | 77 | var ex = Assert.Throws(func); 78 | Assert.Contains("configProfileId", ex.Message); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/Utils/ParameterProcessorUtilTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | using Amazon.Extensions.Configuration.SystemsManager.Utils; 4 | using Xunit; 5 | 6 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests.Utils 7 | { 8 | public class ParameterProcessorUtilTests 9 | { 10 | [Fact] 11 | public void ParseJsonParameterSuccessfully() 12 | { 13 | var result = new Dictionary(); 14 | var value = "{\"key\": \"value\"}"; 15 | var keyPrefix = "prefix"; 16 | 17 | ParameterProcessorUtil.ParseJsonParameter(keyPrefix, value, result); 18 | 19 | Assert.Single(result); 20 | Assert.Contains("prefix:key", result.Keys); 21 | Assert.Equal("value", result["prefix:key"]); 22 | } 23 | 24 | [Fact] 25 | public void ParseJsonParameterWithDuplicateKeyThrowsException() 26 | { 27 | var result = new Dictionary { { "prefix:key", "value" } }; 28 | var value = "{\"key\": \"newvalue\"}"; 29 | var keyPrefix = "prefix"; 30 | 31 | Assert.Throws(() => ParameterProcessorUtil.ParseJsonParameter(keyPrefix, value, result)); 32 | } 33 | 34 | [Fact] 35 | public void ParseJsonParameterForInvalidJsonThrowsException() 36 | { 37 | var result = new Dictionary(); 38 | var value = "invalid json"; 39 | var keyPrefix = ""; 40 | 41 | Assert.ThrowsAny(() => ParameterProcessorUtil.ParseJsonParameter(keyPrefix, value, result)); 42 | } 43 | 44 | [Fact] 45 | public void ParseStringListParameterSuccessfully() 46 | { 47 | var result = new Dictionary(); 48 | var value = "value1,value2,value3"; 49 | var keyPrefix = "prefix"; 50 | 51 | ParameterProcessorUtil.ParseStringListParameter(keyPrefix, value, result); 52 | 53 | Assert.Equal(3, result.Count); 54 | Assert.Contains("prefix:0", result.Keys); 55 | Assert.Contains("prefix:1", result.Keys); 56 | Assert.Contains("prefix:2", result.Keys); 57 | Assert.Equal("value1", result["prefix:0"]); 58 | Assert.Equal("value2", result["prefix:1"]); 59 | Assert.Equal("value3", result["prefix:2"]); 60 | } 61 | 62 | [Fact] 63 | public void ParseStringListParameterWithDuplicateKeyThrowsException() 64 | { 65 | var result = new Dictionary { { "prefix:0", "value" } }; 66 | var value = "value1,value2,value3"; 67 | var keyPrefix = "prefix"; 68 | 69 | Assert.Throws(() => ParameterProcessorUtil.ParseStringListParameter(keyPrefix, value, result)); 70 | } 71 | 72 | [Fact] 73 | public void ParseStringParameterSuccessfully() 74 | { 75 | var result = new Dictionary(); 76 | var value = "stringValue"; 77 | var key = "myKey"; 78 | 79 | ParameterProcessorUtil.ParseStringParameter(key, value, result); 80 | 81 | Assert.Single(result); 82 | Assert.Contains("myKey", result.Keys); 83 | Assert.Equal("stringValue", result["myKey"]); 84 | } 85 | 86 | [Fact] 87 | public void ParseStringParameterWithDuplicateKeyThrowsException() 88 | { 89 | var result = new Dictionary { { "myKey", "existingValue" } }; 90 | var value = "newValue"; 91 | var key = "myKey"; 92 | 93 | Assert.Throws(() => ParameterProcessorUtil.ParseStringParameter(key, value, result)); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Utils/ParameterProcessorUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 4 | using Microsoft.Extensions.Configuration; 5 | using System.Text.Json; 6 | 7 | namespace Amazon.Extensions.Configuration.SystemsManager.Utils 8 | { 9 | public static class ParameterProcessorUtil 10 | { 11 | /// 12 | /// Parses the SSM parameter as JSON 13 | /// 14 | /// prefix to add in configution key 15 | /// SSM parameter value 16 | /// append the parsed JSON value into 17 | /// SSM parameter key is already present in 18 | /// does not represent a valid single JSON value. 19 | public static void ParseJsonParameter(string keyPrefix, string value, IDictionary result) 20 | { 21 | foreach (var kv in JsonConfigurationParser.Parse(value)) 22 | { 23 | var key = !string.IsNullOrEmpty(keyPrefix) ? ConfigurationPath.Combine(keyPrefix, kv.Key) : kv.Key; 24 | if (result.ContainsKey(key)) 25 | { 26 | throw new DuplicateParameterException($"Duplicate parameter '{key}' found. Parameter keys are case-insensitive."); 27 | } 28 | 29 | result.Add(key, kv.Value); 30 | } 31 | } 32 | 33 | /// 34 | /// Parses the StringList SSM parameter as List of String 35 | ///

36 | /// Items in a StringList must be separated by a comma (,). 37 | /// You can't use other punctuation or special characters to escape items in the list. 38 | /// If you have a parameter value that requires a comma, then use the String type. 39 | /// https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html#param-create-cli-stringlist 40 | ///
41 | /// prefix to add in configution key 42 | /// SSM parameter 43 | /// append the parsed string list value into 44 | /// SSM parameter key is already present in 45 | public static void ParseStringListParameter(string keyPrefix, string value, IDictionary result) 46 | { 47 | var configKeyValuePairs = value 48 | .Split(',') 49 | .Select((eachValue, idx) => new KeyValuePair($"{keyPrefix}{ConfigurationPath.KeyDelimiter}{idx}", eachValue)); 50 | 51 | foreach (var kv in configKeyValuePairs) 52 | { 53 | if (result.ContainsKey(kv.Key)) 54 | { 55 | throw new DuplicateParameterException($"Duplicate parameter '{kv.Key}' found. Parameter keys are case-insensitive."); 56 | } 57 | 58 | result.Add(kv.Key, kv.Value); 59 | } 60 | } 61 | 62 | /// 63 | /// Parses the SSM parameter as String 64 | /// 65 | /// key to be used for configution key 66 | /// SSM parameter 67 | /// append the parsed string value into 68 | /// SSM parameter key is already present in 69 | public static void ParseStringParameter(string key, string value, IDictionary result) 70 | { 71 | if (result.ContainsKey(key)) 72 | { 73 | throw new DuplicateParameterException($"Duplicate parameter '{key}' found. Parameter keys are case-insensitive."); 74 | } 75 | 76 | result.Add(key, value); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/JsonConfigurationParser.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Globalization; 19 | using System.IO; 20 | using System.Linq; 21 | using System.Text; 22 | using System.Text.Json; 23 | using Microsoft.Extensions.Configuration; 24 | 25 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 26 | { 27 | public class JsonConfigurationParser 28 | { 29 | private JsonConfigurationParser() { } 30 | 31 | private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase); 32 | private readonly Stack _context = new Stack(); 33 | private string _currentPath; 34 | 35 | public static IDictionary Parse(Stream input) 36 | { 37 | using (var doc = JsonDocument.Parse(input)) 38 | { 39 | var parser = new JsonConfigurationParser(); 40 | parser.VisitElement(doc.RootElement); 41 | return parser._data; 42 | } 43 | } 44 | 45 | public static IDictionary Parse(string input) 46 | { 47 | using (var doc = JsonDocument.Parse(input)) 48 | { 49 | var parser = new JsonConfigurationParser(); 50 | parser.VisitElement(doc.RootElement); 51 | return parser._data; 52 | } 53 | } 54 | 55 | private void VisitElement(JsonElement element) 56 | { 57 | switch (element.ValueKind) 58 | { 59 | case JsonValueKind.Undefined: 60 | break; 61 | case JsonValueKind.Object: 62 | foreach (var property in element.EnumerateObject()) 63 | { 64 | EnterContext(property.Name); 65 | VisitElement(property.Value); 66 | ExitContext(); 67 | } 68 | break; 69 | case JsonValueKind.Array: 70 | VisitArray(element); 71 | break; 72 | case JsonValueKind.String: 73 | case JsonValueKind.Number: 74 | case JsonValueKind.True: 75 | case JsonValueKind.False: 76 | VisitPrimitive(element); 77 | break; 78 | case JsonValueKind.Null: 79 | VisitNull(); 80 | break; 81 | } 82 | 83 | } 84 | 85 | private void VisitArray(JsonElement array) 86 | { 87 | int index = 0; 88 | foreach (var item in array.EnumerateArray()) 89 | { 90 | EnterContext(index.ToString(CultureInfo.InvariantCulture)); 91 | VisitElement(item); 92 | ExitContext(); 93 | 94 | index++; 95 | } 96 | } 97 | 98 | private void VisitNull() 99 | { 100 | var key = _currentPath; 101 | _data[key] = null; 102 | } 103 | 104 | private void VisitPrimitive(JsonElement data) 105 | { 106 | var key = _currentPath; 107 | 108 | if (_data.ContainsKey(key)) 109 | { 110 | throw new FormatException($"A duplicate key '{key}' was found."); 111 | } 112 | 113 | _data[key] = data.ToString(); 114 | } 115 | 116 | private void EnterContext(string context) 117 | { 118 | _context.Push(context); 119 | _currentPath = ConfigurationPath.Combine(_context.Reverse()); 120 | } 121 | 122 | private void ExitContext() 123 | { 124 | _context.Pop(); 125 | _currentPath = ConfigurationPath.Combine(_context.Reverse()); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/Internal/SystemsManagerProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using Amazon.SimpleSystemsManagement; 17 | using Amazon.SimpleSystemsManagement.Model; 18 | using System; 19 | using System.Collections.Generic; 20 | using System.Linq; 21 | using System.Threading.Tasks; 22 | using Microsoft.Extensions.Configuration; 23 | 24 | namespace Amazon.Extensions.Configuration.SystemsManager.Internal 25 | { 26 | public class SystemsManagerProcessor : ISystemsManagerProcessor 27 | { 28 | private const string SecretsManagerPath = "/aws/reference/secretsmanager/"; 29 | 30 | private SystemsManagerConfigurationSource Source { get; } 31 | 32 | public SystemsManagerProcessor(SystemsManagerConfigurationSource source) 33 | { 34 | if (source.AwsOptions == null) throw new ArgumentNullException(nameof(source.AwsOptions)); 35 | if (source.Path == null) throw new ArgumentNullException(nameof(source.Path)); 36 | 37 | Source = source; 38 | Source.ParameterProcessor = Source.ParameterProcessor ?? 39 | (IsSecretsManagerPath(Source.Path) 40 | ? new JsonParameterProcessor() 41 | : new DefaultParameterProcessor()); 42 | } 43 | 44 | public async Task> GetDataAsync() 45 | { 46 | return IsSecretsManagerPath(Source.Path) 47 | ? await GetParameterAsync().ConfigureAwait(false) 48 | : await GetParametersByPathAsync().ConfigureAwait(false); 49 | } 50 | 51 | private async Task> GetParametersByPathAsync() 52 | { 53 | using (var client = Source.AwsOptions.CreateServiceClient()) 54 | { 55 | if (client is AmazonSimpleSystemsManagementClient impl) 56 | { 57 | impl.BeforeRequestEvent += ServiceClientAppender.ServiceClientBeforeRequestEvent; 58 | } 59 | 60 | var parameters = new List(); 61 | string nextToken = null; 62 | do 63 | { 64 | var response = await client.GetParametersByPathAsync(new GetParametersByPathRequest { Path = Source.Path, Recursive = true, WithDecryption = true, NextToken = nextToken, ParameterFilters = Source.Filters }).ConfigureAwait(false); 65 | nextToken = response.NextToken; 66 | parameters.AddRange(response.Parameters ?? new List()); 67 | } while (!string.IsNullOrEmpty(nextToken)); 68 | 69 | return AddPrefix(Source.ParameterProcessor.ProcessParameters(parameters, Source.Path), Source.Prefix); 70 | } 71 | } 72 | 73 | private async Task> GetParameterAsync() 74 | { 75 | using (var client = Source.AwsOptions.CreateServiceClient()) 76 | { 77 | if (client is AmazonSimpleSystemsManagementClient impl) 78 | { 79 | impl.BeforeRequestEvent += ServiceClientAppender.ServiceClientBeforeRequestEvent; 80 | } 81 | 82 | var response = await client.GetParameterAsync(new GetParameterRequest { Name = Source.Path, WithDecryption = true }).ConfigureAwait(false); 83 | 84 | var prefix = Source.Prefix; 85 | return AddPrefix(Source.ParameterProcessor.ProcessParameters(new []{response.Parameter}, Source.Path), prefix); 86 | } 87 | } 88 | 89 | public static bool IsSecretsManagerPath(string path) => path.StartsWith(SecretsManagerPath, StringComparison.OrdinalIgnoreCase); 90 | 91 | public static IDictionary AddPrefix(IDictionary input, string prefix) 92 | { 93 | return string.IsNullOrEmpty(prefix) 94 | ? input 95 | : input.ToDictionary(pair => $"{prefix}{ConfigurationPath.KeyDelimiter}{pair.Key}", pair => pair.Value, StringComparer.OrdinalIgnoreCase); 96 | 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/SystemsManagerConfigurationProviderTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 18 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 19 | using Amazon.Extensions.NETCore.Setup; 20 | using Amazon.SimpleSystemsManagement.Model; 21 | using Moq; 22 | using Xunit; 23 | 24 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 25 | { 26 | public class SystemsManagerConfigurationProviderTests 27 | { 28 | private readonly Mock _systemsManagerProcessorMock = new Mock(); 29 | 30 | [Fact] 31 | public void LoadForParameterStoreShouldReturnProperParameters() 32 | { 33 | var parameters = new List 34 | { 35 | new Parameter {Name = "/start/path/p1/p2-1", Value = "p1:p2-1"}, 36 | new Parameter {Name = "/start/path/p1/p2-2", Value = "p1:p2-2"}, 37 | new Parameter {Name = "/start/path/p1/p2/p3-1", Value = "p1:p2:p3-1"}, 38 | new Parameter {Name = "/start/path/p1/p2/p3-2", Value = "p1:p2:p3-2"} 39 | }; 40 | var parameterProcessorMock = new Mock(); 41 | var provider = ConfigureParameterStoreConfigurationProvider(parameterProcessorMock, parameters); 42 | 43 | provider.Load(); 44 | 45 | foreach (var parameter in parameters) 46 | { 47 | Assert.True(provider.TryGet(parameter.Value, out _)); 48 | } 49 | 50 | parameterProcessorMock.VerifyAll(); 51 | } 52 | 53 | [Fact] 54 | public void LoadForAppConfigShouldReturnProperValues() 55 | { 56 | var values = new Dictionary 57 | { 58 | { "testKey", "testValue" }, 59 | { "testKey2", "testValue2" }, 60 | { "testKey3", "testValue3" }, 61 | { "testKey4", "testValue4" }, 62 | { "testKey5", "testValue5" } 63 | }; 64 | var provider = ConfigureAppConfigConfigurationProvider(values); 65 | 66 | provider.Load(); 67 | 68 | foreach (var parameter in values) 69 | { 70 | Assert.True(provider.TryGet(parameter.Key, out _)); 71 | } 72 | } 73 | 74 | private SystemsManagerConfigurationProvider ConfigureParameterStoreConfigurationProvider(Mock parameterProcessorMock, IReadOnlyCollection parameters) 75 | { 76 | const string path = "/start/path"; 77 | var source = new SystemsManagerConfigurationSource 78 | { 79 | ParameterProcessor = parameterProcessorMock.Object, 80 | AwsOptions = new AWSOptions(), 81 | Path = path 82 | }; 83 | var provider = new SystemsManagerConfigurationProvider(source, _systemsManagerProcessorMock.Object); 84 | 85 | var getData = new DefaultParameterProcessor().ProcessParameters(parameters, path); 86 | 87 | _systemsManagerProcessorMock.Setup(p => p.GetDataAsync()).ReturnsAsync(() => getData); 88 | 89 | return provider; 90 | } 91 | 92 | private SystemsManagerConfigurationProvider ConfigureAppConfigConfigurationProvider(IDictionary values) 93 | { 94 | var source = new AppConfigConfigurationSource 95 | { 96 | ApplicationId = "appId", 97 | EnvironmentId = "envId", 98 | ConfigProfileId = "profileId", 99 | AwsOptions = new AWSOptions() 100 | }; 101 | 102 | _systemsManagerProcessorMock.Setup(p => p.GetDataAsync()).ReturnsAsync(() => values); 103 | 104 | var provider = new SystemsManagerConfigurationProvider(source, _systemsManagerProcessorMock.Object); 105 | 106 | return provider; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Workflow will create a new release branch that contains the updated C# project versions and changelog. 2 | # The workflow will also create a PR that targets `dev` from the release branch. 3 | name: Create Release PR 4 | 5 | # This workflow is manually triggered when in preparation for a release. The workflow should be dispatched from the `dev` branch. 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | OVERRIDE_VERSION: 10 | description: "Override Version" 11 | type: string 12 | required: false 13 | 14 | permissions: 15 | id-token: write 16 | 17 | jobs: 18 | release-pr: 19 | name: Release PR 20 | runs-on: ubuntu-latest 21 | 22 | env: 23 | INPUT_OVERRIDE_VERSION: ${{ github.event.inputs.OVERRIDE_VERSION }} 24 | 25 | steps: 26 | # Assume an AWS Role that provides access to the Access Token 27 | - name: Configure AWS Credentials 28 | uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 29 | with: 30 | role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} 31 | aws-region: us-west-2 32 | # Retrieve the Access Token from Secrets Manager 33 | - name: Retrieve secret from AWS Secrets Manager 34 | uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 #v2.0.10 35 | with: 36 | secret-ids: | 37 | AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} 38 | parse-json-secrets: true 39 | # Checkout a full clone of the repo 40 | - name: Checkout 41 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 42 | with: 43 | fetch-depth: '0' 44 | token: ${{ env.AWS_SECRET_TOKEN }} 45 | # Install .NET8 which is needed for AutoVer 46 | - name: Setup .NET 8.0 47 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 48 | with: 49 | dotnet-version: 8.0.x 50 | # Install AutoVer to automate versioning and changelog creation 51 | - name: Install AutoVer 52 | run: dotnet tool install --global AutoVer --version 0.0.25 53 | # Set up a git user to be able to run git commands later on 54 | - name: Setup Git User 55 | run: | 56 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 57 | git config --global user.name "aws-sdk-dotnet-automation" 58 | # Create the release branch which will contain the version changes and updated changelog 59 | - name: Create Release Branch 60 | id: create-release-branch 61 | run: | 62 | branch=releases/next-release 63 | git checkout -b $branch 64 | echo "BRANCH=$branch" >> $GITHUB_OUTPUT 65 | # Update the version of projects based on the change files 66 | - name: Increment Version 67 | run: autover version 68 | if: env.INPUT_OVERRIDE_VERSION == '' 69 | # Update the version of projects based on the override version 70 | - name: Increment Version 71 | run: autover version --use-version "$INPUT_OVERRIDE_VERSION" 72 | if: env.INPUT_OVERRIDE_VERSION != '' 73 | # Update the changelog based on the change files 74 | - name: Update Changelog 75 | run: autover changelog 76 | # Push the release branch up as well as the created tag 77 | - name: Push Changes 78 | run: | 79 | branch=${{ steps.create-release-branch.outputs.BRANCH }} 80 | git push origin $branch 81 | git push origin $branch --tags 82 | # Get the release name that will be used to create a PR 83 | - name: Read Release Name 84 | id: read-release-name 85 | run: | 86 | version=$(autover changelog --release-name) 87 | echo "VERSION=$version" >> $GITHUB_OUTPUT 88 | # Get the changelog that will be used to create a PR 89 | - name: Read Changelog 90 | id: read-changelog 91 | run: | 92 | changelog=$(autover changelog --output-to-console) 93 | echo "CHANGELOG<> "$GITHUB_OUTPUT" 94 | # Create the Release PR and label it 95 | - name: Create Pull Request 96 | env: 97 | GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} 98 | run: | 99 | pr_url="$(gh pr create --title "${{ steps.read-release-name.outputs.VERSION }}" --body "${{ steps.read-changelog.outputs.CHANGELOG }}" --base dev --head ${{ steps.create-release-branch.outputs.BRANCH }})" 100 | gh label create "Release PR" --description "A Release PR that includes versioning and changelog changes" -c "#FF0000" -f 101 | gh pr edit $pr_url --add-label "Release PR" 102 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/DefaultParameterProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Amazon.SimpleSystemsManagement; 3 | using Amazon.SimpleSystemsManagement.Model; 4 | using Xunit; 5 | 6 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 7 | { 8 | public class DefaultParameterProcessorTests 9 | { 10 | private readonly IParameterProcessor _parameterProcessor = new DefaultParameterProcessor(); 11 | 12 | [Fact] 13 | public void ExtractConfigurationKeyFromParameter() 14 | { 15 | var processor = new DefaultParameterProcessor(); 16 | 17 | Assert.Equal("level1:level2", processor.GetKey(new Parameter() { Name = "/level1/level2" }, "")); 18 | Assert.Equal("level1:level2", processor.GetKey(new Parameter() { Name = "/level1/level2" }, "/")); 19 | Assert.Equal("level1:level2", processor.GetKey(new Parameter() { Name = "/level1/level2" }, "/someotherlevel")); 20 | 21 | Assert.Equal("level2:level3", processor.GetKey(new Parameter() { Name = "/level1/level2/level3" }, "/level1")); 22 | Assert.Equal("level2", processor.GetKey(new Parameter() { Name = "/level1/level2" }, "/LEVEL1")); 23 | } 24 | 25 | [Fact] 26 | public void ExtractConfigurationValueFromParameter() 27 | { 28 | var processor = new DefaultParameterProcessor(); 29 | 30 | Assert.Equal("Some value", processor.GetValue(new Parameter() { Value = "Some value" }, null)); 31 | } 32 | 33 | [Fact] 34 | public void ProcessParametersTest() 35 | { 36 | var parameters = new List 37 | { 38 | new Parameter { Name = "/start/path/p1/p2-1", Value = "p1:p2-1" }, 39 | new Parameter { Name = "/start/path/p1/p2-2", Value = "p1:p2-2" }, 40 | new Parameter { Name = "/start/path/p1/p2/p3-1", Value = "p1:p2:p3-1" }, 41 | new Parameter { Name = "/start/path/p1/p2/p3-2", Value = "p1:p2:p3-2" }, 42 | }; 43 | 44 | const string path = "/start/path"; 45 | 46 | var data = _parameterProcessor.ProcessParameters(parameters, path); 47 | 48 | Assert.All(data, item => Assert.Equal(item.Value, item.Key)); 49 | } 50 | 51 | [Fact] 52 | public void ProcessParametersStringListTest() 53 | { 54 | var parameters = new List 55 | { 56 | new Parameter { Name = "/string-list/single", Value = "p1", Type = ParameterType.StringList }, 57 | new Parameter { Name = "/string-list/multiple", Value = "p1,p2,p3", Type = ParameterType.StringList }, 58 | new Parameter { Name = "/string-list/empty", Value = "", Type = ParameterType.StringList }, 59 | }; 60 | 61 | const string path = "/string-list"; 62 | 63 | var data = _parameterProcessor.ProcessParameters(parameters, path); 64 | 65 | Assert.Equal(5, data.Keys.Count); 66 | Assert.Equal("p1", data["single:0"]); 67 | Assert.Equal("p1", data["multiple:0"]); 68 | Assert.Equal("p2", data["multiple:1"]); 69 | Assert.Equal("p3", data["multiple:2"]); 70 | Assert.Equal("", data["empty:0"]); 71 | } 72 | 73 | 74 | [Fact] 75 | public void ProcessParametersRootTest() 76 | { 77 | var parameters = new List 78 | { 79 | new Parameter { Name = "/p1", Value = "p1" }, 80 | new Parameter { Name = "p2", Value = "p2" }, 81 | }; 82 | 83 | const string path = "/"; 84 | 85 | var data = _parameterProcessor.ProcessParameters(parameters, path); 86 | 87 | Assert.All(data, item => Assert.Equal(item.Value, item.Key)); 88 | } 89 | 90 | [Fact] 91 | public void DuplicateSimpleParametersTest() 92 | { 93 | var parameters = new List 94 | { 95 | new Parameter { Name = "/start/path/p1", Value = "p1:1" }, 96 | new Parameter { Name = "/start/path/P1", Value = "p1:2" } 97 | }; 98 | 99 | const string path = "/start/path"; 100 | Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, path)); 101 | } 102 | 103 | [Fact] 104 | public void DuplicateStringListParametersTest() 105 | { 106 | var parameters = new List 107 | { 108 | new Parameter { Name = "/string-list/multiple", Value = "p1,p2,p3", Type = ParameterType.StringList }, 109 | new Parameter { Name = "/string-list/MULTIPLE", Value = "p3,p5,p6", Type = ParameterType.StringList } 110 | }; 111 | 112 | const string path = "/string-list"; 113 | Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, path)); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws/aws-dotnet-extensions-configuration/issues), or [recently closed](https://github.com/aws/aws-dotnet-extensions-configuration/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | ## Adding a `change file` to your contribution branch 43 | 44 | Each contribution branch should include a `change file` that contains a changelog message for each project that has been updated, as well as the type of increment to perform for those changes when versioning the project. 45 | 46 | A `change file` looks like the following example: 47 | ```json 48 | { 49 | "Projects": [ 50 | { 51 | "Name": "Amazon.Extensions.Configuration.SystemsManager", 52 | "Type": "Patch", 53 | "ChangelogMessages": [ 54 | "Fixed an issue causing a failure somewhere" 55 | ] 56 | } 57 | ] 58 | } 59 | ``` 60 | The `change file` lists all the modified projects, the changelog message for each project as well as the increment type. 61 | 62 | These files are located in the repo at .autover/changes/ 63 | 64 | You can use the `AutoVer` tool to create the change file. You can install it using the following command: 65 | ``` 66 | dotnet tool install -g AutoVer 67 | ``` 68 | 69 | You can create the `change file` using the following command: 70 | ``` 71 | autover change --project-name "Amazon.Extensions.Configuration.SystemsManager" -m "Fixed an issue causing a failure somewhere 72 | ``` 73 | Note: Make sure to run the command from the root of the repository. 74 | 75 | You can update the command to specify which project you are updating. 76 | The available projects are: 77 | * Amazon.Extensions.Configuration.SystemsManager 78 | 79 | The possible increment types are: 80 | * Patch 81 | * Minor 82 | * Major 83 | 84 | Note: You do not need to create a new `change file` for every changelog message or project within your branch. You can create one `change file` that contains all the modified projects and the changelog messages. 85 | 86 | ## Finding contributions to work on 87 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-dotnet-extensions-configuration/labels/help%20wanted) issues is a great place to start. 88 | 89 | 90 | ## Code of Conduct 91 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 92 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 93 | opensource-codeofconduct@amazon.com with any additional questions or comments. 94 | 95 | 96 | ## Security issue notifications 97 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 98 | 99 | 100 | ## Licensing 101 | 102 | See the [LICENSE](https://github.com/aws/aws-dotnet-extensions-configuration/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 103 | 104 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 105 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/SystemsManagerExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | using System; 16 | using Amazon.Extensions.NETCore.Setup; 17 | using Microsoft.Extensions.Configuration; 18 | using Xunit; 19 | 20 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 21 | { 22 | public class SystemsManagerExtensionsTests 23 | { 24 | [Theory] 25 | [MemberData(nameof(SourceExtensionData))] 26 | [MemberData(nameof(WithAWSOptionsExtensionData))] 27 | [MemberData(nameof(NoAWSOptionsExtensionData))] 28 | public void AddSystemsManagerInlineTest(Func configurationBuilder, Type exceptionType, string exceptionMessage) 29 | { 30 | var builder = new ConfigurationBuilder(); 31 | 32 | IConfigurationBuilder ExecuteBuilder() => configurationBuilder(builder); 33 | 34 | if (exceptionType != null) 35 | { 36 | var ex = Assert.Throws(exceptionType, ExecuteBuilder); 37 | Assert.Contains(exceptionMessage, ex.Message, StringComparison.Ordinal); 38 | } 39 | else 40 | { 41 | var result = ExecuteBuilder(); 42 | Assert.Equal(builder, result); 43 | } 44 | } 45 | 46 | public static TheoryData, Type, string> SourceExtensionData => new TheoryData, Type, string> 47 | { 48 | {builder => builder.AddSystemsManager(CreateSource(null, null, false, null, null)), typeof(ArgumentNullException), "Path"}, 49 | {builder => builder.AddSystemsManager(CreateSource(null, null, true, null, null)), typeof(ArgumentNullException), "Path"}, 50 | {builder => builder.AddSystemsManager(CreateSource("/path", null, false, null, null)), null, null}, 51 | {builder => builder.AddSystemsManager(CreateSource("/aws/reference/secretsmanager/somevalue", null, false, null, null)), null, null} 52 | }; 53 | 54 | public static TheoryData, Type, string> WithAWSOptionsExtensionData => new TheoryData, Type, string> 55 | { 56 | {builder => builder.AddSystemsManager(null, null), typeof(ArgumentNullException), "path"}, 57 | {builder => builder.AddSystemsManager("/path", null), typeof(ArgumentNullException), "awsOptions"}, 58 | {builder => builder.AddSystemsManager(null, new AWSOptions()), typeof(ArgumentNullException), "path"}, 59 | {builder => builder.AddSystemsManager("/aws/reference/secretsmanager/somevalue", new AWSOptions()), null, null}, 60 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), true), null, null}, 61 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), false), null, null}, 62 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), TimeSpan.Zero), null, null}, 63 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), TimeSpan.Zero), null, null}, 64 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), true, TimeSpan.Zero), null, null}, 65 | {builder => builder.AddSystemsManager("/path", new AWSOptions(), false, TimeSpan.Zero), null, null} 66 | }; 67 | 68 | public static TheoryData, Type, string> NoAWSOptionsExtensionData => new TheoryData, Type, string> 69 | { 70 | {builder => builder.AddSystemsManager(null as string), typeof(ArgumentNullException), "path"}, 71 | {builder => builder.AddSystemsManager("/path"), null, null}, 72 | {builder => builder.AddSystemsManager("/aws/reference/secretsmanager/somevalue"), null, null}, 73 | {builder => builder.AddSystemsManager("/path", true), null, null}, 74 | {builder => builder.AddSystemsManager("/path", false), null, null}, 75 | {builder => builder.AddSystemsManager("/path", TimeSpan.Zero), null, null}, 76 | {builder => builder.AddSystemsManager("/path", TimeSpan.Zero), null, null}, 77 | {builder => builder.AddSystemsManager("/path", true, TimeSpan.Zero), null, null}, 78 | {builder => builder.AddSystemsManager("/path", false, TimeSpan.Zero), null, null} 79 | }; 80 | 81 | private static Action CreateSource(string path, AWSOptions awsOptions, bool optional, TimeSpan? reloadAfter, Action onLoadException) 82 | { 83 | return source => 84 | { 85 | source.Path = path; 86 | source.AwsOptions = awsOptions; 87 | source.Optional = optional; 88 | source.ReloadAfter = reloadAfter; 89 | source.OnLoadException = onLoadException; 90 | }; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Integ/ConfigurtaionBuilderIntegrationTestFixture.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using Amazon.Extensions.NETCore.Setup; 17 | using Amazon.SimpleSystemsManagement; 18 | using Amazon.SimpleSystemsManagement.Model; 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Linq; 22 | using System.Threading; 23 | using System.Threading.Tasks; 24 | 25 | namespace Amazon.Extensions.Configuration.SystemsManager.Integ 26 | { 27 | public class IntegTestFixture : IDisposable 28 | { 29 | public const string ParameterPrefix = @"/configuration-extension-testdata/ssm/"; 30 | 31 | public AWSOptions AWSOptions { get; private set; } 32 | 33 | private bool disposed = false; 34 | 35 | public IDictionary TestData { get; } = new Dictionary 36 | { 37 | {"hello", "world"}, 38 | {"hello2", "world2"}, 39 | }; 40 | 41 | public IntegTestFixture() 42 | { 43 | AWSOptions = new AWSOptions(); 44 | AWSOptions.Region = Amazon.RegionEndpoint.USWest2; 45 | 46 | seedTestData(); 47 | } 48 | 49 | public void Dispose() 50 | { 51 | Dispose(true); 52 | GC.SuppressFinalize(this); 53 | } 54 | 55 | protected virtual void Dispose(bool disposing) 56 | { 57 | if (disposed) return; 58 | if (disposing) 59 | { 60 | cleanupTestData(); 61 | } 62 | disposed = true; 63 | } 64 | 65 | private void seedTestData() 66 | { 67 | bool success = false; 68 | using (var client = AWSOptions.CreateServiceClient()) 69 | { 70 | var tasks = new List(); 71 | foreach (var kv in TestData) 72 | { 73 | Console.WriteLine($"Adding parameter: ({ParameterPrefix + kv.Key}, {kv.Value})"); 74 | tasks.Add(client.PutParameterAsync(new PutParameterRequest 75 | { 76 | Name = ParameterPrefix + kv.Key, 77 | Value = kv.Value, 78 | Type = ParameterType.String 79 | })); 80 | }; 81 | Task.WaitAll(tasks.ToArray()); 82 | 83 | // due to eventual consistency, wait for 5 sec increments for 3 times to verify 84 | // test data is correctly set before executing tests. 85 | const int tries = 3; 86 | for (int i = 0; i < tries; i++) 87 | { 88 | int count = 0; 89 | GetParametersByPathResponse response; 90 | do 91 | { 92 | response = client.GetParametersByPathAsync(new GetParametersByPathRequest 93 | { 94 | Path = ParameterPrefix 95 | }).Result; 96 | 97 | count += response.Parameters.Count; 98 | } while (!string.IsNullOrEmpty(response.NextToken)); 99 | 100 | success = (count == TestData.Count); 101 | 102 | if (success) 103 | { 104 | Console.WriteLine("Verified that test data is available."); 105 | break; 106 | } 107 | else 108 | { 109 | Console.WriteLine($"Waiting on test data to be available. Waiting {count + 1}/{tries}"); 110 | Thread.Sleep(5 * 1000); 111 | } 112 | } 113 | } 114 | 115 | if (!success) throw new Exception("Failed to seed integration test data"); 116 | } 117 | 118 | private void cleanupTestData() 119 | { 120 | Console.Write($"Delete all test parameters with prefix '{ParameterPrefix}'... "); 121 | using (var client = AWSOptions.CreateServiceClient()) 122 | { 123 | GetParametersByPathResponse response; 124 | do 125 | { 126 | response = client.GetParametersByPathAsync(new GetParametersByPathRequest 127 | { 128 | Path = ParameterPrefix 129 | }).Result; 130 | 131 | client.DeleteParametersAsync(new DeleteParametersRequest 132 | { 133 | Names = response.Parameters.Select(p => p.Name).ToList() 134 | }).Wait(); 135 | } while (!string.IsNullOrEmpty(response.NextToken)); 136 | 137 | // no need to wait for eventual consistency here given we are not running tests back-to-back 138 | } 139 | Console.WriteLine("Done"); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/JsonOrStringParameterProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Amazon.SimpleSystemsManagement; 3 | using Amazon.SimpleSystemsManagement.Model; 4 | using Xunit; 5 | 6 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 7 | { 8 | public class JsonOrStringParameterProcessorTests 9 | { 10 | private readonly IParameterProcessor _parameterProcessor = new JsonOrStringParameterProcessor(); 11 | 12 | [Fact] 13 | public void ParsesJsonParametersSuccessfully() 14 | { 15 | var parameters = new List 16 | { 17 | new Parameter() { Name = "/test/level1/level2", Type = ParameterType.String, Value = "{\"level1Key\":\"level1value\"}" }, 18 | new Parameter() { Name = "/test/level1", Type = ParameterType.String, Value = "{\"level1Key\" : {\"level2key\" : \"level2value\"}}" } 19 | }; 20 | var result = _parameterProcessor.ProcessParameters(parameters, "/test"); 21 | 22 | Assert.Equal(2, result.Count); 23 | Assert.True(result.ContainsKey("level1:level2:level1Key")); 24 | Assert.True(result.ContainsKey("level1:level1Key:level2key")); 25 | Assert.Equal("level1value", result["level1:level2:level1Key"]); 26 | Assert.Equal("level2value", result["level1:level1Key:level2key"]); 27 | } 28 | 29 | [Fact] 30 | public void ProcessParametersParsesJsonParametersWithoutPrefixSuccessfully() 31 | { 32 | var parameters = new List 33 | { 34 | new Parameter() 35 | { 36 | Name = "/test/level1/level2", Type = ParameterType.String, Value = "{\"level1Key\":\"level1value\"}" 37 | }, 38 | new Parameter() 39 | { 40 | Name = "/test/level1", Type = ParameterType.String, 41 | Value = "{\"level1Key\" : {\"level2key\" : \"level2value\"}}" 42 | } 43 | }; 44 | var result = _parameterProcessor.ProcessParameters(parameters, ""); 45 | 46 | Assert.Equal(2, result.Count); 47 | Assert.True(result.ContainsKey("test:level1:level2:level1Key")); 48 | Assert.True(result.ContainsKey("test:level1:level1Key:level2key")); 49 | Assert.Equal("level1value", result["test:level1:level2:level1Key"]); 50 | Assert.Equal("level2value", result["test:level1:level1Key:level2key"]); 51 | } 52 | 53 | 54 | [Fact] 55 | public void ProcessParametersFallBackOnString() 56 | { 57 | var parameters = new List 58 | { 59 | new Parameter() { Name = "/test/stringParam", Type = ParameterType.String, Value = "some string" } 60 | }; 61 | var result = _parameterProcessor.ProcessParameters(parameters, "/test"); 62 | 63 | Assert.Single(result); 64 | Assert.True(result.ContainsKey("stringParam")); 65 | Assert.Equal("some string", result["stringParam"]); 66 | } 67 | 68 | [Fact] 69 | public void ProcessParametersThrowsOnDuplicateParameter() 70 | { 71 | var parameters = new List 72 | { 73 | new Parameter() { Name = "/test/Duplicate", Type = ParameterType.String, Value = "value1" }, 74 | new Parameter() { Name = "/test/duplicate", Type = ParameterType.String, Value = "value2" } 75 | }; 76 | var duplicateParameterException = Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, "/test")); 77 | Assert.Equal("Duplicate parameter 'duplicate' found. Parameter keys are case-insensitive.", duplicateParameterException.Message); 78 | } 79 | 80 | [Fact] 81 | public void ProcessParametersThrowsOnDuplicateParameterAtMultiLevel() 82 | { 83 | var parameters = new List 84 | { 85 | new Parameter() { Name = "/test/level1", Type = ParameterType.String, Value = "{\"level1Key\":\"level1value\"}" }, 86 | new Parameter() { Name = "/test/level1/level1key", Type = ParameterType.String, Value = "level1valueOverriden" }, 87 | }; 88 | 89 | var duplicateParameterException = Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, "/test")); 90 | Assert.Equal("Duplicate parameter 'level1:level1key' found. Parameter keys are case-insensitive.", duplicateParameterException.Message); 91 | } 92 | 93 | [Fact] 94 | public void ProcessParametersThrowsOnDuplicateParameterAtMultilevelForJsonArray() 95 | { 96 | var parameters = new List 97 | { 98 | new Parameter() { Name = "/test/level1", Type = ParameterType.String, Value = "{\"level1Key\" : [\"level1value1\", \"level1value2\"] }" }, 99 | new Parameter() { Name = "/test/level1/Level1key", Type = ParameterType.StringList, Value = "level1valueOverriden" }, 100 | }; 101 | 102 | var duplicateParameterException = Assert.Throws(() => _parameterProcessor.ProcessParameters(parameters, "/test")); 103 | Assert.Equal("Duplicate parameter 'level1:Level1key:0' found. Parameter keys are case-insensitive.", duplicateParameterException.Message); 104 | } 105 | 106 | 107 | [Fact] 108 | public void ProcessParametersProcessesStringListParameters() 109 | { 110 | var parameters = new List 111 | { 112 | new Parameter() { Name = "/test/stringList", Type = ParameterType.StringList, Value = "value1,value2" } 113 | }; 114 | var result = _parameterProcessor.ProcessParameters(parameters, "/test"); 115 | 116 | Assert.Equal(2, result.Count); 117 | Assert.True(result.ContainsKey("stringList:0")); 118 | Assert.Equal("value1", result["stringList:0"]); 119 | Assert.True(result.ContainsKey("stringList:1")); 120 | Assert.Equal("value2", result["stringList:1"]); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Text; 20 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 21 | using Xunit; 22 | 23 | namespace Amazon.Extensions.Configuration.SystemsManager.Tests 24 | { 25 | public class AppConfigProcessorTests 26 | { 27 | [Fact] 28 | public void ParseConfig_ApplicationJson_ParsesSuccessfully() 29 | { 30 | var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; 31 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 32 | 33 | var result = AppConfigProcessor.ParseConfig("application/json", stream); 34 | 35 | Assert.Equal("value1", result["key1"]); 36 | Assert.Equal("value2", result["key2"]); 37 | } 38 | 39 | [Fact] 40 | public void ParseConfig_ApplicationOctetStream_ParsesJsonSuccessfully() 41 | { 42 | var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; 43 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 44 | 45 | var result = AppConfigProcessor.ParseConfig("application/octet-stream", stream); 46 | 47 | Assert.Equal("value1", result["key1"]); 48 | Assert.Equal("value2", result["key2"]); 49 | } 50 | 51 | [Fact] 52 | public void ParseConfig_ApplicationJsonWithCharset_ParsesSuccessfully() 53 | { 54 | var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; 55 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 56 | 57 | var result = AppConfigProcessor.ParseConfig("application/json; charset=utf-8", stream); 58 | 59 | Assert.Equal("value1", result["key1"]); 60 | Assert.Equal("value2", result["key2"]); 61 | } 62 | 63 | [Fact] 64 | public void ParseConfig_UnknownContentType_ParsesJsonSuccessfully() 65 | { 66 | var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; 67 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 68 | 69 | var result = AppConfigProcessor.ParseConfig("application/unknown", stream); 70 | 71 | Assert.Equal("value1", result["key1"]); 72 | Assert.Equal("value2", result["key2"]); 73 | } 74 | 75 | [Fact] 76 | public void ParseConfig_NullContentType_ParsesJsonSuccessfully() 77 | { 78 | var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; 79 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 80 | 81 | var result = AppConfigProcessor.ParseConfig(null, stream); 82 | 83 | Assert.Equal("value1", result["key1"]); 84 | Assert.Equal("value2", result["key2"]); 85 | } 86 | 87 | [Fact] 88 | public void ParseConfig_NestedJson_ParsesSuccessfully() 89 | { 90 | var jsonContent = "{\"section1\":{\"key1\":\"value1\",\"key2\":\"value2\"},\"section2\":{\"key3\":\"value3\"}}"; 91 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 92 | 93 | var result = AppConfigProcessor.ParseConfig("application/octet-stream", stream); 94 | 95 | Assert.Equal("value1", result["section1:key1"]); 96 | Assert.Equal("value2", result["section1:key2"]); 97 | Assert.Equal("value3", result["section2:key3"]); 98 | } 99 | 100 | [Fact] 101 | public void ParseConfig_InvalidJson_ThrowsInvalidOperationException() 102 | { 103 | var invalidJsonContent = "{ invalid json content }"; 104 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidJsonContent)); 105 | 106 | var exception = Assert.Throws(() => 107 | AppConfigProcessor.ParseConfig("application/json", stream)); 108 | 109 | Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); 110 | Assert.Contains("Content-Type was 'application/json'", exception.Message); 111 | } 112 | 113 | [Fact] 114 | public void ParseConfig_InvalidJsonWithOctetStream_ThrowsInvalidOperationException() 115 | { 116 | var invalidJsonContent = "not json at all"; 117 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidJsonContent)); 118 | 119 | var exception = Assert.Throws(() => 120 | AppConfigProcessor.ParseConfig("application/octet-stream", stream)); 121 | 122 | Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); 123 | Assert.Contains("Content-Type was 'application/octet-stream'", exception.Message); 124 | } 125 | 126 | [Fact] 127 | public void ParseConfig_EmptyJson_ReturnsEmptyDictionary() 128 | { 129 | var jsonContent = "{}"; 130 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); 131 | 132 | var result = AppConfigProcessor.ParseConfig("application/json", stream); 133 | 134 | Assert.Empty(result); 135 | } 136 | 137 | [Fact] 138 | public void ParseConfig_EmptyStream_ThrowsInvalidOperationException() 139 | { 140 | using var stream = new MemoryStream(); 141 | 142 | var exception = Assert.Throws(() => 143 | AppConfigProcessor.ParseConfig("application/json", stream)); 144 | 145 | Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /.github/workflows/sync-main-dev.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Workflow is designed to run automatically after the Release PR, which was created by the `Create Release PR` workflow, is closed. 2 | # This workflow has 2 jobs. One will run if the `Release PR` is successfully merged, indicating that a release should go out. 3 | # The other will run if the `Release PR` was closed and a release is not intended to go out. 4 | name: Sync 'dev' and 'main' 5 | 6 | # The workflow will automatically be triggered when any PR is closed. 7 | on: 8 | pull_request: 9 | types: [closed] 10 | 11 | permissions: 12 | contents: write 13 | id-token: write 14 | 15 | jobs: 16 | # This job will check if the PR was successfully merged, it's source branch is `releases/next-release` and target branch is `dev`. 17 | # This indicates that the merged PR was the `Release PR`. 18 | # This job will synchronize `dev` and `main`, create a GitHub Release and delete the `releases/next-release` branch. 19 | sync-dev-and-main: 20 | name: Sync dev and main 21 | if: | 22 | github.event.pull_request.merged == true && 23 | github.event.pull_request.head.ref == 'releases/next-release' && 24 | github.event.pull_request.base.ref == 'dev' 25 | runs-on: ubuntu-latest 26 | steps: 27 | # Assume an AWS Role that provides access to the Access Token 28 | - name: Configure AWS Credentials 29 | uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 30 | with: 31 | role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} 32 | aws-region: us-west-2 33 | # Retrieve the Access Token from Secrets Manager 34 | - name: Retrieve secret from AWS Secrets Manager 35 | uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 #v2.0.10 36 | with: 37 | secret-ids: | 38 | AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} 39 | parse-json-secrets: true 40 | # Checkout a full clone of the repo 41 | - name: Checkout code 42 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 43 | with: 44 | ref: dev 45 | fetch-depth: 0 46 | token: ${{ env.AWS_SECRET_TOKEN }} 47 | # Install .NET8 which is needed for AutoVer 48 | - name: Setup .NET 8.0 49 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 50 | with: 51 | dotnet-version: 8.0.x 52 | # Install AutoVer which is needed to retrieve information about the current release. 53 | - name: Install AutoVer 54 | run: dotnet tool install --global AutoVer --version 0.0.25 55 | # Set up a git user to be able to run git commands later on 56 | - name: Setup Git User 57 | run: | 58 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 59 | git config --global user.name "aws-sdk-dotnet-automation" 60 | # Retrieve the release name which is needed for the GitHub Release 61 | - name: Read Release Name 62 | id: read-release-name 63 | run: | 64 | version=$(autover changelog --release-name) 65 | echo "VERSION=$version" >> $GITHUB_OUTPUT 66 | # Retrieve the tag name which is needed for the GitHub Release 67 | - name: Read Tag Name 68 | id: read-tag-name 69 | run: | 70 | tag=$(autover changelog --tag-name) 71 | echo "TAG=$tag" >> $GITHUB_OUTPUT 72 | # Retrieve the changelog which is needed for the GitHub Release 73 | - name: Read Changelog 74 | id: read-changelog 75 | run: | 76 | changelog=$(autover changelog --output-to-console) 77 | echo "CHANGELOG<> "$GITHUB_OUTPUT" 78 | # Merge dev into main in order to synchronize the 2 branches 79 | - name: Merge dev to main 80 | run: | 81 | git fetch origin 82 | git checkout main 83 | git merge dev 84 | git push origin main 85 | # Create the GitHub Release 86 | - name: Create GitHub Release 87 | env: 88 | GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} 89 | run: | 90 | gh release create "${{ steps.read-tag-name.outputs.TAG }}" --title "${{ steps.read-release-name.outputs.VERSION }}" --notes "${{ steps.read-changelog.outputs.CHANGELOG }}" 91 | # Delete the `releases/next-release` branch 92 | - name: Clean up 93 | run: | 94 | git fetch origin 95 | git push origin --delete releases/next-release 96 | # This job will check if the PR was closed, it's source branch is `releases/next-release` and target branch is `dev`. 97 | # This indicates that the closed PR was the `Release PR`. 98 | # This job will delete the tag created by AutoVer and the release branch. 99 | clean-up-closed-release: 100 | name: Clean up closed release 101 | if: | 102 | github.event.pull_request.merged == false && 103 | github.event.pull_request.head.ref == 'releases/next-release' && 104 | github.event.pull_request.base.ref == 'dev' 105 | runs-on: ubuntu-latest 106 | steps: 107 | # Checkout a full clone of the repo 108 | - name: Checkout code 109 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 110 | with: 111 | ref: releases/next-release 112 | fetch-depth: 0 113 | # Install .NET8 which is needed for AutoVer 114 | - name: Setup .NET 8.0 115 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 116 | with: 117 | dotnet-version: 8.0.x 118 | # Install AutoVer which is needed to retrieve information about the current release. 119 | - name: Install AutoVer 120 | run: dotnet tool install --global AutoVer --version 0.0.25 121 | # Set up a git user to be able to run git commands later on 122 | - name: Setup Git User 123 | run: | 124 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 125 | git config --global user.name "aws-sdk-dotnet-automation" 126 | # Retrieve the tag name to be deleted 127 | - name: Read Tag Name 128 | id: read-tag-name 129 | run: | 130 | tag=$(autover changelog --tag-name) 131 | echo "TAG=$tag" >> $GITHUB_OUTPUT 132 | # Delete the tag created by AutoVer and the release branch 133 | - name: Clean up 134 | run: | 135 | git fetch origin 136 | git push --delete origin ${{ steps.read-tag-name.outputs.TAG }} 137 | git push origin --delete releases/next-release 138 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/SystemsManagerConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 21 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 22 | using Microsoft.Extensions.Configuration; 23 | using Microsoft.Extensions.Primitives; 24 | 25 | namespace Amazon.Extensions.Configuration.SystemsManager 26 | { 27 | /// 28 | /// 29 | /// An AWS Systems Manager based . 30 | /// 31 | public class SystemsManagerConfigurationProvider : ConfigurationProvider 32 | { 33 | private ISystemsManagerConfigurationSource Source { get; } 34 | private ISystemsManagerProcessor SystemsManagerProcessor { get; } 35 | 36 | private ManualResetEvent ReloadTaskEvent { get; } = new ManualResetEvent(true); 37 | 38 | /// 39 | /// 40 | /// Initializes a new instance with the specified source. 41 | /// 42 | /// The used to retrieve values from AWS Systems Manager Parameter Store 43 | public SystemsManagerConfigurationProvider(SystemsManagerConfigurationSource source) : this(source, new SystemsManagerProcessor(source)) 44 | { 45 | } 46 | 47 | /// 48 | /// 49 | /// Initializes a new instance with the specified source. 50 | /// 51 | /// The used to retrieve values from AWS Systems Manager AppConfig 52 | public SystemsManagerConfigurationProvider(AppConfigConfigurationSource source) : this(source, new AppConfigProcessor(source)) 53 | { 54 | } 55 | 56 | 57 | /// 58 | /// 59 | /// Initializes a new instance with the specified source. 60 | /// 61 | /// The used to retrieve values from AWS Systems Manager 62 | /// The used to retrieve values from AWS Systems Manager 63 | public SystemsManagerConfigurationProvider(ISystemsManagerConfigurationSource source, ISystemsManagerProcessor systemsManagerProcessor) 64 | { 65 | Source = source ?? throw new ArgumentNullException(nameof(source)); 66 | SystemsManagerProcessor = systemsManagerProcessor ?? throw new ArgumentNullException(nameof(systemsManagerProcessor)); 67 | 68 | if (source.ReloadAfter != null) 69 | { 70 | ChangeToken.OnChange(() => 71 | { 72 | var cancellationTokenSource = new CancellationTokenSource(source.ReloadAfter.Value); 73 | var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token); 74 | return cancellationChangeToken; 75 | }, async () => 76 | { 77 | ReloadTaskEvent.Reset(); 78 | try 79 | { 80 | await LoadAsync(true).ConfigureAwait(false); 81 | } 82 | finally 83 | { 84 | ReloadTaskEvent.Set(); 85 | } 86 | }); 87 | } 88 | } 89 | 90 | /// 91 | /// If this configuration provider is currently performing a reload of the config data this method will block until 92 | /// the reload is called. 93 | /// 94 | /// This method is not meant for general use. It is exposed so a AWS Lambda function can wait for the reload to complete 95 | /// before completing the event causing the AWS Lambda compute environment to be frozen. 96 | /// 97 | public void WaitForReloadToComplete(TimeSpan timeout) 98 | { 99 | ReloadTaskEvent.WaitOne(timeout); 100 | } 101 | 102 | /// 103 | /// 104 | /// Loads the AWS Systems Manager Parameters. 105 | /// 106 | public override void Load() => LoadAsync(false).ConfigureAwait(false).GetAwaiter().GetResult(); 107 | 108 | // If 1) reload flag is set to true and 2) OnLoadException handler is not set, 109 | // all exceptions raised during OnReload() will be ignored. 110 | private async Task LoadAsync(bool reload) 111 | { 112 | try 113 | { 114 | var newData = await SystemsManagerProcessor.GetDataAsync().ConfigureAwait(false) ?? new Dictionary(); 115 | 116 | if (!Data.EquivalentTo(newData)) 117 | { 118 | Data = newData; 119 | 120 | OnReload(); 121 | } 122 | } 123 | catch (DuplicateParameterException) // Throw duplicate parameter exception irrespective of whether parameter is optional or not. 124 | { 125 | throw; 126 | } 127 | catch (Exception ex) 128 | { 129 | if (Source.Optional) return; 130 | 131 | var ignoreException = reload; 132 | if (Source.OnLoadException != null) 133 | { 134 | var exceptionContext = new SystemsManagerExceptionContext 135 | { 136 | Provider = this, 137 | Exception = ex, 138 | Reload = reload 139 | }; 140 | Source.OnLoadException(exceptionContext); 141 | ignoreException = exceptionContext.Ignore; 142 | } 143 | 144 | if (!ignoreException) 145 | throw; 146 | } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release 2025-11-17 2 | 3 | ### Amazon.Extensions.Configuration.SystemsManager (7.0.1) 4 | * Update AWS SDK Dependencies 5 | * Removed content type check for app config. We now always try to parse json first 6 | 7 | ## Release 2025-04-28 8 | 9 | ### Amazon.Extensions.Configuration.SystemsManager (7.0.0) 10 | * Updated the .NET SDK dependencies to the latest version GA 4.0.0 11 | 12 | ## Release 2025-04-01 #2 13 | 14 | ### Amazon.Extensions.Configuration.SystemsManager (7.0.0-preview.3) 15 | * Update AWS SDK to Preview 11 16 | * Remove support for .NET 6 17 | * Remove support for netcoreapp3.1 18 | 19 | ## Release 2025-02-28 20 | 21 | ### Amazon.Extensions.Configuration.SystemsManager (7.0.0-preview.2) 22 | * Update .NET SDK dependencies to v4.0.0-preview.8 23 | 24 | ## Release 2025-02-20 25 | 26 | ### Amazon.Extensions.Configuration.SystemsManager (6.3.0) 27 | * Add opt-in JsonOrStringParameterProcessor processor supporting parameters being either strings or JSON 28 | 29 | ## Release 2024-10-17 30 | 31 | ### Amazon.Extensions.Configuration.SystemsManager (7.0.0-preview.1) 32 | * Updated the .NET SDK dependencies to the latest version 4.0.0-preview.4 33 | * Marked project as trimmable 34 | * Added SourceLink support 35 | 36 | ## Release 2024-10-15 37 | 38 | ### Amazon.Extensions.Configuration.SystemsManager (6.2.2) 39 | * Update System.Text.Json for .NET Standard 2.0 target to version 8.0.5. 40 | 41 | ## Release 2024-07-30 42 | 43 | ### Amazon.Extensions.Configuration.SystemsManager (6.2.1) 44 | * For .NET Standard 2.0 target updated the dependency version of System.Text.Json to version 8.0.4 45 | 46 | ## Release 2024-06-19 47 | 48 | ### Amazon.Extensions.Configuration.SystemsManager (6.2.0) 49 | * **Breaking Change:** Throw exception if duplicate Systems Manager parameter keys (case insensitive) are detected irrespective of whether the parameter is optional or not. 50 | 51 | ## Release 2024-04-20 52 | 53 | ### Amazon.Extensions.Configuration.SystemsManager (6.1.1) 54 | * Update User-Agent string 55 | 56 | ## Release 2024-03-29 57 | 58 | ### Amazon.Extensions.Configuration.SystemsManager (6.1.0) 59 | * Pull request [#163](https://github.com/aws/aws-dotnet-extensions-configuration/pull/163) adding .NET 8 target. Thanks [Jon Armen](https://github.com/jon-armen) 60 | 61 | ## Release 2023-09-21 62 | 63 | ### Amazon.Extensions.Configuration.SystemsManager (6.0.0) 64 | * BREAKING CHANGE: Added StringList handling in default parameter processor. 65 | * Note: This is re-releasing 5.1.1 as a major version bump because it introduced a change in behavior if a user was already handling StringList parameters. 66 | 67 | ## Release 2023-09-11 68 | 69 | ### Amazon.Extensions.Configuration.SystemsManager (5.1.1) 70 | * Pull request [#142](https://github.com/aws/aws-dotnet-extensions-configuration/pull/142) Added StringList handling in default parameter processor. Thanks [ArtemMelnychenko](https://github.com/Plazmius) 71 | * Note: We have unlisted and re-released this as 6.0.0. due to the breaking change reported in [#156](https://github.com/aws/aws-dotnet-extensions-configuration/issues/156) 72 | 73 | ## Release 2023-05-31 74 | 75 | ### Amazon.Extensions.Configuration.SystemsManager (5.1.0) 76 | * Pull request [#150](https://github.com/aws/aws-dotnet-extensions-configuration/pull/150) Adding .NET Standard 2.0 support for .NET Framework applications. Thanks [Matthew Heaton](https://github.com/heatonmatthew) 77 | 78 | ## Release 2023-03-21 79 | 80 | ### Amazon.Extensions.Configuration.SystemsManager (5.0.2) 81 | * Fixed an issue where AppConfig JSON configuration with charset information in ContentType was not being parsed. 82 | 83 | ## Release 2023-03-06 84 | 85 | ### Amazon.Extensions.Configuration.SystemsManager (5.0.1) 86 | * Fixed an issue where JsonParameterProcessor was prefixing `:` character to parameter properties while retrieving parameter by name. 87 | 88 | ## Release 2023-02-07 89 | 90 | ### Amazon.Extensions.Configuration.SystemsManager (5.0.0) 91 | * **Breaking Change:** Fixed issue when parsing JSON SSM parameter values to include the relative SSM parameter name to the JSON property names. 92 | 93 | ## Release 2023-02-01 94 | 95 | ### Amazon.Extensions.Configuration.SystemsManager (4.0.1) 96 | * Merged PR [#128](https://github.com/aws/aws-dotnet-extensions-configuration/pull/128) fixed issue parsing AppConfig response when services returns empty response for no changes. Thanks [Tyler Ohlsen](https://github.com/tylerohlsen) 97 | 98 | ## Release 2022-06-28 99 | 100 | ### Amazon.Extensions.Configuration.SystemsManager (4.0.0) 101 | * Merged PR [#99](https://github.com/aws/aws-dotnet-extensions-configuration/pull/99) adding support for using AppConfig Lambda extension. Thanks [mgorski-mg](https://github.com/mgorski-mg) 102 | * Merged PR [#69](https://github.com/aws/aws-dotnet-extensions-configuration/pull/69) making IParameterProcessor used for customizing data from AWS into config values more genenric. Thanks [warej](https://github.com/warej) 103 | * Merged PR [#59](https://github.com/aws/aws-dotnet-extensions-configuration/pull/59) removing dependency on Newtonsoft.JSON and now use System.Text.Json. Thanks [Adilson de Almeida Junior](https://github.com/Adilson) 104 | * Accessing AppConfig data now uses the AWS AppConfig Data APIs StargConfigurationSession and GetLatestConfiguration 105 | * **Breaking Change:** The IncludeParameter, GetKey and GetValue methods were removed from IParameterProcessor in favor of the new generic ProcessParameters method allowing more flexibility in implementations. 106 | * **Breaking Change:** `ClientId` was removed from `AppConfigConfigurationSource`. 107 | * **Breaking Change:** When using AppConfig IAM permissions for `appconfig:StartConfigurationSession` and `appconfig:GetLatestConfiguration` are now required and `appconfig:GetConfiguration` is not longer required. 108 | 109 | ## Release 2021-08-18 110 | 111 | ### Amazon.Extensions.Configuration.SystemsManager (3.0.0) 112 | * Merged PR [#82](https://github.com/aws/aws-dotnet-extensions-configuration/pull/82) Adding [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) support. Thanks [Michał Górski](https://github.com/mgorski-mg) 113 | 114 | ## Release 2021-07-23 115 | 116 | ### Amazon.Extensions.Configuration.SystemsManager (2.1.1) 117 | * Update AWSSDK.Extensions.NETCore.Setup and AWSSDK.SimpleSystemsManagement package references for SSO credential support. 118 | 119 | ## Release 2021-03-30 120 | 121 | ### Amazon.Extensions.Configuration.SystemsManager (2.1.0) 122 | * Update AWS SDK dependencies to version 3.7 123 | 124 | ## Release 2020-10-09 125 | 126 | ### Amazon.Extensions.Configuration.SystemsManager (2.0.0) 127 | * Merged PR [#75](https://github.com/aws/aws-dotnet-extensions-configuration/pull/75) Update AWS SDK dependencies to 3.5. Thanks [Doug Ferris](https://github.com/doug-ferris) 128 | * Update Newtonsoft.Json to version 12. 129 | 130 | ## Release 2019-07-23 131 | 132 | ### Amazon.Extensions.Configuration.SystemsManager (1.2.0) 133 | * Merged PR [#41](https://github.com/aws/aws-dotnet-extensions-configuration/pull/41) Prevents stripping the first char when the Path is "/". Thanks [Ken Hundley](https://github.com/KenHundley) 134 | * Merged PR [#44](https://github.com/aws/aws-dotnet-extensions-configuration/pull/44) Added `Filters` property to SystemsManagerConfigurationSource. Thanks [Zahy Hwary](https://github.com/zahycs) 135 | 136 | ## Release 2019-04-09 137 | 138 | ### Amazon.Extensions.Configuration.SystemsManager (1.1.1) 139 | * Added AWS Secrets Manager support. Thanks [Ken Hundley](https://github.com/KenHundley) 140 | * Only trigger OnReload when values have changed. 141 | * Update version of the AWS SDK for .NET to 3.3.100 142 | 143 | ## Release 2019-01-11 144 | 145 | ### Amazon.Extensions.Configuration.SystemsManager (1.0.1) 146 | * Made Analysers for development/local only 147 | 148 | 149 | ## Release 2019-12-17 150 | 151 | ### Amazon.Extensions.Configuration.SystemsManager (1.0.0) 152 | * Initial release 153 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Net.Http; 20 | using System.Text.Json; 21 | using System.Threading; 22 | using System.Threading.Tasks; 23 | using Amazon.AppConfigData; 24 | using Amazon.AppConfigData.Model; 25 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 26 | 27 | namespace Amazon.Extensions.Configuration.SystemsManager.AppConfig 28 | { 29 | // Types that own disposable fields should be disposable. This warning is okay to ignore because 30 | // the instance of IAmazonAppConfigData is meant to last for the length of the application. 31 | #pragma warning disable CA1001 32 | public class AppConfigProcessor : ISystemsManagerProcessor 33 | #pragma warning restore CA1001 34 | { 35 | private AppConfigConfigurationSource Source { get; } 36 | private IDictionary LastConfig { get; set; } 37 | 38 | private string PollConfigurationToken { get; set; } 39 | private DateTime NextAllowedPollTime { get; set; } 40 | 41 | private SemaphoreSlim _lastConfigLock = new SemaphoreSlim(1, 1); 42 | private const int _lastConfigLockTimeout = 3000; 43 | 44 | private Uri _lambdaExtensionUri; 45 | private HttpClient _lambdaExtensionClient; 46 | 47 | private IAmazonAppConfigData _appConfigDataClient; 48 | 49 | public AppConfigProcessor(AppConfigConfigurationSource source) 50 | { 51 | Source = source; 52 | 53 | if (source.ApplicationId == null) throw new ArgumentNullException(nameof(source.ApplicationId)); 54 | if (source.EnvironmentId == null) throw new ArgumentNullException(nameof(source.EnvironmentId)); 55 | if (source.ConfigProfileId == null) throw new ArgumentNullException(nameof(source.ConfigProfileId)); 56 | 57 | // Check to see if the function is being run inside Lambda. If it is not because it is running in a integ test or in the 58 | // the .NET Lambda Test Tool the Lambda extension is not available and fallback to using the AppConfig service directly. 59 | if(Source.UseLambdaExtension && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME"))) 60 | { 61 | var port = Environment.GetEnvironmentVariable("AWS_APPCONFIG_EXTENSION_HTTP_PORT") ?? "2772"; 62 | _lambdaExtensionUri = new Uri($"http://localhost:{port}/applications/{Source.ApplicationId}/environments/{Source.EnvironmentId}/configurations/{Source.ConfigProfileId}"); 63 | _lambdaExtensionClient = source.CustomHttpClientForLambdaExtension ?? new HttpClient(); 64 | } 65 | else 66 | { 67 | if(source.AwsOptions != null) 68 | { 69 | _appConfigDataClient = source.AwsOptions.CreateServiceClient(); 70 | } 71 | else 72 | { 73 | _appConfigDataClient = new AmazonAppConfigDataClient(); 74 | } 75 | 76 | if (_appConfigDataClient is AmazonAppConfigDataClient impl) 77 | { 78 | impl.BeforeRequestEvent += ServiceClientAppender.ServiceClientBeforeRequestEvent; 79 | } 80 | } 81 | } 82 | 83 | public async Task> GetDataAsync() 84 | { 85 | 86 | if(_appConfigDataClient != null) 87 | { 88 | return await GetDataFromServiceAsync().ConfigureAwait(false) 89 | ?? new Dictionary(); 90 | } 91 | else 92 | { 93 | return await GetDataFromLambdaExtensionAsync().ConfigureAwait(false) 94 | ?? new Dictionary(); 95 | } 96 | } 97 | 98 | private async Task> GetDataFromLambdaExtensionAsync() 99 | { 100 | using (var response = await _lambdaExtensionClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, _lambdaExtensionUri)).ConfigureAwait(false)) 101 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 102 | { 103 | LastConfig = ParseConfig(response.Content.Headers.ContentType.ToString(), stream); 104 | } 105 | 106 | return LastConfig; 107 | } 108 | 109 | private async Task> GetDataFromServiceAsync() 110 | { 111 | if(await _lastConfigLock.WaitAsync(_lastConfigLockTimeout).ConfigureAwait(false)) 112 | { 113 | try 114 | { 115 | if(DateTime.UtcNow < NextAllowedPollTime) 116 | { 117 | return LastConfig; 118 | } 119 | 120 | if (string.IsNullOrEmpty(PollConfigurationToken)) 121 | { 122 | this.PollConfigurationToken = await GetInitialConfigurationTokenAsync(_appConfigDataClient).ConfigureAwait(false); 123 | } 124 | 125 | var request = new GetLatestConfigurationRequest 126 | { 127 | ConfigurationToken = PollConfigurationToken 128 | }; 129 | 130 | var response = await _appConfigDataClient.GetLatestConfigurationAsync(request).ConfigureAwait(false); 131 | PollConfigurationToken = response.NextPollConfigurationToken; 132 | NextAllowedPollTime = DateTime.UtcNow.AddSeconds(response.NextPollIntervalInSeconds ?? 0); 133 | 134 | // Configuration is empty when the last received config is the latest 135 | // so only attempt to parse the AppConfig response when it is not empty 136 | if (response.ContentLength > 0) 137 | { 138 | LastConfig = ParseConfig(response.ContentType, response.Configuration); 139 | } 140 | } 141 | finally 142 | { 143 | _lastConfigLock.Release(); 144 | } 145 | } 146 | else 147 | { 148 | return LastConfig; 149 | } 150 | 151 | return LastConfig; 152 | } 153 | 154 | private async Task GetInitialConfigurationTokenAsync(IAmazonAppConfigData appConfigClient) 155 | { 156 | var request = new StartConfigurationSessionRequest 157 | { 158 | ApplicationIdentifier = Source.ApplicationId, 159 | EnvironmentIdentifier = Source.EnvironmentId, 160 | ConfigurationProfileIdentifier = Source.ConfigProfileId 161 | }; 162 | 163 | return (await appConfigClient.StartConfigurationSessionAsync(request).ConfigureAwait(false)).InitialConfigurationToken; 164 | } 165 | 166 | internal static IDictionary ParseConfig(string contentType, Stream configuration) 167 | { 168 | try 169 | { 170 | return JsonConfigurationParser.Parse(configuration); 171 | } 172 | catch (JsonException ex) 173 | { 174 | throw new InvalidOperationException( 175 | $"Failed to parse AppConfig content as JSON. Content-Type was '{contentType}'. {ex.Message}", 176 | ex); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigForLambdaExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Amazon.Extensions.Configuration.SystemsManager; 18 | using Amazon.Extensions.Configuration.SystemsManager.AppConfig; 19 | using Amazon.Runtime; 20 | using Amazon.AppConfigData; 21 | 22 | // ReSharper disable once CheckNamespace 23 | namespace Microsoft.Extensions.Configuration 24 | { 25 | /// 26 | /// Extension methods for registering with . 27 | /// 28 | public static class AppConfigForLambdaExtensions 29 | { 30 | /// 31 | /// Adds an that reads configuration values from AWS Systems Manager AWS AppConfig using the AWS Lambda Extension. 32 | /// For more information about using the AppConfig Lambda Extension checkout the AppConfig user guide. 33 | /// 34 | /// 35 | /// The AppConfig Lambda extension reloads configuration data using the interval set by the AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS environment variable 36 | /// or 45 seconds if not set. The .NET configuration provider will refresh at the same interval plus a 5 second buffer for the extension to complete its update 37 | /// process. 38 | /// 39 | /// The to add to. 40 | /// The AppConfig application id. 41 | /// The AppConfig environment id. 42 | /// The AppConfig configuration profile id. 43 | /// Whether the AWS Systems Manager AppConfig is optional. 44 | /// cannot be null 45 | /// cannot be null 46 | /// cannot be null 47 | /// The . 48 | public static IConfigurationBuilder AddAppConfigUsingLambdaExtension(this IConfigurationBuilder builder, string applicationId, string environmentId, string configProfileId, bool optional) 49 | { 50 | if (applicationId == null) throw new ArgumentNullException(nameof(applicationId)); 51 | if (environmentId == null) throw new ArgumentNullException(nameof(environmentId)); 52 | if (configProfileId == null) throw new ArgumentNullException(nameof(configProfileId)); 53 | 54 | return builder.AddAppConfigUsingLambdaExtension(ConfigureSource(applicationId, environmentId, configProfileId, optional: optional)); 55 | } 56 | 57 | /// 58 | /// Adds an that reads configuration values from AWS Systems Manager AWS AppConfig using the AWS Lambda Extension. 59 | /// For more information about using the AppConfig Lambda Extension checkout the AppConfig user guide. 60 | /// 61 | /// 62 | /// The AppConfig Lambda extension reloads configuration data using the interval set by the AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS environment variable 63 | /// or 45 seconds if not set. The .NET configuration provider will refresh at the same interval plus a 5 second buffer for the extension to complete its update 64 | /// process. 65 | /// 66 | /// The to add to. 67 | /// The AppConfig application id. 68 | /// The AppConfig environment id. 69 | /// The AppConfig configuration profile id. 70 | /// cannot be null 71 | /// cannot be null 72 | /// cannot be null 73 | /// The . 74 | public static IConfigurationBuilder AddAppConfigUsingLambdaExtension(this IConfigurationBuilder builder, string applicationId, string environmentId, string configProfileId) 75 | { 76 | if (applicationId == null) throw new ArgumentNullException(nameof(applicationId)); 77 | if (environmentId == null) throw new ArgumentNullException(nameof(environmentId)); 78 | if (configProfileId == null) throw new ArgumentNullException(nameof(configProfileId)); 79 | 80 | return builder.AddAppConfigUsingLambdaExtension(ConfigureSource(applicationId, environmentId, configProfileId)); 81 | } 82 | 83 | /// 84 | /// Adds an that reads configuration values from AWS Systems Manager AWS AppConfig using the AWS Lambda Extension. 85 | /// For more information about using the AppConfig Lambda Extension checkout the AppConfig user guide. 86 | /// 87 | /// 88 | /// The AppConfig Lambda extension reloads configuration data using the interval set by the AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS environment variable 89 | /// or 45 seconds if not set. The .NET configuration provider will refresh at the same interval plus a 5 second buffer for the extension to complete its update 90 | /// process. 91 | /// 92 | /// The to add to. 93 | /// Configuration source. 94 | /// cannot be null 95 | /// . cannot be null 96 | /// . cannot be null 97 | /// . cannot be null 98 | /// The . 99 | public static IConfigurationBuilder AddAppConfigUsingLambdaExtension(this IConfigurationBuilder builder, AppConfigConfigurationSource source) 100 | { 101 | if (source == null) throw new ArgumentNullException(nameof(source)); 102 | if (source.ApplicationId == null) throw new ArgumentNullException(nameof(source.ApplicationId)); 103 | if (source.EnvironmentId == null) throw new ArgumentNullException(nameof(source.EnvironmentId)); 104 | if (source.ConfigProfileId == null) throw new ArgumentNullException(nameof(source.ConfigProfileId)); 105 | 106 | // Create a specific instance of AmazonAppConfigClient that is configured to make calls to the endpoint setup by the AppConfig Lambda layer. 107 | source.UseLambdaExtension = true; 108 | 109 | if(!source.ReloadAfter.HasValue) 110 | { 111 | // default polling duration is 45 seconds https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html 112 | var reloadAfter = 45; 113 | 114 | // Since the user is using Lambda extension which automatically refreshes the data default to the configuration provider defaulting 115 | // to reload at the rate the extension reloads plus 5 second buffer. 116 | var reloadAfterStr = Environment.GetEnvironmentVariable("AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS"); 117 | if (reloadAfterStr != null && !int.TryParse(reloadAfterStr, out reloadAfter)) 118 | { 119 | throw new ArgumentException("Environment variable AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS used for computing ReloadAfter is not set to a valid integer"); 120 | } 121 | 122 | reloadAfter += 5; 123 | source.ReloadAfter = TimeSpan.FromSeconds(reloadAfter); 124 | } 125 | 126 | return builder.Add(source); 127 | } 128 | 129 | private static AppConfigConfigurationSource ConfigureSource( 130 | string applicationId, 131 | string environmentId, 132 | string configProfileId, 133 | bool optional = false 134 | ) 135 | { 136 | return new AppConfigConfigurationSource 137 | { 138 | ApplicationId = applicationId, 139 | EnvironmentId = environmentId, 140 | ConfigProfileId = configProfileId, 141 | Optional = optional 142 | }; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /test/Amazon.Extensions.Configuration.SystemsManager.Integ/AppConfigEndToEndTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | using Amazon.AppConfig; 10 | using Amazon.AppConfig.Model; 11 | using Amazon.Extensions.NETCore.Setup; 12 | using Microsoft.Extensions.Configuration; 13 | using Xunit; 14 | 15 | namespace Amazon.Extensions.Configuration.SystemsManager.Integ 16 | { 17 | public class AppConfigEndToEndTests 18 | { 19 | IAmazonAppConfig _appConfigClient = new AmazonAppConfigClient(RegionEndpoint.USWest2); 20 | 21 | [Fact] 22 | public async Task RefreshConfiguration() 23 | { 24 | var configSettings = new Dictionary 25 | { 26 | { "key1", "value1" }, 27 | { "key2", "value2" } 28 | }; 29 | (string applicationId, string environmentId, string configProfileId) = await CreateAppConfigResourcesAsync("RefreshWebAppTest", configSettings); 30 | try 31 | { 32 | var builder = new ConfigurationBuilder() 33 | .AddAppConfig(applicationId, environmentId, configProfileId, new AWSOptions {Region = RegionEndpoint.USWest2 }, TimeSpan.FromSeconds(5)); 34 | 35 | var configuration = builder.Build(); 36 | 37 | Assert.Equal("value1", configuration["key1"]); 38 | 39 | const string newValue = "newValue1"; 40 | configSettings["key1"] = newValue; 41 | var versionNumber = await CreateNewHostedConfig(applicationId, configProfileId, configSettings); 42 | await PerformDeploymentAsync(applicationId, environmentId, configProfileId, versionNumber); 43 | 44 | for(int i = 0; i < 10; i++) 45 | { 46 | // Wait for ConfigProvider to perform the reload 47 | await Task.Delay(TimeSpan.FromSeconds(10)); 48 | var value = configuration["key1"]; 49 | if(string.Equals(newValue, value)) 50 | { 51 | break; 52 | } 53 | } 54 | 55 | Assert.Equal(newValue, configuration["key1"]); 56 | } 57 | finally 58 | { 59 | await CleanupAppConfigResourcesAsync(applicationId, environmentId, configProfileId); 60 | } 61 | } 62 | 63 | [Fact] 64 | public async Task JsonWithCharsetConfiguration() 65 | { 66 | var configSettings = new Dictionary 67 | { 68 | { "key1", "value1" }, 69 | { "key2", "value2" } 70 | }; 71 | (string applicationId, string environmentId, string configProfileId) = await CreateAppConfigResourcesAsync("JsonWithCharsetConfiguration", configSettings, "application/json; charset=utf-8"); 72 | try 73 | { 74 | var builder = new ConfigurationBuilder() 75 | .AddAppConfig(applicationId, environmentId, configProfileId, new AWSOptions { Region = RegionEndpoint.USWest2 }, TimeSpan.FromSeconds(5)); 76 | 77 | var configuration = builder.Build(); 78 | 79 | Assert.Equal("value1", configuration["key1"]); 80 | } 81 | finally 82 | { 83 | await CleanupAppConfigResourcesAsync(applicationId, environmentId, configProfileId); 84 | } 85 | } 86 | 87 | [Fact] 88 | public async Task AppConfigWithOctetStreamContentType() 89 | { 90 | var configSettings = new Dictionary 91 | { 92 | { "key1", "value1" }, 93 | { "key2", "value2" }, 94 | { "database:connectionString", "server=localhost;database=test" }, 95 | { "api:timeout", "30" } 96 | }; 97 | 98 | // Create AppConfig with application/octet-stream content type (simulates Parameter Store backend) 99 | (string applicationId, string environmentId, string configProfileId) = 100 | await CreateAppConfigResourcesAsync("OctetStreamTest", configSettings, "application/octet-stream"); 101 | 102 | try 103 | { 104 | var builder = new ConfigurationBuilder() 105 | .AddAppConfig(applicationId, environmentId, configProfileId, 106 | new AWSOptions { Region = RegionEndpoint.USWest2 }, 107 | TimeSpan.FromSeconds(5)); 108 | 109 | var configuration = builder.Build(); 110 | 111 | // Verify configuration loads correctly despite application/octet-stream content type 112 | Assert.Equal("value1", configuration["key1"]); 113 | Assert.Equal("value2", configuration["key2"]); 114 | Assert.Equal("server=localhost;database=test", configuration["database:connectionString"]); 115 | Assert.Equal("30", configuration["api:timeout"]); 116 | } 117 | finally 118 | { 119 | await CleanupAppConfigResourcesAsync(applicationId, environmentId, configProfileId); 120 | } 121 | } 122 | 123 | private async Task CleanupAppConfigResourcesAsync(string applicationId, string environmentId, string configProfileId) 124 | { 125 | await _appConfigClient.DeleteEnvironmentAsync(new DeleteEnvironmentRequest {ApplicationId = applicationId, EnvironmentId = environmentId }); 126 | 127 | var listHostConfigResponse = await _appConfigClient.ListHostedConfigurationVersionsAsync(new ListHostedConfigurationVersionsRequest 128 | { 129 | ApplicationId = applicationId, 130 | ConfigurationProfileId = configProfileId 131 | }); 132 | 133 | foreach(var item in listHostConfigResponse.Items) 134 | { 135 | await _appConfigClient.DeleteHostedConfigurationVersionAsync(new DeleteHostedConfigurationVersionRequest 136 | { 137 | ApplicationId = item.ApplicationId, 138 | ConfigurationProfileId = item.ConfigurationProfileId, 139 | VersionNumber = item.VersionNumber 140 | }); 141 | } 142 | 143 | await _appConfigClient.DeleteConfigurationProfileAsync(new DeleteConfigurationProfileRequest 144 | { 145 | ApplicationId = applicationId, 146 | ConfigurationProfileId = configProfileId 147 | }); 148 | 149 | await _appConfigClient.DeleteApplicationAsync(new DeleteApplicationRequest {ApplicationId = applicationId }); 150 | } 151 | 152 | 153 | private async Task<(string applicationId, string environmentId, string configProfileId)> CreateAppConfigResourcesAsync(string seedName, IDictionary configs, string contentType = "application/json") 154 | { 155 | var nameSuffix = DateTime.Now.Ticks; 156 | 157 | var createAppResponse = await _appConfigClient.CreateApplicationAsync(new CreateApplicationRequest { Name = seedName + "-" + nameSuffix }); 158 | 159 | var createConfigResponse = await _appConfigClient.CreateConfigurationProfileAsync(new CreateConfigurationProfileRequest 160 | { 161 | ApplicationId = createAppResponse.Id, 162 | Name = seedName + "-" + nameSuffix, 163 | LocationUri = "hosted" 164 | }); 165 | 166 | var createEnvironmentResponse = await _appConfigClient.CreateEnvironmentAsync(new CreateEnvironmentRequest 167 | { 168 | ApplicationId = createAppResponse.Id, 169 | Name = seedName + "-" + nameSuffix 170 | }); 171 | 172 | var versionNumber = await CreateNewHostedConfig(createAppResponse.Id, createConfigResponse.Id, configs, contentType); 173 | await PerformDeploymentAsync(createAppResponse.Id, createEnvironmentResponse.Id, createConfigResponse.Id, versionNumber); 174 | 175 | return (createAppResponse.Id, createEnvironmentResponse.Id, createConfigResponse.Id); 176 | } 177 | 178 | private async Task CreateNewHostedConfig(string applicationId, string configProfileId, IDictionary configs, string contentType = "application/json") 179 | { 180 | var json = JsonSerializer.Serialize(configs); 181 | 182 | var createHostedresponse = await _appConfigClient.CreateHostedConfigurationVersionAsync(new CreateHostedConfigurationVersionRequest 183 | { 184 | ApplicationId = applicationId, 185 | ConfigurationProfileId = configProfileId, 186 | ContentType = contentType, 187 | Content = new MemoryStream(UTF8Encoding.UTF8.GetBytes(json)) 188 | }); 189 | 190 | return createHostedresponse.VersionNumber.ToString(); 191 | } 192 | 193 | private async Task PerformDeploymentAsync(string applicationId, string environmentId, string configProfileId, string configVersionNumber, bool waitForDeployment = true) 194 | { 195 | var deploymentStrategyId = await GetDeploymentStrategyId(); 196 | var deploymentResponse = await _appConfigClient.StartDeploymentAsync(new StartDeploymentRequest 197 | { 198 | ApplicationId = applicationId, 199 | EnvironmentId = environmentId, 200 | ConfigurationProfileId = configProfileId, 201 | ConfigurationVersion = configVersionNumber, 202 | DeploymentStrategyId = deploymentStrategyId 203 | }); 204 | 205 | if(waitForDeployment) 206 | { 207 | await WaitForDeploymentAsync(applicationId, environmentId); 208 | } 209 | } 210 | 211 | private async Task WaitForDeploymentAsync(string applicationId, string environmentId) 212 | { 213 | var getRequest = new GetEnvironmentRequest {ApplicationId = applicationId, EnvironmentId = environmentId }; 214 | GetEnvironmentResponse getResponse; 215 | do 216 | { 217 | await Task.Delay(2000); 218 | getResponse = await _appConfigClient.GetEnvironmentAsync(getRequest); 219 | } while (getResponse.State == EnvironmentState.DEPLOYING); 220 | } 221 | 222 | private async Task GetDeploymentStrategyId() 223 | { 224 | const string integTestDeploymentStrategyName = "IntegTestFast"; 225 | 226 | var paginator = _appConfigClient.Paginators.ListDeploymentStrategies(new ListDeploymentStrategiesRequest()); 227 | await foreach(var response in paginator.Responses) 228 | { 229 | var strategy = response.Items.FirstOrDefault(x => string.Equals(x.Name, integTestDeploymentStrategyName)); 230 | 231 | if (strategy != null) 232 | { 233 | return strategy.Id; 234 | } 235 | } 236 | 237 | var createResponse = await _appConfigClient.CreateDeploymentStrategyAsync(new CreateDeploymentStrategyRequest 238 | { 239 | Name = integTestDeploymentStrategyName, 240 | DeploymentDurationInMinutes = 1, 241 | FinalBakeTimeInMinutes = 0, 242 | GrowthFactor = 100, 243 | ReplicateTo = ReplicateTo.NONE 244 | }); 245 | 246 | return createResponse.Id; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/Amazon.Extensions.Configuration.SystemsManager/SystemsManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | using System; 17 | using Amazon.Extensions.Configuration.SystemsManager; 18 | using Amazon.Extensions.Configuration.SystemsManager.Internal; 19 | using Amazon.Extensions.NETCore.Setup; 20 | 21 | // ReSharper disable once CheckNamespace 22 | namespace Microsoft.Extensions.Configuration 23 | { 24 | /// 25 | /// Extension methods for registering with . 26 | /// 27 | public static class SystemsManagerExtensions 28 | { 29 | /// 30 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 31 | /// 32 | /// The to add to. 33 | /// used to create an AWS Systems Manager Client connection 34 | /// The path that variable names must start with. The path will be removed from the variable names. 35 | /// Whether the AWS Systems Manager Parameters are optional. 36 | /// Initiate reload after TimeSpan 37 | /// cannot be null 38 | /// cannot be null 39 | /// The . 40 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, AWSOptions awsOptions, bool optional, TimeSpan reloadAfter) 41 | { 42 | if (path == null) throw new ArgumentNullException(nameof(path)); 43 | if (awsOptions == null) throw new ArgumentNullException(nameof(awsOptions)); 44 | 45 | return builder.AddSystemsManager(ConfigureSource(path, awsOptions, optional, reloadAfter)); 46 | } 47 | 48 | /// 49 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 50 | /// 51 | /// The to add to. 52 | /// used to create an AWS Systems Manager Client connection 53 | /// The path that variable names must start with. The path will be removed from the variable names. 54 | /// Whether the AWS Systems Manager Parameters are optional. 55 | /// cannot be null 56 | /// cannot be null 57 | /// The . 58 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, AWSOptions awsOptions, bool optional) 59 | { 60 | if (path == null) throw new ArgumentNullException(nameof(path)); 61 | if (awsOptions == null) throw new ArgumentNullException(nameof(awsOptions)); 62 | 63 | return builder.AddSystemsManager(ConfigureSource(path, awsOptions, optional)); 64 | } 65 | 66 | /// 67 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 68 | /// 69 | /// The to add to. 70 | /// used to create an AWS Systems Manager Client connection 71 | /// The path that variable names must start with. The path will be removed from the variable names. 72 | /// Initiate reload after TimeSpan 73 | /// cannot be null 74 | /// cannot be null 75 | /// The . 76 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, AWSOptions awsOptions, TimeSpan reloadAfter) 77 | { 78 | if (path == null) throw new ArgumentNullException(nameof(path)); 79 | if (awsOptions == null) throw new ArgumentNullException(nameof(awsOptions)); 80 | 81 | return builder.AddSystemsManager(ConfigureSource(path, awsOptions, reloadAfter: reloadAfter)); 82 | } 83 | /// 84 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 85 | /// 86 | /// The to add to. 87 | /// used to create an AWS Systems Manager Client connection 88 | /// The path that variable names must start with. The path will be removed from the variable names. 89 | /// cannot be null 90 | /// cannot be null 91 | /// The . 92 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, AWSOptions awsOptions) 93 | { 94 | if (path == null) throw new ArgumentNullException(nameof(path)); 95 | if (awsOptions == null) throw new ArgumentNullException(nameof(awsOptions)); 96 | 97 | return builder.AddSystemsManager(ConfigureSource(path, awsOptions)); 98 | } 99 | 100 | /// 101 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 102 | /// 103 | /// The to add to. 104 | /// The path that variable names must start with. The path will be removed from the variable names. 105 | /// Whether the AWS Systems Manager Parameters are optional. 106 | /// Initiate reload after TimeSpan 107 | /// cannot be null 108 | /// The . 109 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, bool optional, TimeSpan reloadAfter) 110 | { 111 | if (path == null) throw new ArgumentNullException(nameof(path)); 112 | 113 | return builder.AddSystemsManager(ConfigureSource(path, null, optional, reloadAfter)); 114 | } 115 | 116 | /// 117 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 118 | /// 119 | /// The to add to. 120 | /// The path that variable names must start with. The path will be removed from the variable names. 121 | /// Whether the AWS Systems Manager Parameters are optional. 122 | /// cannot be null 123 | /// The . 124 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, bool optional) 125 | { 126 | if (path == null) throw new ArgumentNullException(nameof(path)); 127 | 128 | return builder.AddSystemsManager(ConfigureSource(path, null, optional)); 129 | } 130 | 131 | /// 132 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 133 | /// 134 | /// The to add to. 135 | /// The path that variable names must start with. The path will be removed from the variable names. 136 | /// Initiate reload after TimeSpan 137 | /// cannot be null 138 | /// The . 139 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path, TimeSpan reloadAfter) 140 | { 141 | if (path == null) throw new ArgumentNullException(nameof(path)); 142 | 143 | return builder.AddSystemsManager(ConfigureSource(path, null, reloadAfter: reloadAfter)); 144 | } 145 | 146 | /// 147 | /// Adds an that reads configuration values from AWS Systems Manager Parameter Store with a specified path. 148 | /// 149 | /// The to add to. 150 | /// The path that variable names must start with. The path will be removed from the variable names. 151 | /// cannot be null 152 | /// The . 153 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, string path) 154 | { 155 | if (path == null) throw new ArgumentNullException(nameof(path)); 156 | 157 | return builder.AddSystemsManager(ConfigureSource(path, null)); 158 | } 159 | 160 | /// 161 | /// Adds an that reads configuration values from AWS Systems Manager Parameter variables with a specified path. 162 | /// 163 | /// The to add to. 164 | /// Configures the source. 165 | /// cannot be null 166 | /// . cannot be null 167 | /// The . 168 | public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, Action configureSource) 169 | { 170 | if (configureSource == null) throw new ArgumentNullException(nameof(configureSource)); 171 | 172 | var source = new SystemsManagerConfigurationSource(); 173 | configureSource(source); 174 | 175 | if (string.IsNullOrWhiteSpace(source.Path)) throw new ArgumentNullException(nameof(source.Path)); 176 | if (source.AwsOptions != null) return builder.Add(source); 177 | 178 | source.AwsOptions = AwsOptionsProvider.GetAwsOptions(builder); 179 | return builder.Add(source); 180 | } 181 | 182 | private static Action ConfigureSource(string path, AWSOptions awsOptions, bool optional = false, TimeSpan? reloadAfter = null) 183 | { 184 | return configurationSource => 185 | { 186 | configurationSource.Path = path; 187 | configurationSource.AwsOptions = awsOptions; 188 | configurationSource.Optional = optional; 189 | configurationSource.ReloadAfter = reloadAfter; 190 | }; 191 | } 192 | } 193 | } --------------------------------------------------------------------------------