[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
|
- Warn when a secret config is read as a non-secret
|
||||||
[#6896](https://github.com/pulumi/pulumi/pull/6896)
|
[#6896](https://github.com/pulumi/pulumi/pull/6896)
|
||||||
[#7078](https://github.com/pulumi/pulumi/pull/7078)
|
[#7078](https://github.com/pulumi/pulumi/pull/7078)
|
||||||
|
[#7079](https://github.com/pulumi/pulumi/pull/7079)
|
||||||
|
|
||||||
### Bug Fixes
|
### 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
|
private class ValidStack : Stack
|
||||||
{
|
{
|
||||||
[Output("exp_static")]
|
[Output("exp_static")]
|
||||||
|
|
|
@ -47,7 +47,8 @@ namespace Pulumi.Automation
|
||||||
request.Project,
|
request.Project,
|
||||||
request.Stack,
|
request.Stack,
|
||||||
request.Parallel,
|
request.Parallel,
|
||||||
request.DryRun);
|
request.DryRun,
|
||||||
|
request.ConfigSecretKeys);
|
||||||
|
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
this._callerContext.CancellationToken,
|
this._callerContext.CancellationToken,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright 2016-2019, Pulumi Corporation
|
// Copyright 2016-2019, Pulumi Corporation
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Pulumi
|
namespace Pulumi
|
||||||
|
@ -54,30 +56,44 @@ namespace Pulumi
|
||||||
=> Output.CreateSecret(value);
|
=> 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>
|
/// <summary>
|
||||||
/// Loads an optional configuration value by its key, or <see langword="null"/> if it doesn't exist.
|
/// Loads an optional configuration value by its key, or <see langword="null"/> if it doesn't exist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Get(string key)
|
public string? Get(string key)
|
||||||
=> Deployment.InternalInstance.GetConfig(FullKey(key));
|
=> GetImpl(key, nameof(GetSecret));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads an optional configuration value by its key, marking it as a secret, or <see
|
/// Loads an optional configuration value by its key, marking it as a secret, or <see
|
||||||
/// langword="null"/> if it doesn't exist.
|
/// langword="null"/> if it doesn't exist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<string>? GetSecret(string key)
|
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>
|
/// <summary>
|
||||||
/// Loads an optional configuration value, as a boolean, by its key, or null if it doesn't exist.
|
/// 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.
|
/// If the configuration value isn't a legal boolean, this function will throw an error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? GetBoolean(string key)
|
public bool? GetBoolean(string key)
|
||||||
{
|
=> GetBooleanImpl(key, nameof(GetSecretBoolean));
|
||||||
var v = Get(key);
|
|
||||||
return v == null ? default(bool?) :
|
|
||||||
v == "true" ? true :
|
|
||||||
v == "false" ? false : throw new ConfigTypeException(FullKey(key), v, nameof(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads an optional configuration value, as a boolean, by its key, making it as a secret or
|
/// 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.
|
/// function will throw an error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<bool>? GetSecretBoolean(string key)
|
public Output<bool>? GetSecretBoolean(string key)
|
||||||
=> MakeStructSecret(GetBoolean(key));
|
=> MakeStructSecret(GetBooleanImpl(key));
|
||||||
|
|
||||||
/// <summary>
|
private int? GetInt32Impl(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
var v = Get(key);
|
var v = GetImpl(key, use, insteadOf);
|
||||||
return v == null
|
return v == null
|
||||||
? default(int?)
|
? default(int?)
|
||||||
: int.TryParse(v, out var result)
|
: int.TryParse(v, out var result)
|
||||||
|
@ -101,23 +113,25 @@ namespace Pulumi
|
||||||
: throw new ConfigTypeException(FullKey(key), v, nameof(Int32));
|
: 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>
|
/// <summary>
|
||||||
/// Loads an optional configuration value, as a number, by its key, marking it as a secret
|
/// Loads an optional configuration value, as a number, by its key, marking it as a secret
|
||||||
/// or null if it doesn't exist.
|
/// or null if it doesn't exist.
|
||||||
/// If the configuration value isn't a legal number, this function will throw an error.
|
/// If the configuration value isn't a legal number, this function will throw an error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<int>? GetSecretInt32(string key)
|
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]
|
[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
|
try
|
||||||
{
|
{
|
||||||
return v == null ? default : JsonSerializer.Deserialize<T>(v);
|
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>
|
/// <summary>
|
||||||
/// Loads an optional configuration value, as an object, by its key, marking it as a secret
|
/// 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
|
/// or null if it doesn't exist. This works by taking the value associated with <paramref
|
||||||
|
@ -136,53 +159,71 @@ namespace Pulumi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<T>? GetSecretObject<T>(string key)
|
public Output<T>? GetSecretObject<T>(string key)
|
||||||
{
|
{
|
||||||
var v = Get(key);
|
var v = GetImpl(key);
|
||||||
if (v == null)
|
if (v == null)
|
||||||
return 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>
|
/// <summary>
|
||||||
/// Loads a configuration value by its given key. If it doesn't exist, an error is thrown.
|
/// Loads a configuration value by its given key. If it doesn't exist, an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Require(string key)
|
public string Require(string key)
|
||||||
=> Get(key) ?? throw new ConfigMissingException(FullKey(key));
|
=> RequireImpl(key, nameof(RequireSecret));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a configuration value by its given key, marking it as a secret. If it doesn't exist, an error
|
/// Loads a configuration value by its given key, marking it as a secret. If it doesn't exist, an error
|
||||||
/// is thrown.
|
/// is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<string> RequireSecret(string key)
|
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>
|
/// <summary>
|
||||||
/// Loads a configuration value, as a boolean, by its given key. If it doesn't exist, or the
|
/// 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.
|
/// configuration value is not a legal boolean, an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RequireBoolean(string key)
|
public bool RequireBoolean(string key)
|
||||||
=> GetBoolean(key) ?? throw new ConfigMissingException(FullKey(key));
|
=> RequireBooleanImpl(key, nameof(RequireSecretBoolean));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a configuration value, as a boolean, by its given key, marking it as a secret.
|
/// 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.
|
/// If it doesn't exist, or the configuration value is not a legal boolean, an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<bool> RequireSecretBoolean(string key)
|
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>
|
/// <summary>
|
||||||
/// Loads a configuration value, as a number, by its given key. If it doesn't exist, or the
|
/// 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.
|
/// configuration value is not a legal number, an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int RequireInt32(string key)
|
public int RequireInt32(string key)
|
||||||
=> GetInt32(key) ?? throw new ConfigMissingException(FullKey(key));
|
=> RequireInt32Impl(key, nameof(RequireSecretInt32));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a configuration value, as a number, by its given key, marking it as a secret.
|
/// 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.
|
/// If it doesn't exist, or the configuration value is not a legal number, an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<int> RequireSecretInt32(string key)
|
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>
|
/// <summary>
|
||||||
/// Loads a configuration value as a JSON string and deserializes the JSON into an object.
|
/// Loads a configuration value as a JSON string and deserializes the JSON into an object.
|
||||||
|
@ -191,13 +232,7 @@ namespace Pulumi
|
||||||
/// thrown.
|
/// thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T RequireObject<T>(string key)
|
public T RequireObject<T>(string key)
|
||||||
{
|
=> RequireObjectImpl<T>(key, nameof(RequireSecretObject));
|
||||||
var v = Get(key);
|
|
||||||
if (v == null)
|
|
||||||
throw new ConfigMissingException(FullKey(key));
|
|
||||||
|
|
||||||
return GetObject<T>(key)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a configuration value as a JSON string and deserializes the JSON into a JavaScript
|
/// 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.
|
/// JsonSerializerOptions)"/>. an error is thrown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Output<T> RequireSecretObject<T>(string key)
|
public Output<T> RequireSecretObject<T>(string key)
|
||||||
=> Output.CreateSecret(RequireObject<T>(key));
|
=> Output.CreateSecret(RequireObjectImpl<T>(key));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Turns a simple configuration key into a fully resolved one, by prepending the bag's name.
|
/// Turns a simple configuration key into a fully resolved one, by prepending the bag's name.
|
||||||
|
|
|
@ -14,11 +14,21 @@ namespace Pulumi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string _configEnvKey = "PULUMI_CONFIG";
|
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>
|
/// <summary>
|
||||||
/// Returns a copy of the full config map.
|
/// Returns a copy of the full config map.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal ImmutableDictionary<string, string> AllConfig { get; private set; } = ParseConfig();
|
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>
|
/// <summary>
|
||||||
/// Sets a configuration variable.
|
/// Sets a configuration variable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -28,8 +38,14 @@ namespace Pulumi
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Appends all provided configuration.
|
/// Appends all provided configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SetAllConfig(IDictionary<string, string> config)
|
internal void SetAllConfig(IDictionary<string, string> config, IEnumerable<string>? secretKeys = null)
|
||||||
=> AllConfig = AllConfig.AddRange(config);
|
{
|
||||||
|
AllConfig = AllConfig.AddRange(config);
|
||||||
|
if (secretKeys != null)
|
||||||
|
{
|
||||||
|
ConfigSecretKeys = ConfigSecretKeys.Union(secretKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a configuration variable's value or <see langword="null"/> if it is unset.
|
/// 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)
|
string? IDeploymentInternal.GetConfig(string key)
|
||||||
=> AllConfig.TryGetValue(key, out var value) ? value : null;
|
=> 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()
|
private static ImmutableDictionary<string, string> ParseConfig()
|
||||||
{
|
{
|
||||||
var parsedConfig = ImmutableDictionary.CreateBuilder<string, string>();
|
var parsedConfig = ImmutableDictionary.CreateBuilder<string, string>();
|
||||||
|
@ -54,6 +76,23 @@ namespace Pulumi
|
||||||
return parsedConfig.ToImmutable();
|
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>
|
/// <summary>
|
||||||
/// CleanKey takes a configuration key, and if it is of the form "(string):config:(string)"
|
/// 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
|
/// removes the ":config:" portion. Previously, our keys always had the string ":config:" in
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Pulumi
|
||||||
_projectName = settings.Project;
|
_projectName = settings.Project;
|
||||||
_stackName = settings.Stack;
|
_stackName = settings.Stack;
|
||||||
_isDryRun = settings.IsDryRun;
|
_isDryRun = settings.IsDryRun;
|
||||||
SetAllConfig(settings.Config);
|
SetAllConfig(settings.Config, settings.ConfigSecretKeys);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(settings.MonitorAddr)
|
if (string.IsNullOrEmpty(settings.MonitorAddr)
|
||||||
|| string.IsNullOrEmpty(settings.EngineAddr)
|
|| string.IsNullOrEmpty(settings.EngineAddr)
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace Pulumi
|
||||||
internal interface IDeploymentInternal : IDeployment
|
internal interface IDeploymentInternal : IDeployment
|
||||||
{
|
{
|
||||||
string? GetConfig(string fullKey);
|
string? GetConfig(string fullKey);
|
||||||
|
bool IsConfigSecret(string fullKey);
|
||||||
|
|
||||||
Stack Stack { get; set; }
|
Stack Stack { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ namespace Pulumi
|
||||||
|
|
||||||
public IDictionary<string, string> Config { get; }
|
public IDictionary<string, string> Config { get; }
|
||||||
|
|
||||||
|
public IEnumerable<string>? ConfigSecretKeys { get; }
|
||||||
|
|
||||||
public string Project { get; }
|
public string Project { get; }
|
||||||
|
|
||||||
public string Stack { get; }
|
public string Stack { get; }
|
||||||
|
@ -25,7 +27,8 @@ namespace Pulumi
|
||||||
string project,
|
string project,
|
||||||
string stack,
|
string stack,
|
||||||
int parallel,
|
int parallel,
|
||||||
bool isDryRun)
|
bool isDryRun,
|
||||||
|
IEnumerable<string>? configSecretKeys = null)
|
||||||
{
|
{
|
||||||
EngineAddr = engineAddr;
|
EngineAddr = engineAddr;
|
||||||
MonitorAddr = monitorAddr;
|
MonitorAddr = monitorAddr;
|
||||||
|
@ -34,6 +37,7 @@ namespace Pulumi
|
||||||
Stack = stack;
|
Stack = stack;
|
||||||
Parallel = parallel;
|
Parallel = parallel;
|
||||||
IsDryRun = isDryRun;
|
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")
|
err = errors.Wrap(err, "failed to serialize configuration")
|
||||||
return nil, err
|
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
|
executable := host.exec
|
||||||
args := []string{}
|
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 := exec.Command(executable, args...) // nolint: gas // intentionally running dynamic program name.
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = host.constructEnv(req, config)
|
cmd.Env = host.constructEnv(req, config, configSecretKeys)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
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
|
// 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
|
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()
|
env := os.Environ()
|
||||||
|
|
||||||
maybeAppendEnv := func(k, v string) {
|
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("parallel", fmt.Sprint(req.GetParallel()))
|
||||||
maybeAppendEnv("tracing", host.tracing)
|
maybeAppendEnv("tracing", host.tracing)
|
||||||
maybeAppendEnv("config", config)
|
maybeAppendEnv("config", config)
|
||||||
|
maybeAppendEnv("config_secret_keys", configSecretKeys)
|
||||||
|
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
@ -585,6 +591,22 @@ func (host *dotnetLanguageHost) constructConfig(req *pulumirpc.RunRequest) (stri
|
||||||
return string(configJSON), nil
|
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) {
|
func (host *dotnetLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
|
||||||
return &pulumirpc.PluginInfo{
|
return &pulumirpc.PluginInfo{
|
||||||
Version: version.Version,
|
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"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/v3/testing/integration"
|
"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.
|
// Tests that stack references work in .NET.
|
||||||
func TestStackReferenceDotnet(t *testing.T) {
|
func TestStackReferenceDotnet(t *testing.T) {
|
||||||
if runtime.GOOS == WindowsOS {
|
if runtime.GOOS == WindowsOS {
|
||||||
|
|
Loading…
Reference in a new issue