[sdk/dotnet] Warn when a secret config is read as a non-secret (#7079)

This commit is contained in:
Justin Van Patten 2021-05-18 15:01:57 -07:00 committed by GitHub
parent 480173a57f
commit 34a40d2b10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 533 additions and 46 deletions

View file

@ -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

View file

@ -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")]

View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -8,6 +8,7 @@ namespace Pulumi
internal interface IDeploymentInternal : IDeployment
{
string? GetConfig(string fullKey);
bool IsConfigSecret(string fullKey);
Stack Stack { get; set; }

View file

@ -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;
}
}
}

View file

@ -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,

View file

@ -0,0 +1,5 @@
/.pulumi/
[Bb]in/
[Oo]bj/

View file

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

View 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");
});
}
}

View file

@ -0,0 +1,3 @@
name: config_secrets_dotnet
description: A simple .NET program that uses configuration secrets.
runtime: dotnet

View file

@ -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 {