[sdk/dotnet] Warn when a secret config is read as a non-secret (#7079)
This commit is contained in:
parent
480173a57f
commit
34a40d2b10
|
@ -17,6 +17,7 @@
|
|||
- Warn when a secret config is read as a non-secret
|
||||
[#6896](https://github.com/pulumi/pulumi/pull/6896)
|
||||
[#7078](https://github.com/pulumi/pulumi/pull/7078)
|
||||
[#7079](https://github.com/pulumi/pulumi/pull/7079)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
|
@ -783,6 +783,185 @@ namespace Pulumi.Automation.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigSecretWarnings()
|
||||
{
|
||||
var program = PulumiFn.Create(() =>
|
||||
{
|
||||
var config = new Config();
|
||||
|
||||
config.Get("plainstr1");
|
||||
config.Require("plainstr2");
|
||||
config.GetSecret("plainstr3");
|
||||
config.RequireSecret("plainstr4");
|
||||
|
||||
config.GetBoolean("plainbool1");
|
||||
config.RequireBoolean("plainbool2");
|
||||
config.GetSecretBoolean("plainbool3");
|
||||
config.RequireSecretBoolean("plainbool4");
|
||||
|
||||
config.GetInt32("plainint1");
|
||||
config.RequireInt32("plainint2");
|
||||
config.GetSecretInt32("plainint3");
|
||||
config.RequireSecretInt32("plainint4");
|
||||
|
||||
config.GetObject<System.Text.Json.JsonElement>("plainobj1");
|
||||
config.RequireObject<System.Text.Json.JsonElement>("plainobj2");
|
||||
config.GetSecretObject<System.Text.Json.JsonElement>("plainobj3");
|
||||
config.RequireSecretObject<System.Text.Json.JsonElement>("plainobj4");
|
||||
|
||||
config.Get("str1");
|
||||
config.Require("str2");
|
||||
config.GetSecret("str3");
|
||||
config.RequireSecret("str4");
|
||||
|
||||
config.GetBoolean("bool1");
|
||||
config.RequireBoolean("bool2");
|
||||
config.GetSecretBoolean("bool3");
|
||||
config.RequireSecretBoolean("bool4");
|
||||
|
||||
config.GetInt32("int1");
|
||||
config.RequireInt32("int2");
|
||||
config.GetSecretInt32("int3");
|
||||
config.RequireSecretInt32("int4");
|
||||
|
||||
config.GetObject<System.Text.Json.JsonElement>("obj1");
|
||||
config.RequireObject<System.Text.Json.JsonElement>("obj2");
|
||||
config.GetSecretObject<System.Text.Json.JsonElement>("obj3");
|
||||
config.RequireSecretObject<System.Text.Json.JsonElement>("obj4");
|
||||
});
|
||||
var projectName = "inline_dotnet";
|
||||
var stackName = $"inline_dotnet{GetTestSuffix()}";
|
||||
using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program)
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string?>()
|
||||
{
|
||||
["PULUMI_CONFIG_PASSPHRASE"] = "test",
|
||||
}
|
||||
});
|
||||
|
||||
var config = new Dictionary<string, ConfigValue>()
|
||||
{
|
||||
{ "plainstr1", new ConfigValue("1") },
|
||||
{ "plainstr2", new ConfigValue("2") },
|
||||
{ "plainstr3", new ConfigValue("3") },
|
||||
{ "plainstr4", new ConfigValue("4") },
|
||||
{ "plainbool1", new ConfigValue("true") },
|
||||
{ "plainbool2", new ConfigValue("true") },
|
||||
{ "plainbool3", new ConfigValue("true") },
|
||||
{ "plainbool4", new ConfigValue("true") },
|
||||
{ "plainint1", new ConfigValue("1") },
|
||||
{ "plainint2", new ConfigValue("2") },
|
||||
{ "plainint3", new ConfigValue("3") },
|
||||
{ "plainint4", new ConfigValue("4") },
|
||||
{ "plainobj1", new ConfigValue("{}") },
|
||||
{ "plainobj2", new ConfigValue("{}") },
|
||||
{ "plainobj3", new ConfigValue("{}") },
|
||||
{ "plainobj4", new ConfigValue("{}") },
|
||||
{ "str1", new ConfigValue("1", isSecret: true) },
|
||||
{ "str2", new ConfigValue("2", isSecret: true) },
|
||||
{ "str3", new ConfigValue("3", isSecret: true) },
|
||||
{ "str4", new ConfigValue("4", isSecret: true) },
|
||||
{ "bool1", new ConfigValue("true", isSecret: true) },
|
||||
{ "bool2", new ConfigValue("true", isSecret: true) },
|
||||
{ "bool3", new ConfigValue("true", isSecret: true) },
|
||||
{ "bool4", new ConfigValue("true", isSecret: true) },
|
||||
{ "int1", new ConfigValue("1", isSecret: true) },
|
||||
{ "int2", new ConfigValue("2", isSecret: true) },
|
||||
{ "int3", new ConfigValue("3", isSecret: true) },
|
||||
{ "int4", new ConfigValue("4", isSecret: true) },
|
||||
{ "obj1", new ConfigValue("{}", isSecret: true) },
|
||||
{ "obj2", new ConfigValue("{}", isSecret: true) },
|
||||
{ "obj3", new ConfigValue("{}", isSecret: true) },
|
||||
{ "obj4", new ConfigValue("{}", isSecret: true) },
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await stack.SetAllConfigAsync(config);
|
||||
|
||||
// pulumi preview
|
||||
await RunCommand<PreviewResult, PreviewOptions>(stack.PreviewAsync, "preview");
|
||||
|
||||
// pulumi up
|
||||
await RunCommand<UpResult, UpOptions>(stack.UpAsync, "up");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await stack.Workspace.RemoveStackAsync(stackName);
|
||||
}
|
||||
|
||||
static async Task<T> RunCommand<T, TOptions>(Func<TOptions, CancellationToken, Task<T>> func, string command)
|
||||
where TOptions : UpdateOptions, new()
|
||||
{
|
||||
var expectedWarnings = new string[]
|
||||
{
|
||||
"Configuration 'inline_dotnet:str1' value is a secret; use `GetSecret` instead of `Get`",
|
||||
"Configuration 'inline_dotnet:str2' value is a secret; use `RequireSecret` instead of `Require`",
|
||||
"Configuration 'inline_dotnet:bool1' value is a secret; use `GetSecretBoolean` instead of `GetBoolean`",
|
||||
"Configuration 'inline_dotnet:bool2' value is a secret; use `RequireSecretBoolean` instead of `RequireBoolean`",
|
||||
"Configuration 'inline_dotnet:int1' value is a secret; use `GetSecretInt32` instead of `GetInt32`",
|
||||
"Configuration 'inline_dotnet:int2' value is a secret; use `RequireSecretInt32` instead of `RequireInt32`",
|
||||
"Configuration 'inline_dotnet:obj1' value is a secret; use `GetSecretObject` instead of `GetObject`",
|
||||
"Configuration 'inline_dotnet:obj2' value is a secret; use `RequireSecretObject` instead of `RequireObject`",
|
||||
};
|
||||
|
||||
// These keys should not be in any warning messages.
|
||||
var unexpectedWarnings = new string[]
|
||||
{
|
||||
"plainstr1",
|
||||
"plainstr2",
|
||||
"plainstr3",
|
||||
"plainstr4",
|
||||
"plainbool1",
|
||||
"plainbool2",
|
||||
"plainbool3",
|
||||
"plainbool4",
|
||||
"plainint1",
|
||||
"plainint2",
|
||||
"plainint3",
|
||||
"plainint4",
|
||||
"plainobj1",
|
||||
"plainobj2",
|
||||
"plainobj3",
|
||||
"plainobj4",
|
||||
"str3",
|
||||
"str4",
|
||||
"bool3",
|
||||
"bool4",
|
||||
"int3",
|
||||
"int4",
|
||||
"obj3",
|
||||
"obj4",
|
||||
};
|
||||
|
||||
var events = new List<DiagnosticEvent>();
|
||||
|
||||
var result = await func(new TOptions()
|
||||
{
|
||||
OnEvent = @event =>
|
||||
{
|
||||
if (@event.DiagnosticEvent?.Severity == "warning")
|
||||
{
|
||||
events.Add(@event.DiagnosticEvent);
|
||||
}
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
foreach (var expected in expectedWarnings)
|
||||
{
|
||||
Assert.Contains(events, @event => @event.Message.Contains(expected));
|
||||
}
|
||||
|
||||
foreach (var unexpected in unexpectedWarnings)
|
||||
{
|
||||
Assert.DoesNotContain(events, @event => @event.Message.Contains(unexpected));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidStack : Stack
|
||||
{
|
||||
[Output("exp_static")]
|
||||
|
|
|
@ -47,7 +47,8 @@ namespace Pulumi.Automation
|
|||
request.Project,
|
||||
request.Stack,
|
||||
request.Parallel,
|
||||
request.DryRun);
|
||||
request.DryRun,
|
||||
request.ConfigSecretKeys);
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
this._callerContext.CancellationToken,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Pulumi
|
||||
|
@ -54,30 +56,44 @@ namespace Pulumi
|
|||
=> Output.CreateSecret(value);
|
||||
|
||||
|
||||
private string? GetImpl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
{
|
||||
var fullKey = FullKey(key);
|
||||
if (use != null && Deployment.InternalInstance.IsConfigSecret(fullKey))
|
||||
{
|
||||
Debug.Assert(insteadOf != null);
|
||||
Log.Warn($"Configuration '{fullKey}' value is a secret; use `{use}` instead of `{insteadOf}`");
|
||||
}
|
||||
return Deployment.InternalInstance.GetConfig(fullKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value by its key, or <see langword="null"/> if it doesn't exist.
|
||||
/// </summary>
|
||||
public string? Get(string key)
|
||||
=> Deployment.InternalInstance.GetConfig(FullKey(key));
|
||||
=> GetImpl(key, nameof(GetSecret));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value by its key, marking it as a secret, or <see
|
||||
/// langword="null"/> if it doesn't exist.
|
||||
/// </summary>
|
||||
public Output<string>? GetSecret(string key)
|
||||
=> MakeClassSecret(Get(key));
|
||||
=> MakeClassSecret(GetImpl(key));
|
||||
|
||||
private bool? GetBooleanImpl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
{
|
||||
var v = GetImpl(key, use, insteadOf);
|
||||
return v == null ? default(bool?) :
|
||||
v == "true" ? true :
|
||||
v == "false" ? false : throw new ConfigTypeException(FullKey(key), v, nameof(Boolean));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as a boolean, by its key, or null if it doesn't exist.
|
||||
/// If the configuration value isn't a legal boolean, this function will throw an error.
|
||||
/// </summary>
|
||||
public bool? GetBoolean(string key)
|
||||
{
|
||||
var v = Get(key);
|
||||
return v == null ? default(bool?) :
|
||||
v == "true" ? true :
|
||||
v == "false" ? false : throw new ConfigTypeException(FullKey(key), v, nameof(Boolean));
|
||||
}
|
||||
=> GetBooleanImpl(key, nameof(GetSecretBoolean));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as a boolean, by its key, making it as a secret or
|
||||
|
@ -85,15 +101,11 @@ namespace Pulumi
|
|||
/// function will throw an error.
|
||||
/// </summary>
|
||||
public Output<bool>? GetSecretBoolean(string key)
|
||||
=> MakeStructSecret(GetBoolean(key));
|
||||
=> MakeStructSecret(GetBooleanImpl(key));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as a number, by its key, or null if it doesn't exist.
|
||||
/// If the configuration value isn't a legal number, this function will throw an error.
|
||||
/// </summary>
|
||||
public int? GetInt32(string key)
|
||||
private int? GetInt32Impl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
{
|
||||
var v = Get(key);
|
||||
var v = GetImpl(key, use, insteadOf);
|
||||
return v == null
|
||||
? default(int?)
|
||||
: int.TryParse(v, out var result)
|
||||
|
@ -101,23 +113,25 @@ namespace Pulumi
|
|||
: throw new ConfigTypeException(FullKey(key), v, nameof(Int32));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as a number, by its key, or null if it doesn't exist.
|
||||
/// If the configuration value isn't a legal number, this function will throw an error.
|
||||
/// </summary>
|
||||
public int? GetInt32(string key)
|
||||
=> GetInt32Impl(key, nameof(GetSecretInt32));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as a number, by its key, marking it as a secret
|
||||
/// or null if it doesn't exist.
|
||||
/// If the configuration value isn't a legal number, this function will throw an error.
|
||||
/// </summary>
|
||||
public Output<int>? GetSecretInt32(string key)
|
||||
=> MakeStructSecret(GetInt32(key));
|
||||
=> MakeStructSecret(GetInt32Impl(key));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as an object, by its key, or null if it doesn't
|
||||
/// exist. This works by taking the value associated with <paramref name="key"/> and passing
|
||||
/// it to <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>.
|
||||
/// </summary>
|
||||
[return: MaybeNull]
|
||||
public T GetObject<T>(string key)
|
||||
private T GetObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
{
|
||||
var v = Get(key);
|
||||
var v = GetImpl(key, use, insteadOf);
|
||||
try
|
||||
{
|
||||
return v == null ? default : JsonSerializer.Deserialize<T>(v);
|
||||
|
@ -128,6 +142,15 @@ namespace Pulumi
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as an object, by its key, or null if it doesn't
|
||||
/// exist. This works by taking the value associated with <paramref name="key"/> and passing
|
||||
/// it to <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>.
|
||||
/// </summary>
|
||||
[return: MaybeNull]
|
||||
public T GetObject<T>(string key)
|
||||
=> GetObjectImpl<T>(key, nameof(GetSecretObject));
|
||||
|
||||
/// <summary>
|
||||
/// Loads an optional configuration value, as an object, by its key, marking it as a secret
|
||||
/// or null if it doesn't exist. This works by taking the value associated with <paramref
|
||||
|
@ -136,53 +159,71 @@ namespace Pulumi
|
|||
/// </summary>
|
||||
public Output<T>? GetSecretObject<T>(string key)
|
||||
{
|
||||
var v = Get(key);
|
||||
var v = GetImpl(key);
|
||||
if (v == null)
|
||||
return null;
|
||||
|
||||
return Output.CreateSecret(GetObject<T>(key)!);
|
||||
return Output.CreateSecret(GetObjectImpl<T>(key)!);
|
||||
}
|
||||
|
||||
private string RequireImpl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
=> GetImpl(key, use, insteadOf) ?? throw new ConfigMissingException(FullKey(key));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value by its given key. If it doesn't exist, an error is thrown.
|
||||
/// </summary>
|
||||
public string Require(string key)
|
||||
=> Get(key) ?? throw new ConfigMissingException(FullKey(key));
|
||||
=> RequireImpl(key, nameof(RequireSecret));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value by its given key, marking it as a secret. If it doesn't exist, an error
|
||||
/// is thrown.
|
||||
/// </summary>
|
||||
public Output<string> RequireSecret(string key)
|
||||
=> MakeClassSecret(Require(key));
|
||||
=> MakeClassSecret(RequireImpl(key));
|
||||
|
||||
private bool RequireBooleanImpl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
=> GetBooleanImpl(key, use, insteadOf) ?? throw new ConfigMissingException(FullKey(key));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value, as a boolean, by its given key. If it doesn't exist, or the
|
||||
/// configuration value is not a legal boolean, an error is thrown.
|
||||
/// </summary>
|
||||
public bool RequireBoolean(string key)
|
||||
=> GetBoolean(key) ?? throw new ConfigMissingException(FullKey(key));
|
||||
=> RequireBooleanImpl(key, nameof(RequireSecretBoolean));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value, as a boolean, by its given key, marking it as a secret.
|
||||
/// If it doesn't exist, or the configuration value is not a legal boolean, an error is thrown.
|
||||
/// </summary>
|
||||
public Output<bool> RequireSecretBoolean(string key)
|
||||
=> MakeStructSecret(RequireBoolean(key));
|
||||
=> MakeStructSecret(RequireBooleanImpl(key));
|
||||
|
||||
private int RequireInt32Impl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
=> GetInt32Impl(key, use, insteadOf) ?? throw new ConfigMissingException(FullKey(key));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value, as a number, by its given key. If it doesn't exist, or the
|
||||
/// configuration value is not a legal number, an error is thrown.
|
||||
/// </summary>
|
||||
public int RequireInt32(string key)
|
||||
=> GetInt32(key) ?? throw new ConfigMissingException(FullKey(key));
|
||||
=> RequireInt32Impl(key, nameof(RequireSecretInt32));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value, as a number, by its given key, marking it as a secret.
|
||||
/// If it doesn't exist, or the configuration value is not a legal number, an error is thrown.
|
||||
/// </summary>
|
||||
public Output<int> RequireSecretInt32(string key)
|
||||
=> MakeStructSecret(RequireInt32(key));
|
||||
=> MakeStructSecret(RequireInt32Impl(key));
|
||||
|
||||
private T RequireObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||
{
|
||||
var v = GetImpl(key);
|
||||
if (v == null)
|
||||
throw new ConfigMissingException(FullKey(key));
|
||||
|
||||
return GetObjectImpl<T>(key, use, insteadOf)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value as a JSON string and deserializes the JSON into an object.
|
||||
|
@ -191,13 +232,7 @@ namespace Pulumi
|
|||
/// thrown.
|
||||
/// </summary>
|
||||
public T RequireObject<T>(string key)
|
||||
{
|
||||
var v = Get(key);
|
||||
if (v == null)
|
||||
throw new ConfigMissingException(FullKey(key));
|
||||
|
||||
return GetObject<T>(key)!;
|
||||
}
|
||||
=> RequireObjectImpl<T>(key, nameof(RequireSecretObject));
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration value as a JSON string and deserializes the JSON into a JavaScript
|
||||
|
@ -206,7 +241,7 @@ namespace Pulumi
|
|||
/// JsonSerializerOptions)"/>. an error is thrown.
|
||||
/// </summary>
|
||||
public Output<T> RequireSecretObject<T>(string key)
|
||||
=> Output.CreateSecret(RequireObject<T>(key));
|
||||
=> Output.CreateSecret(RequireObjectImpl<T>(key));
|
||||
|
||||
/// <summary>
|
||||
/// Turns a simple configuration key into a fully resolved one, by prepending the bag's name.
|
||||
|
|
|
@ -14,11 +14,21 @@ namespace Pulumi
|
|||
/// </summary>
|
||||
private const string _configEnvKey = "PULUMI_CONFIG";
|
||||
|
||||
/// <summary>
|
||||
/// The environment variable key that the language plugin uses to set the list of secret configuration keys.
|
||||
/// </summary>
|
||||
private const string _configSecretKeysEnvKey = "PULUMI_CONFIG_SECRET_KEYS";
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the full config map.
|
||||
/// </summary>
|
||||
internal ImmutableDictionary<string, string> AllConfig { get; private set; } = ParseConfig();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the config secret keys.
|
||||
/// </summary>
|
||||
internal ImmutableHashSet<string> ConfigSecretKeys { get; private set; } = ParseConfigSecretKeys();
|
||||
|
||||
/// <summary>
|
||||
/// Sets a configuration variable.
|
||||
/// </summary>
|
||||
|
@ -28,8 +38,14 @@ namespace Pulumi
|
|||
/// <summary>
|
||||
/// Appends all provided configuration.
|
||||
/// </summary>
|
||||
internal void SetAllConfig(IDictionary<string, string> config)
|
||||
=> AllConfig = AllConfig.AddRange(config);
|
||||
internal void SetAllConfig(IDictionary<string, string> config, IEnumerable<string>? secretKeys = null)
|
||||
{
|
||||
AllConfig = AllConfig.AddRange(config);
|
||||
if (secretKeys != null)
|
||||
{
|
||||
ConfigSecretKeys = ConfigSecretKeys.Union(secretKeys);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a configuration variable's value or <see langword="null"/> if it is unset.
|
||||
|
@ -37,6 +53,12 @@ namespace Pulumi
|
|||
string? IDeploymentInternal.GetConfig(string key)
|
||||
=> AllConfig.TryGetValue(key, out var value) ? value : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the key contains a secret value.
|
||||
/// </summary>
|
||||
bool IDeploymentInternal.IsConfigSecret(string fullKey)
|
||||
=> ConfigSecretKeys.Contains(fullKey);
|
||||
|
||||
private static ImmutableDictionary<string, string> ParseConfig()
|
||||
{
|
||||
var parsedConfig = ImmutableDictionary.CreateBuilder<string, string>();
|
||||
|
@ -54,6 +76,23 @@ namespace Pulumi
|
|||
return parsedConfig.ToImmutable();
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> ParseConfigSecretKeys()
|
||||
{
|
||||
var parsedConfigSecretKeys = ImmutableHashSet.CreateBuilder<string>();
|
||||
var envConfigSecretKeys = Environment.GetEnvironmentVariable(_configSecretKeysEnvKey);
|
||||
|
||||
if (envConfigSecretKeys != null)
|
||||
{
|
||||
var envObject = JsonDocument.Parse(envConfigSecretKeys);
|
||||
foreach (var element in envObject.RootElement.EnumerateArray())
|
||||
{
|
||||
parsedConfigSecretKeys.Add(element.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
return parsedConfigSecretKeys.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CleanKey takes a configuration key, and if it is of the form "(string):config:(string)"
|
||||
/// removes the ":config:" portion. Previously, our keys always had the string ":config:" in
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Pulumi
|
|||
_projectName = settings.Project;
|
||||
_stackName = settings.Stack;
|
||||
_isDryRun = settings.IsDryRun;
|
||||
SetAllConfig(settings.Config);
|
||||
SetAllConfig(settings.Config, settings.ConfigSecretKeys);
|
||||
|
||||
if (string.IsNullOrEmpty(settings.MonitorAddr)
|
||||
|| string.IsNullOrEmpty(settings.EngineAddr)
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace Pulumi
|
|||
internal interface IDeploymentInternal : IDeployment
|
||||
{
|
||||
string? GetConfig(string fullKey);
|
||||
bool IsConfigSecret(string fullKey);
|
||||
|
||||
Stack Stack { get; set; }
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace Pulumi
|
|||
|
||||
public IDictionary<string, string> Config { get; }
|
||||
|
||||
public IEnumerable<string>? ConfigSecretKeys { get; }
|
||||
|
||||
public string Project { get; }
|
||||
|
||||
public string Stack { get; }
|
||||
|
@ -25,7 +27,8 @@ namespace Pulumi
|
|||
string project,
|
||||
string stack,
|
||||
int parallel,
|
||||
bool isDryRun)
|
||||
bool isDryRun,
|
||||
IEnumerable<string>? configSecretKeys = null)
|
||||
{
|
||||
EngineAddr = engineAddr;
|
||||
MonitorAddr = monitorAddr;
|
||||
|
@ -34,6 +37,7 @@ namespace Pulumi
|
|||
Stack = stack;
|
||||
Parallel = parallel;
|
||||
IsDryRun = isDryRun;
|
||||
ConfigSecretKeys = configSecretKeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -488,6 +488,11 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
|
|||
err = errors.Wrap(err, "failed to serialize configuration")
|
||||
return nil, err
|
||||
}
|
||||
configSecretKeys, err := host.constructConfigSecretKeys(req)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to serialize configuration secret keys")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executable := host.exec
|
||||
args := []string{}
|
||||
|
@ -518,7 +523,7 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
|
|||
cmd := exec.Command(executable, args...) // nolint: gas // intentionally running dynamic program name.
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = host.constructEnv(req, config)
|
||||
cmd.Env = host.constructEnv(req, config, configSecretKeys)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// If the program ran, but exited with a non-zero error code. This will happen often, since user
|
||||
|
@ -547,7 +552,7 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
|
|||
return &pulumirpc.RunResponse{Error: errResult}, nil
|
||||
}
|
||||
|
||||
func (host *dotnetLanguageHost) constructEnv(req *pulumirpc.RunRequest, config string) []string {
|
||||
func (host *dotnetLanguageHost) constructEnv(req *pulumirpc.RunRequest, config, configSecretKeys string) []string {
|
||||
env := os.Environ()
|
||||
|
||||
maybeAppendEnv := func(k, v string) {
|
||||
|
@ -566,6 +571,7 @@ func (host *dotnetLanguageHost) constructEnv(req *pulumirpc.RunRequest, config s
|
|||
maybeAppendEnv("parallel", fmt.Sprint(req.GetParallel()))
|
||||
maybeAppendEnv("tracing", host.tracing)
|
||||
maybeAppendEnv("config", config)
|
||||
maybeAppendEnv("config_secret_keys", configSecretKeys)
|
||||
|
||||
return env
|
||||
}
|
||||
|
@ -585,6 +591,22 @@ func (host *dotnetLanguageHost) constructConfig(req *pulumirpc.RunRequest) (stri
|
|||
return string(configJSON), nil
|
||||
}
|
||||
|
||||
// constructConfigSecretKeys JSON-serializes the list of keys that contain secret values given as part of
|
||||
// a RunRequest.
|
||||
func (host *dotnetLanguageHost) constructConfigSecretKeys(req *pulumirpc.RunRequest) (string, error) {
|
||||
configSecretKeys := req.GetConfigSecretKeys()
|
||||
if configSecretKeys == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
|
||||
configSecretKeysJSON, err := json.Marshal(configSecretKeys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(configSecretKeysJSON), nil
|
||||
}
|
||||
|
||||
func (host *dotnetLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
|
||||
return &pulumirpc.PluginInfo{
|
||||
Version: version.Version,
|
||||
|
|
5
tests/integration/config_secrets_warn/dotnet/.gitignore
vendored
Normal file
5
tests/integration/config_secrets_warn/dotnet/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.pulumi/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
69
tests/integration/config_secrets_warn/dotnet/Program.cs
Normal file
69
tests/integration/config_secrets_warn/dotnet/Program.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi;
|
||||
|
||||
class Program
|
||||
{
|
||||
static Task<int> Main(string[] args)
|
||||
{
|
||||
return Deployment.RunAsync(() =>
|
||||
{
|
||||
var config = new Config();
|
||||
|
||||
config.Get("plainstr1");
|
||||
config.Require("plainstr2");
|
||||
config.GetSecret("plainstr3");
|
||||
config.RequireSecret("plainstr4");
|
||||
|
||||
config.GetBoolean("plainbool1");
|
||||
config.RequireBoolean("plainbool2");
|
||||
config.GetSecretBoolean("plainbool3");
|
||||
config.RequireSecretBoolean("plainbool4");
|
||||
|
||||
config.GetInt32("plainint1");
|
||||
config.RequireInt32("plainint2");
|
||||
config.GetSecretInt32("plainint3");
|
||||
config.RequireSecretInt32("plainint4");
|
||||
|
||||
config.GetObject<JsonElement>("plainobj1");
|
||||
config.RequireObject<JsonElement>("plainobj2");
|
||||
config.GetSecretObject<JsonElement>("plainobj3");
|
||||
config.RequireSecretObject<JsonElement>("plainobj4");
|
||||
|
||||
config.Get("str1");
|
||||
config.Require("str2");
|
||||
config.GetSecret("str3");
|
||||
config.RequireSecret("str4");
|
||||
|
||||
config.GetBoolean("bool1");
|
||||
config.RequireBoolean("bool2");
|
||||
config.GetSecretBoolean("bool3");
|
||||
config.RequireSecretBoolean("bool4");
|
||||
|
||||
config.GetInt32("int1");
|
||||
config.RequireInt32("int2");
|
||||
config.GetSecretInt32("int3");
|
||||
config.RequireSecretInt32("int4");
|
||||
|
||||
config.GetObject<JsonElement>("obj1");
|
||||
config.RequireObject<JsonElement>("obj2");
|
||||
config.GetSecretObject<JsonElement>("obj3");
|
||||
config.RequireSecretObject<JsonElement>("obj4");
|
||||
|
||||
config.GetObject<JsonElement>("parent1");
|
||||
config.RequireObject<JsonElement>("parent2");
|
||||
config.GetSecretObject<JsonElement>("parent1");
|
||||
config.RequireSecretObject<JsonElement>("parent2");
|
||||
|
||||
config.GetObject<JsonElement>("names1");
|
||||
config.RequireObject<JsonElement>("names2");
|
||||
config.GetSecretObject<JsonElement>("names1");
|
||||
config.RequireSecretObject<JsonElement>("names2");
|
||||
});
|
||||
}
|
||||
}
|
3
tests/integration/config_secrets_warn/dotnet/Pulumi.yaml
Normal file
3
tests/integration/config_secrets_warn/dotnet/Pulumi.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
name: config_secrets_dotnet
|
||||
description: A simple .NET program that uses configuration secrets.
|
||||
runtime: dotnet
|
|
@ -8,6 +8,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/v3/testing/integration"
|
||||
|
@ -120,6 +121,125 @@ func TestConfigBasicDotNet(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// Tests that accessing config secrets using non-secret APIs results in warnings being logged.
|
||||
func TestConfigSecretsWarnDotNet(t *testing.T) {
|
||||
integration.ProgramTest(t, &integration.ProgramTestOptions{
|
||||
Dir: filepath.Join("config_secrets_warn", "dotnet"),
|
||||
Dependencies: []string{"Pulumi"},
|
||||
Quick: true,
|
||||
Config: map[string]string{
|
||||
"plainstr1": "1",
|
||||
"plainstr2": "2",
|
||||
"plainstr3": "3",
|
||||
"plainstr4": "4",
|
||||
"plainbool1": "true",
|
||||
"plainbool2": "true",
|
||||
"plainbool3": "true",
|
||||
"plainbool4": "true",
|
||||
"plainint1": "1",
|
||||
"plainint2": "2",
|
||||
"plainint3": "3",
|
||||
"plainint4": "4",
|
||||
"plainobj1": "{}",
|
||||
"plainobj2": "{}",
|
||||
"plainobj3": "{}",
|
||||
"plainobj4": "{}",
|
||||
},
|
||||
Secrets: map[string]string{
|
||||
"str1": "1",
|
||||
"str2": "2",
|
||||
"str3": "3",
|
||||
"str4": "4",
|
||||
"bool1": "true",
|
||||
"bool2": "true",
|
||||
"bool3": "true",
|
||||
"bool4": "true",
|
||||
"int1": "1",
|
||||
"int2": "2",
|
||||
"int3": "3",
|
||||
"int4": "4",
|
||||
"obj1": "{}",
|
||||
"obj2": "{}",
|
||||
"obj3": "{}",
|
||||
"obj4": "{}",
|
||||
},
|
||||
OrderedConfig: []integration.ConfigValue{
|
||||
{Key: "parent1.foo", Value: "plain1", Path: true},
|
||||
{Key: "parent1.bar", Value: "secret1", Path: true, Secret: true},
|
||||
{Key: "parent2.foo", Value: "plain2", Path: true},
|
||||
{Key: "parent2.bar", Value: "secret2", Path: true, Secret: true},
|
||||
{Key: "names1[0]", Value: "plain1", Path: true},
|
||||
{Key: "names1[1]", Value: "secret1", Path: true, Secret: true},
|
||||
{Key: "names2[0]", Value: "plain2", Path: true},
|
||||
{Key: "names2[1]", Value: "secret2", Path: true, Secret: true},
|
||||
},
|
||||
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
|
||||
assert.NotEmpty(t, stackInfo.Events)
|
||||
//nolint:lll
|
||||
expectedWarnings := []string{
|
||||
"Configuration 'config_secrets_dotnet:str1' value is a secret; use `GetSecret` instead of `Get`",
|
||||
"Configuration 'config_secrets_dotnet:str2' value is a secret; use `RequireSecret` instead of `Require`",
|
||||
"Configuration 'config_secrets_dotnet:bool1' value is a secret; use `GetSecretBoolean` instead of `GetBoolean`",
|
||||
"Configuration 'config_secrets_dotnet:bool2' value is a secret; use `RequireSecretBoolean` instead of `RequireBoolean`",
|
||||
"Configuration 'config_secrets_dotnet:int1' value is a secret; use `GetSecretInt32` instead of `GetInt32`",
|
||||
"Configuration 'config_secrets_dotnet:int2' value is a secret; use `RequireSecretInt32` instead of `RequireInt32`",
|
||||
"Configuration 'config_secrets_dotnet:obj1' value is a secret; use `GetSecretObject` instead of `GetObject`",
|
||||
"Configuration 'config_secrets_dotnet:obj2' value is a secret; use `RequireSecretObject` instead of `RequireObject`",
|
||||
"Configuration 'config_secrets_dotnet:parent1' value is a secret; use `GetSecretObject` instead of `GetObject`",
|
||||
"Configuration 'config_secrets_dotnet:parent2' value is a secret; use `RequireSecretObject` instead of `RequireObject`",
|
||||
"Configuration 'config_secrets_dotnet:names1' value is a secret; use `GetSecretObject` instead of `GetObject`",
|
||||
"Configuration 'config_secrets_dotnet:names2' value is a secret; use `RequireSecretObject` instead of `RequireObject`",
|
||||
}
|
||||
for _, warning := range expectedWarnings {
|
||||
var found bool
|
||||
for _, event := range stackInfo.Events {
|
||||
if event.DiagnosticEvent != nil && event.DiagnosticEvent.Severity == "warning" &&
|
||||
strings.Contains(event.DiagnosticEvent.Message, warning) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected warning %q", warning)
|
||||
}
|
||||
|
||||
// These keys should not be in any warning messages.
|
||||
unexpectedWarnings := []string{
|
||||
"plainstr1",
|
||||
"plainstr2",
|
||||
"plainstr3",
|
||||
"plainstr4",
|
||||
"plainbool1",
|
||||
"plainbool2",
|
||||
"plainbool3",
|
||||
"plainbool4",
|
||||
"plainint1",
|
||||
"plainint2",
|
||||
"plainint3",
|
||||
"plainint4",
|
||||
"plainobj1",
|
||||
"plainobj2",
|
||||
"plainobj3",
|
||||
"plainobj4",
|
||||
"str3",
|
||||
"str4",
|
||||
"bool3",
|
||||
"bool4",
|
||||
"int3",
|
||||
"int4",
|
||||
"obj3",
|
||||
"obj4",
|
||||
}
|
||||
for _, warning := range unexpectedWarnings {
|
||||
for _, event := range stackInfo.Events {
|
||||
if event.DiagnosticEvent != nil {
|
||||
assert.NotContains(t, event.DiagnosticEvent.Message, warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Tests that stack references work in .NET.
|
||||
func TestStackReferenceDotnet(t *testing.T) {
|
||||
if runtime.GOOS == WindowsOS {
|
||||
|
|
Loading…
Reference in a new issue