Add config output

This commit is contained in:
Micaiah Martin
2026-06-04 12:05:51 -06:00
parent 3c7606fdc6
commit ed3ff6ce09
6 changed files with 84 additions and 15 deletions

View File

@@ -1,4 +1,5 @@
using Bit.SelfHost.Deployments;
using Bit.SelfHost.Commands;
using Bit.SelfHost.Deployments;
using Bit.SelfHost.Setup;
using Xunit;
@@ -69,6 +70,26 @@ public class StandardTopologyTests
=> Assert.Equal(13, new StandardDeployment().BuildTopology(CtxWith(true, true)).Count);
}
public class ConfigRedactionTests
{
[Theory]
[InlineData("SA_PASSWORD")]
[InlineData("globalSettings__sqlServer__connectionString")]
[InlineData("globalSettings__identityServer__certificatePassword")]
[InlineData("globalSettings__internalIdentityKey")]
[InlineData("globalSettings__mail__smtp__password")]
[InlineData("BW_INSTALLATION_KEY")]
public void Secrets_are_redacted(string key) => Assert.True(ConfigCommand.IsSecret(key));
[Theory]
[InlineData("globalSettings__mail__smtp__host")]
[InlineData("globalSettings__mail__smtp__username")]
[InlineData("globalSettings__installation__id")]
[InlineData("BW_DOMAIN")]
[InlineData("DATABASE")]
public void Non_secrets_are_shown(string key) => Assert.False(ConfigCommand.IsSecret(key));
}
public class StandardAssetBuilderTests
{
private static StandardConfig Config() => new() { Url = "http://localhost", GenerateNginxConfig = false };

View File

@@ -7,19 +7,17 @@ public static class ConfigCommand
{
public static Command Build()
{
var cmd = new Command("config", "Get or set deployment configuration (e.g. config key=value).");
var cmd = new Command("config", "Show the current config, or get/set a key (config key=value).");
var assignment = new Argument<string?>("assignment")
{ Description = "key=value to set, or key to get. Omit with --show to list.", Arity = ArgumentArity.ZeroOrOne };
{ Description = "key=value to set, or key to get. Omit to print the current config.", Arity = ArgumentArity.ZeroOrOne };
var deployment = Cli.DeploymentOption();
var root = new Option<string>("--root")
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
var show = new Option<bool>("--show") { Description = "Show resolved config files." };
cmd.Arguments.Add(assignment);
cmd.Options.Add(deployment);
cmd.Options.Add(root);
cmd.Options.Add(show);
cmd.SetAction(parseResult =>
{
@@ -28,15 +26,8 @@ public static class ConfigCommand
var rootDir = parseResult.GetValue(root)!;
var arg = parseResult.GetValue(assignment);
if (parseResult.GetValue(show) || arg is null)
{
Console.WriteLine($"{kind} deployment config files (under {rootDir}):");
Console.WriteLine(kind == DeploymentKind.Standard
? " config.yml (structural; run `bwsh apply`)\n" +
" env/global.override.env (SMTP / admin / globalSettings; run `bwsh apply`)"
: " settings.env (all BW_* / globalSettings__*; run `bwsh apply`)");
return 0;
}
if (arg is null)
return PrintConfig(dep, kind, rootDir);
var eq = arg.IndexOf('=');
var key = eq >= 0 ? arg[..eq] : arg;
@@ -74,4 +65,47 @@ public static class ConfigCommand
return cmd;
}
/// <summary>Prints the deployment's on-disk config files, redacting secret values.</summary>
private static int PrintConfig(IDeployment dep, DeploymentKind kind, string rootDir)
{
var printed = false;
foreach (var rel in dep.ConfigFiles)
{
var path = Path.Combine(rootDir, rel);
if (!File.Exists(path)) continue;
printed = true;
Console.WriteLine($"# {rel}");
foreach (var raw in File.ReadLines(path))
{
var line = raw.TrimEnd();
var eq = line.IndexOf('=');
if (eq > 0 && !line.TrimStart().StartsWith('#'))
{
var key = line[..eq];
Console.WriteLine(IsSecret(key.Trim()) ? $"{key}=<redacted>" : line);
}
else
{
Console.WriteLine(line); // YAML (config.yml), comments, blanks
}
}
Console.WriteLine();
}
if (!printed)
{
Console.Error.WriteLine($"No {kind} deployment config found under {rootDir}. Run `install` first.");
return 4;
}
return 0;
}
/// <summary>Values worth hiding: anything that ends in a key, or names a password/connection string.</summary>
internal static bool IsSecret(string key)
{
var k = key.ToLowerInvariant();
return k.Contains("password") || k.Contains("connectionstring") || k.EndsWith("key");
}
}

View File

@@ -38,7 +38,10 @@ public interface IDeployment
/// <summary>The container graph the Orchestrator brings up.</summary>
IReadOnlyList<ServiceSpec> BuildTopology(InstallContext ctx);
/// <summary>Resolve a `config set key=value` key to the file it lives in + the action to apply it.</summary>
/// <summary>Relative paths of the on-disk config files, for `config` to print (secrets redacted).</summary>
IReadOnlyList<string> ConfigFiles { get; }
/// <summary>Resolve a `config key=value` key to the file it lives in + the action to apply it.</summary>
bool TryResolveConfigKey(string key, out ConfigBinding binding);
}

View File

@@ -20,6 +20,8 @@ public sealed class LiteDeployment : IDeployment
public string InstalledMarker => SettingsFile;
public IReadOnlyList<string> ConfigFiles { get; } = [SettingsFile];
public string ResolveUrl(string root)
{
var env = new Dictionary<string, string>();

View File

@@ -12,6 +12,14 @@ public sealed class StandardDeployment : IDeployment
public string InstalledMarker => "config.yml";
public IReadOnlyList<string> ConfigFiles { get; } =
[
"config.yml",
"env/global.override.env",
"env/mssql.override.env",
"env/key-connector.override.env",
];
public string ResolveUrl(string root) => Setup.StandardConfig.Load(root).Url;
public IReadOnlyList<NetworkSpec> Networks { get; } =

View File

@@ -66,6 +66,7 @@ This is how you change config (e.g. add SMTP under the manifest's `config:` bloc
```bash
dotnet run -- status # health, versions, and vault URL
dotnet run -- config # print the current config (secrets redacted); config key=value to set
dotnet run -- logs identity # a service's logs; --export bundles all to a zip
dotnet run -- update # pull latest images and recreate changed services
dotnet run -- backup # snapshot config + secrets + database to a .tar.gz