Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ROADMAP.md

## Local secrets (real credentials, never commit)
seed-secrets.local.sh
fixtures/

## .NET
bin/
Expand Down
35 changes: 35 additions & 0 deletions DGates.AwsSecretsManager.Examples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleExample", "src\ConsoleExample\ConsoleExample.csproj", "{E28243EB-4BD5-413D-9FE2-38F4C19D07B9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcExample", "src\MvcExample\MvcExample.csproj", "{6C5F9C04-E31B-4233-80C8-DB43632F66B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcExample.Core", "src\MvcExample.Core\MvcExample.Core.csproj", "{16F52BBF-890F-4392-B82E-689FA6FEDE6B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcExample.Tests", "tests\MvcExample.Tests\MvcExample.Tests.csproj", "{12FE25F6-666C-43D5-B6EC-E1E874705D92}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcExample.Infrastructure", "src\MvcExample.Infrastructure\MvcExample.Infrastructure.csproj", "{8F93F9CE-F722-4039-B4F1-F4360D9ED5AB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B70FFAF2-D2B1-48FA-BCB4-EF80F9166706}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{F8AC86F4-A8EB-44FC-99E7-3374B0CFC796}"
ProjectSection(SolutionItems) = preProject
docs\LOCAL_DEV.md = docs\LOCAL_DEV.md
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,11 +31,30 @@ Global
{E28243EB-4BD5-413D-9FE2-38F4C19D07B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E28243EB-4BD5-413D-9FE2-38F4C19D07B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E28243EB-4BD5-413D-9FE2-38F4C19D07B9}.Release|Any CPU.Build.0 = Release|Any CPU
{6C5F9C04-E31B-4233-80C8-DB43632F66B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C5F9C04-E31B-4233-80C8-DB43632F66B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C5F9C04-E31B-4233-80C8-DB43632F66B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C5F9C04-E31B-4233-80C8-DB43632F66B1}.Release|Any CPU.Build.0 = Release|Any CPU
{16F52BBF-890F-4392-B82E-689FA6FEDE6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16F52BBF-890F-4392-B82E-689FA6FEDE6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F52BBF-890F-4392-B82E-689FA6FEDE6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F52BBF-890F-4392-B82E-689FA6FEDE6B}.Release|Any CPU.Build.0 = Release|Any CPU
{12FE25F6-666C-43D5-B6EC-E1E874705D92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12FE25F6-666C-43D5-B6EC-E1E874705D92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12FE25F6-666C-43D5-B6EC-E1E874705D92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12FE25F6-666C-43D5-B6EC-E1E874705D92}.Release|Any CPU.Build.0 = Release|Any CPU
{8F93F9CE-F722-4039-B4F1-F4360D9ED5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F93F9CE-F722-4039-B4F1-F4360D9ED5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F93F9CE-F722-4039-B4F1-F4360D9ED5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F93F9CE-F722-4039-B4F1-F4360D9ED5AB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB83C080-7ED8-4646-8154-63E66301BD5B}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F8AC86F4-A8EB-44FC-99E7-3374B0CFC796} = {B70FFAF2-D2B1-48FA-BCB4-EF80F9166706}
EndGlobalSection
EndGlobal
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Minimal wiring demo — reads a typed secret from LocalStack, demonstrates cache
### MvcExample _(coming soon)_
ASP.NET MVC app using `SecretsManagerServiceFactory` for DI. Retrieves an OpenWeatherMap API key from Secrets Manager and displays live weather data.

> **Platform note:** MvcExample targets classic ASP.NET MVC 5 (`System.Web.Mvc`), which requires IIS/IIS Express to host. It builds on any platform with the .NET SDK, but only runs on Windows (Visual Studio or IIS Express). See [docs/LOCAL_DEV.md](docs/LOCAL_DEV.md#running-mvcexample-without-docker) for a Docker-free fallback.

## Local Development

See [docs/LOCAL_DEV.md](docs/LOCAL_DEV.md) for prerequisites and setup steps.
Expand All @@ -32,6 +34,8 @@ docker-compose up

Then run ConsoleExample from Visual Studio or the `dotnet` CLI. _(MvcExample coming soon.)_

No Docker available? See [Running MvcExample without Docker](docs/LOCAL_DEV.md#running-mvcexample-without-docker) for a JSON-file fallback.

## Library

- Library repo: https://github.com/dgates82/DGates.AwsSecretsManager
Expand Down
2 changes: 1 addition & 1 deletion docker/seed-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ create_or_update_secret "dev/ConsoleExample/DbConfig" \
'{"Server":"localhost","Database":"ExampleDb","Username":"app_user","Password":"s3cr3t!"}'

# MvcExample — placeholder OpenWeatherMap key (replace with real key for live data)
create_or_update_secret "dev/DGates.AwsSecretsManager.Examples/OpenWeatherMap" \
create_or_update_secret "dev/MvcExample/OpenWeatherMap" \
'{"Url":"https://api.openweathermap.org/data/2.5/weather","Key":"YOUR_KEY_HERE"}'

echo "Done. Seeded secrets are available at $ENDPOINT (region $REGION)."
91 changes: 91 additions & 0 deletions docs/LOCAL_DEV.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Local Development

## Prerequisites

| Tool | Purpose |
|---|---|
| [Docker Desktop](https://www.docker.com/products/docker-desktop/) | Runs LocalStack and seeds secrets |
Expand All @@ -10,43 +11,133 @@
> The examples target **net48** but build with any recent .NET SDK.

## Start LocalStack

```bash
docker compose up
```

This starts LocalStack and runs the init container which seeds all secrets
automatically. You should see `Seeding complete.` in the logs before starting
an example project.

To run in the background:

```bash
docker compose up -d
```

## Run ConsoleExample

```bash
dotnet run --project src/ConsoleExample/ConsoleExample.csproj
```

Or open `DGates.AwsSecretsManager.Examples.sln` in Visual Studio and run
`ConsoleExample`.

## Running examples without Docker

Both examples can run without LocalStack/Docker using the library's `LocalJsonFallbackPath`
setting, which reads secrets from a local JSON file instead of calling AWS/LocalStack. This is
useful if Docker isn't available in your environment — for example, a Windows VM nested inside
another hypervisor without virtualization passthrough (Docker Desktop's Linux-container support
needs that passthrough, which many nested-VM setups don't expose), or any machine without
Docker installed at all.

#### ConsoleExample

1. Create a fixture JSON file at `src/ConsoleExample/fixtures/console-secrets.json`:
```json
{
"dev/ConsoleExample/DbConfig": {
"Server": "localhost",
"Database": "ExampleDb",
"Username": "app_user",
"Password": "s3cr3t!"
}
}
```
This file is copied to the build output directory automatically (see
`ConsoleExample.csproj`), so the relative path below resolves correctly whether you run
via `dotnet run` or the built exe.
2. Add `LocalJsonFallbackPath` to `App.config`'s `appSettings`:
```xml
<add key="LocalJsonFallbackPath" value="fixtures\console-secrets.json" />
```
3. Run as normal:
```bash
dotnet run --project src/ConsoleExample/ConsoleExample.csproj
```

### MvcExample

1. Create a fixture JSON file, e.g. `fixtures/mvc-secrets.json`:
```json
{
"dev/MvcExample/OpenWeatherMap": {
"Url": "https://api.openweathermap.org/data/2.5/weather",
"Key": "YOUR_KEY_HERE"
}
}
```

Unlike ConsoleExample, IIS/IIS Express serves a classic ASP.NET app from its project's physical
root (where `Web.config` lives), not from a `bin\` output folder — so the relative path should
resolve directly against the fixture file's source location, with no copy-to-output step needed.
`MvcExample.csproj` still copies the file to `bin\` defensively (`Condition="Exists(...)"`, so it's
a no-op until the file exists), in case that assumption is wrong. **This hasn't been verified
against a real IIS Express run yet** — confirm on Windows which path actually resolves, and correct
this section if the assumption above turns out to be wrong.

2. Point `Web.config` at it:
```xml
<appSettings>
<add key="LocalJsonFallbackPath" value="fixtures\mvc-secrets.json" />
</appSettings>
```
3. Run via IIS Express or Visual Studio as normal. MvcExample only hosts on Windows regardless
of backend, since it requires IIS/IIS Express — see [CLAUDE.md](../CLAUDE.md) for details.

### Limitations

This fallback doesn't exercise the same code path as a real Secrets Manager call (no cache TTL
expiry against a live backend, no rotation handling), so it's not a substitute for the
LocalStack-backed integration coverage the library's test suite and a Docker-enabled ConsoleExample
run already provide. Treat it as "can I boot and see the app work," not "have I verified
Secrets Manager integration."

## Tearing down

```bash
docker compose down -v
```

## Secrets seeded by default

| Secret name | Purpose |
|---|---|
| `dev/ConsoleExample/DbConfig` | Fake DB credentials used by ConsoleExample |
| `dev/MvcExample/OpenWeatherMap` | Placeholder OpenWeatherMap API URL/key used by MvcExample |

## Re-seeding manually

If LocalStack is already running and you need to re-seed:

```bash
docker/seed-secrets.sh
```

> The seed script is idempotent — re-running it is safe.

## Testing with a real OpenWeatherMap key

`docker/seed-secrets.sh` seeds a placeholder key by default. If you want to test against the
real OpenWeatherMap API, create `docker/seed-secrets.local.sh` with your real key (this file is
gitignored and never committed) and run it after `docker compose up` instead of the default
seed script.

## Inspecting secrets

```bash
# List all secrets
aws --endpoint-url=http://localhost:4566 secretsmanager list-secrets
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleExample/ConsoleExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<None Update="App.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="fixtures\console-secrets.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
3 changes: 2 additions & 1 deletion src/ConsoleExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ private static async Task Main()
var settings = new SecretsManagerSettings
{
ServiceUrl = ConfigurationManager.AppSettings["AWSServiceURL"],
Region = ConfigurationManager.AppSettings["AWSRegion"],
Region = ConfigurationManager.AppSettings["AWSRegion"],
LocalJsonFallbackPath = ConfigurationManager.AppSettings["LocalJsonFallbackPath"]
};

var secretName = ConfigurationManager.AppSettings["Secrets:DbConfigName"];
Expand Down
15 changes: 15 additions & 0 deletions src/MvcExample.Core/ApiKeyEvaluator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace MvcExample.Core
{
public static class ApiKeyEvaluator
{
public const string PlaceholderKey = "YOUR_KEY_HERE";

public static bool IsPlaceholder(string key)
{
return string.IsNullOrWhiteSpace(key)
|| string.Equals(key.Trim(), PlaceholderKey, StringComparison.OrdinalIgnoreCase);
}
}
}
10 changes: 10 additions & 0 deletions src/MvcExample.Core/GeocodeResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace MvcExample.Core
{
public class GeocodeResult
{
public double Lat { get; set; }
public double Lon { get; set; }
public string Name { get; set; }
public string Country { get; set; }
}
}
14 changes: 14 additions & 0 deletions src/MvcExample.Core/MvcExample.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<RootNamespace>MvcExample.Core</RootNamespace>
<AssemblyName>MvcExample.Core</AssemblyName>
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions src/MvcExample.Core/OpenWeatherMapSecret.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace MvcExample.Core
{
public class OpenWeatherMapSecret
{
public string Url { get; set; }
public string Key { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/MvcExample.Core/WeatherApiResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace MvcExample.Core
{
public class WeatherApiResponse
{
public double Temperature { get; set; }
public double FeelsLike { get; set; }
public string Description { get; set; }
public int Humidity { get; set; }
public double Pressure { get; set; }
public double WindSpeed { get; set; }
}
}
17 changes: 17 additions & 0 deletions src/MvcExample.Core/WeatherApiUrlBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace MvcExample.Core
{
public static class WeatherApiUrlBuilder
{
public static string BuildGeocodeUrl(string apiKey, string cityName)
{
return $"http://api.openweathermap.org/geo/1.0/direct?q={Uri.EscapeDataString(cityName)}&limit=1&appid={apiKey}";
}

public static string BuildWeatherUrl(OpenWeatherMapSecret secret, double lat, double lon)
{
return $"{secret.Url}?lat={lat}&lon={lon}&appid={secret.Key}&units=imperial";
}
}
}
34 changes: 34 additions & 0 deletions src/MvcExample.Core/WeatherResponseParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Linq;
using Newtonsoft.Json.Linq;

namespace MvcExample.Core
{
public static class WeatherResponseParser
{
public static GeocodeResult[] ParseGeocode(string json)
{
var array = JArray.Parse(json);
return array.Select(t => new GeocodeResult
{
Lat = (double)t["lat"],
Lon = (double)t["lon"],
Name = (string)t["name"],
Country = (string)t["country"]
}).ToArray();
}

public static WeatherApiResponse ParseWeather(string json)
{
var root = JObject.Parse(json);
return new WeatherApiResponse
{
Temperature = (double)root["main"]["temp"],
FeelsLike = (double)root["main"]["feels_like"],
Description = (string)root["weather"][0]["description"],
Humidity = (int)root["main"]["humidity"],
Pressure = (double)root["main"]["pressure"],
WindSpeed = (double)root["wind"]["speed"]
};
}
}
}
21 changes: 21 additions & 0 deletions src/MvcExample.Core/WeatherViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace MvcExample.Core
{
public class WeatherViewModel
{
public string CityName { get; set; }

public bool IsConfigured { get; set; } = true;
public string ConfigurationMessage { get; set; }

public string ErrorMessage { get; set; }

public bool HasResult { get; set; }
public string Location { get; set; }
public double Temperature { get; set; }
public double FeelsLike { get; set; }
public string Description { get; set; }
public int Humidity { get; set; }
public double Pressure { get; set; }
public double WindSpeed { get; set; }
}
}
Loading
Loading