Skip to content
Merged
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
2 changes: 2 additions & 0 deletions NonaConfig.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@
</Folder>
<Folder Name="/migrator/" />
<Folder Name="/migrator/src/">
<Project Path="migrator/src/AwsParameterStoreMigrator/Migrator.AwsParameterStore.csproj" />
<Project Path="migrator/src/ConfigMigrator.Core/Migrator.Core.csproj" />
<Project Path="migrator/src/FirebaseRemoteConfigMigrator/Migrator.FirebaseRemoteConfig.csproj" />
</Folder>
<Folder Name="/migrator/tests/">
<Project Path="migrator/tests/AwsParameterStoreMigrator.Tests/Migrator.AwsParameterStore.Tests.csproj" />
<Project Path="migrator/tests/FirebaseRemoteConfigMigrator.Tests/Migrator.FirebaseRemoteConfig.Tests.csproj" />
</Folder>
</Solution>
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ so change whatever you like.
- [Client Libraries](#client-libraries)
- [API](#api)
- [Docker Compose](#docker-compose)
- [Migrate from Firebase Remote Config](#migrate-from-firebase-remote-config)
- [Migrate existing configuration](#migrate-existing-configuration)
- [Performance](#performance)
- [Architecture](#architecture)

Expand Down Expand Up @@ -287,7 +287,7 @@ docker run -d \

---

## Migrate from Firebase Remote Config
## Migrate existing configuration

The Nona CLI includes a built-in Firebase Remote Config migration command that imports your existing parameters using a migration config file.

Expand All @@ -303,6 +303,16 @@ nona migrate firebase \

See [`cli/src/Nona.Cli/README.md`](cli/src/Nona.Cli/README.md) for the full CLI reference.

The CLI can also import AWS Parameter Store `String` values referenced by an ECS task definition:

```bash
nona migrate parameter-store \
--task-definition ./task-definition.json \
--environment production \
--project backend-service \
--dry-run
```

---

## Performance
Expand Down
61 changes: 61 additions & 0 deletions cli/src/Nona.Cli/Core/CliValueResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@ public ConnectionResolutionResult ResolveConnection(
return ConnectionResolutionResult.Ok(new NonaCliConnectionOptions(baseUrl, token));
}

public MigrationConnectionResolution ResolveMigrationConnection(
string? parsedBaseUrl,
string? parsedToken,
string? parsedEmail,
string? parsedPassword)
{
var baseUrl = BaseUrl(parsedBaseUrl);
var token = Token(parsedToken);
var email = Email(parsedEmail);
var password = Password(parsedPassword);

if (string.IsNullOrWhiteSpace(baseUrl) && session is not null && !session.IsExpired)
baseUrl = session.BaseUrl;

if (string.IsNullOrWhiteSpace(token)
&& string.IsNullOrWhiteSpace(email)
&& session is not null
&& !session.IsExpired
&& !string.IsNullOrWhiteSpace(baseUrl)
&& session.MatchesBaseUrl(baseUrl))
{
token = session.Token;
}

return new MigrationConnectionResolution(baseUrl, token, email, password);
}

public string[] BuildFirebaseArgs(
string? config,
bool dryRun,
Expand All @@ -63,6 +90,34 @@ public string[] BuildFirebaseArgs(
return [.. args];
}

public string[] BuildParameterStoreArgs(
string taskDefinition,
string environment,
string? region,
string? profile,
bool dryRun,
string? baseUrl,
string? project,
string? token,
string? email,
string? password)
{
var args = new List<string>
{
"--task-definition", taskDefinition,
"--environment", environment
};
if (region is not null) { args.Add("--region"); args.Add(region); }
if (profile is not null) { args.Add("--profile"); args.Add(profile); }
if (dryRun) args.Add("--dry-run");
if (baseUrl is not null) { args.Add("--base-url"); args.Add(baseUrl); }
if (project is not null) { args.Add("--project"); args.Add(project); }
if (token is not null) { args.Add("--token"); args.Add(token); }
if (email is not null) { args.Add("--email"); args.Add(email); }
if (password is not null) { args.Add("--password"); args.Add(password); }
return [.. args];
}

public static string? NormalizeConfigSettingName(string name) =>
name.ToLowerInvariant() switch
{
Expand All @@ -86,3 +141,9 @@ internal sealed record ConnectionResolutionResult(bool Success, string? Error, N
public static ConnectionResolutionResult Ok(NonaCliConnectionOptions connection) => new(true, null, connection);
public static ConnectionResolutionResult Fail(string error) => new(false, error, null);
}

internal sealed record MigrationConnectionResolution(
string? BaseUrl,
string? Token,
string? Email,
string? Password);
11 changes: 11 additions & 0 deletions cli/src/Nona.Cli/Migrate/Commands/ParameterStoreMigrateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Nona.Migrator.AwsParameterStore;

namespace Nona.Cli.Migrate.Commands;

internal sealed record ParameterStoreMigrateCommand(string[] Args);

internal sealed class ParameterStoreMigrateCommandHandler
{
public Task<int> HandleAsync(ParameterStoreMigrateCommand command, CancellationToken cancellationToken)
=> AwsParameterStoreMigrationCommand.RunAsync(command.Args, cancellationToken);
}
87 changes: 75 additions & 12 deletions cli/src/Nona.Cli/Migrate/MigrateCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public Command Build()
{
var migrate = new Command("migrate", "Run migration commands.");
migrate.AddCommand(BuildFirebase());
migrate.AddCommand(BuildParameterStore());
return migrate;
}

Expand All @@ -35,27 +36,89 @@ private Command BuildFirebase()
cmd.AddOption(passwordOpt);
cmd.Handler = CommandHandler.Create(async (InvocationContext ic) =>
{
var baseUrl = ctx.Resolver.BaseUrl(ic.ParseResult.GetValueForOption(baseUrlOpt));
var connection = ctx.Resolver.ResolveMigrationConnection(
ic.ParseResult.GetValueForOption(baseUrlOpt),
ic.ParseResult.GetValueForOption(tokenOpt),
ic.ParseResult.GetValueForOption(emailOpt),
ic.ParseResult.GetValueForOption(passwordOpt));
var project = ctx.Resolver.Project(ic.ParseResult.GetValueForOption(projectOpt));
var email = ctx.Resolver.Email(ic.ParseResult.GetValueForOption(emailOpt));
var password = ctx.Resolver.Password(ic.ParseResult.GetValueForOption(passwordOpt));
var token = ctx.Resolver.Token(ic.ParseResult.GetValueForOption(tokenOpt));

if (string.IsNullOrWhiteSpace(token) && string.IsNullOrWhiteSpace(email) &&
ctx.Session is not null && !ctx.Session.IsExpired &&
baseUrl is not null && ctx.Session.MatchesBaseUrl(baseUrl))
{
token = ctx.Session.Token;
}

var args = ctx.Resolver.BuildFirebaseArgs(
ic.ParseResult.GetValueForOption(configOpt),
ic.ParseResult.GetValueForOption(dryRunOpt),
baseUrl, project, token, email, password);
connection.BaseUrl,
project,
connection.Token,
connection.Email,
connection.Password);

ic.ExitCode = await handler.HandleAsync(new FirebaseMigrateCommand(args), ic.GetCancellationToken());
});

return cmd;
}

private Command BuildParameterStore()
{
var taskDefinitionOpt = new Option<string>(
"--task-definition",
"Local ECS task definition JSON path.")
{
IsRequired = true
};
var environmentOpt = new Option<string>(
"--environment",
"Target Nona environment.")
{
IsRequired = true
};
var regionOpt = new Option<string?>("--region", "AWS region for non-ARN Parameter Store references.");
var profileOpt = new Option<string?>("--profile", "AWS shared credentials profile.");
var dryRunOpt = new Option<bool>("--dry-run", "Preview changes without applying them.");
var baseUrlOpt = new Option<string?>(["--base-url", "--api-url"], "Nona base URL.");
var projectOpt = new Option<string?>(["--project", "--project-name"], "Nona project name.");
var tokenOpt = new Option<string?>(["--token", "--bearer-token"], "Admin bearer token.");
var emailOpt = new Option<string?>("--email", "Admin email used by the migrator when no token is supplied.");
var passwordOpt = new Option<string?>("--password", "Admin password used by the migrator when no token is supplied.");

var handler = new ParameterStoreMigrateCommandHandler();
var cmd = new Command("parameter-store", "Import AWS Parameter Store values referenced by an ECS task definition into Nona.");
cmd.AddOption(taskDefinitionOpt);
cmd.AddOption(environmentOpt);
cmd.AddOption(regionOpt);
cmd.AddOption(profileOpt);
cmd.AddOption(dryRunOpt);
cmd.AddOption(baseUrlOpt);
cmd.AddOption(projectOpt);
cmd.AddOption(tokenOpt);
cmd.AddOption(emailOpt);
cmd.AddOption(passwordOpt);
cmd.Handler = CommandHandler.Create(async (InvocationContext ic) =>
{
var connection = ctx.Resolver.ResolveMigrationConnection(
ic.ParseResult.GetValueForOption(baseUrlOpt),
ic.ParseResult.GetValueForOption(tokenOpt),
ic.ParseResult.GetValueForOption(emailOpt),
ic.ParseResult.GetValueForOption(passwordOpt));
var project = ctx.Resolver.Project(ic.ParseResult.GetValueForOption(projectOpt));

var args = ctx.Resolver.BuildParameterStoreArgs(
ic.ParseResult.GetValueForOption(taskDefinitionOpt)!,
ic.ParseResult.GetValueForOption(environmentOpt)!,
ic.ParseResult.GetValueForOption(regionOpt),
ic.ParseResult.GetValueForOption(profileOpt),
ic.ParseResult.GetValueForOption(dryRunOpt),
connection.BaseUrl,
project,
connection.Token,
connection.Email,
connection.Password);

ic.ExitCode = await handler.HandleAsync(
new ParameterStoreMigrateCommand(args),
ic.GetCancellationToken());
});

return cmd;
}
}
3 changes: 2 additions & 1 deletion cli/src/Nona.Cli/Nona.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackAsTool>true</PackAsTool>
<ToolCommandName>nona</ToolCommandName>
<PackageId>Nona.Cli</PackageId>
<Description>CLI for Nona key management and Firebase Remote Config migrations.</Description>
<Description>CLI for Nona administration and configuration migrations.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>nona;cli;config;migration</PackageTags>
</PropertyGroup>
Expand Down Expand Up @@ -38,6 +38,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\migrator\src\AwsParameterStoreMigrator\Migrator.AwsParameterStore.csproj" />
<ProjectReference Include="..\..\..\migrator\src\ConfigMigrator.Core\Migrator.Core.csproj" />
<ProjectReference Include="..\..\..\migrator\src\FirebaseRemoteConfigMigrator\Migrator.FirebaseRemoteConfig.csproj" />
</ItemGroup>
Expand Down
15 changes: 15 additions & 0 deletions cli/src/Nona.Cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ Run a Firebase Remote Config migration:
nona migrate firebase --config ./nona.migration.json --base-url https://nona.example.com --email admin@example.com --password secret
```

Import string parameters referenced by an ECS task definition:

```bash
nona migrate parameter-store \
--task-definition ./task-definition.json \
--environment production \
--profile my-aws-profile \
--base-url https://nona.example.com \
--project backend-service \
--token "$NONA_ADMIN_TOKEN" \
--dry-run
```

The Parameter Store migrator uses ECS `secrets[].name` as the Nona key and imports only parameters whose AWS type is exactly `String`. Entries are written as server-scoped text. `SecureString`, `StringList`, Secrets Manager references, and plaintext ECS `environment` entries are skipped. Run without `--dry-run` to apply the upserts.

## On-Prem

For on-prem deployments, point the CLI at the local API host:
Expand Down
Loading
Loading