Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Improvements-590.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component: sdk/auto
kind: Improvements
body: implement config options functionality
time: 2025-05-08T10:49:51.83992634-05:00
custom:
PR: "590"
2 changes: 1 addition & 1 deletion pulumi
Submodule pulumi updated 1804 files
147 changes: 147 additions & 0 deletions sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2513,5 +2513,152 @@ public async Task InstallLanguageVersionToolsRequiresSupportedVersion()

await Assert.ThrowsAsync<InvalidOperationException>(async () => await workspace.InstallAsync(installOptions));
}

[Fact]
public async Task ConfigOptions_PathOption_Works()
{
var projectName = "config_options_path_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string?>
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"{RandomStackName()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
try
{
var options = new ConfigOptions(path: true);
await stack.SetConfigWithOptionsAsync("foo.bar", new ConfigValue("baz"), options);
var value = await stack.GetConfigWithOptionsAsync("foo.bar", options);
Assert.Equal("baz", value.Value);
}
finally
{
await workspace.RemoveStackAsync(stackName);
}
}

[Fact]
public async Task ConfigOptions_ConfigFileOption_Works()
{
var projectName = "config_options_configfile_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
var configFile = Path.Combine(Path.GetTempPath(), $"Pulumi.{Guid.NewGuid()}.yaml");
File.WriteAllText(configFile, "config: { test:key: value }\n");
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string?>
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"{RandomStackName()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
try
{
var options = new ConfigOptions(configFile: configFile);
var value = await stack.GetConfigWithOptionsAsync("test:key", options);
Assert.Equal("value", value.Value);
}
finally
{
await workspace.RemoveStackAsync(stackName);
File.Delete(configFile);
}
}

[Fact]
public async Task ConfigOptions_ShowSecretsOption_Works()
{
var projectName = "config_options_secrets_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string?>
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"{RandomStackName()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
try
{
var config = new Dictionary<string, ConfigValue>
{
["secret"] = new ConfigValue("supersecret", isSecret: true),
};
await stack.SetAllConfigAsync(config);
var options = new GetAllConfigOptions(showSecrets: true);
var values = await stack.GetAllConfigWithOptionsAsync(options);
Assert.True(values.TryGetValue($"{projectName}:secret", out var secretValue));
Assert.Equal("supersecret", secretValue!.Value);
Assert.True(secretValue.IsSecret);
}
finally
{
await workspace.RemoveStackAsync(stackName);
}
}

[Fact]
public async Task ConfigOptions_MultipleOptions_Works()
{
var projectName = "config_options_multi_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string?>
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"{RandomStackName()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
try
{
var options = new ConfigOptions(path: true, showSecrets: true);
await stack.SetConfigWithOptionsAsync("foo.bar", new ConfigValue("baz", isSecret: true), options);
var value = await stack.GetConfigWithOptionsAsync("foo.bar", options);
Assert.Equal("baz", value.Value);
}
finally
{
await workspace.RemoveStackAsync(stackName);
}
}

[Fact]
public async Task ConfigOptions_InvalidConfigFile_Throws()
{
var projectName = "config_options_invalidfile_test";
var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS);
var invalidConfigFile = Path.Combine(Path.GetTempPath(), $"Pulumi.invalid.{Guid.NewGuid()}.yaml");
using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions
{
ProjectSettings = projectSettings,
EnvironmentVariables = new Dictionary<string, string?>
{
["PULUMI_CONFIG_PASSPHRASE"] = "test",
}
});
var stackName = $"{RandomStackName()}";
var stack = await WorkspaceStack.CreateAsync(stackName, workspace);
try
{
var options = new ConfigOptions(configFile: invalidConfigFile);
await Assert.ThrowsAsync<CommandException>(() => stack.GetConfigWithOptionsAsync("foo", options));
}
finally
{
await workspace.RemoveStackAsync(stackName);
}
}
}
}
32 changes: 32 additions & 0 deletions sdk/Pulumi.Automation/ConfigOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024, Pulumi Corporation

namespace Pulumi.Automation
{
public class ConfigOptions
{
public bool Path { get; set; }
public string? ConfigFile { get; set; }
public bool ShowSecrets { get; set; }

public ConfigOptions(bool path = false, string? configFile = null, bool showSecrets = false)
{
Path = path;
ConfigFile = configFile;
ShowSecrets = showSecrets;
}
}

public class GetAllConfigOptions
{
public bool Path { get; set; }
public string? ConfigFile { get; set; }
public bool ShowSecrets { get; set; }

public GetAllConfigOptions(bool path = false, string? configFile = null, bool showSecrets = false)
{
Path = path;
ConfigFile = configFile;
ShowSecrets = showSecrets;
}
}
}
193 changes: 193 additions & 0 deletions sdk/Pulumi.Automation/LocalWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,5 +1040,198 @@ private void CheckSupportsEnvironmentsCommands()
}
}

// Config options overloads
public override async Task<ConfigValue> GetConfigWithOptionsAsync(string stackName, string key, ConfigOptions? options = null, CancellationToken cancellationToken = default)
{
var args = new List<string> { "config", "get", key, "--json", "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
// Note: --show-secrets flag is not supported for 'config get' command,
// it's only for 'config' command (get all)
}
var result = await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(result.StandardOutput))
{
return new ConfigValue(string.Empty);
}

try
{
var jsonOptions = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

// Try deserializing as ConfigValue object with value/secret properties
var configValue = System.Text.Json.JsonSerializer.Deserialize<ConfigValueModel>(
result.StandardOutput, jsonOptions);

if (configValue != null)
{
return new ConfigValue(configValue.Value ?? string.Empty, configValue.Secret);
}

// If that fails, might be a plain string value
return new ConfigValue(result.StandardOutput.Trim('"'));
}
catch (System.Text.Json.JsonException)
{
// If JSON parsing fails, return the raw value trimmed
return new ConfigValue(result.StandardOutput.Trim());
}
}

// Helper class to deserialize JSON response from pulumi config get --json
private class ConfigValueModel
{
public string? Value { get; set; }
public bool Secret { get; set; }
}

public override async Task SetConfigWithOptionsAsync(string stackName, string key, ConfigValue value, ConfigOptions? options = null, CancellationToken cancellationToken = default)
{
var args = new List<string> { "config", "set", key, value.Value, "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
}
if (value.IsSecret) args.Add("--secret");
await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);
}

public override async Task RemoveConfigWithOptionsAsync(string stackName, string key, ConfigOptions? options = null, CancellationToken cancellationToken = default)
{
var args = new List<string> { "config", "rm", key, "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
}
await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);
}

public override async Task<ImmutableDictionary<string, ConfigValue>> GetAllConfigWithOptionsAsync(string stackName, GetAllConfigOptions? options = null, CancellationToken cancellationToken = default)
{
var args = new List<string> { "config", "--json", "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
if (options.ShowSecrets) args.Add("--show-secrets");
}
var result = await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(result.StandardOutput))
{
return ImmutableDictionary<string, ConfigValue>.Empty;
}

try
{
// Parse JSON response into Dictionary<string, JsonElement> first to handle various value types
var jsonOptions = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

var configValues = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, System.Text.Json.JsonElement>>(
result.StandardOutput, jsonOptions);

if (configValues == null)
{
return ImmutableDictionary<string, ConfigValue>.Empty;
}

// Convert each entry to a ConfigValue
var builder = ImmutableDictionary.CreateBuilder<string, ConfigValue>();
foreach (var entry in configValues)
{
if (entry.Value.ValueKind == System.Text.Json.JsonValueKind.Object)
{
// For secret values, look for a structure like {"value":"secretvalue","secret":true}
var isSecret = false;
var value = string.Empty;

if (entry.Value.TryGetProperty("secret", out var secretProp) &&
secretProp.ValueKind == System.Text.Json.JsonValueKind.True)
{
isSecret = true;
}

if (entry.Value.TryGetProperty("value", out var valueProp) &&
valueProp.ValueKind == System.Text.Json.JsonValueKind.String)
{
value = valueProp.GetString() ?? string.Empty;
}

builder.Add(entry.Key, new ConfigValue(value, isSecret));
}
else if (entry.Value.ValueKind == System.Text.Json.JsonValueKind.String)
{
// Plain string value
builder.Add(entry.Key, new ConfigValue(entry.Value.GetString() ?? string.Empty));
}
else
{
// Convert any other value to string
builder.Add(entry.Key, new ConfigValue(entry.Value.ToString()));
}
}

return builder.ToImmutable();
}
catch (System.Text.Json.JsonException ex)
{
throw new InvalidOperationException($"Could not parse config values: {ex.Message}");
}
}

public override async Task SetAllConfigWithOptionsAsync(string stackName, IDictionary<string, ConfigValue> configMap, ConfigOptions? options = null, CancellationToken cancellationToken = default)
{
if (configMap == null || !configMap.Any())
{
return;
}

var args = new List<string> { "config", "set-all", "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
}

// For each config value, add as either secret or plaintext
foreach (var kvp in configMap)
{
args.Add(kvp.Value.IsSecret ? "--secret" : "--plaintext");
args.Add($"{kvp.Key}={kvp.Value.Value}");
}

await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);
}

public override async Task RemoveAllConfigWithOptionsAsync(string stackName, IEnumerable<string> keys, ConfigOptions? options = null, CancellationToken cancellationToken = default)
{
if (keys == null || !keys.Any())
{
return;
}

var args = new List<string> { "config", "rm-all", "--stack", stackName };
if (options != null)
{
if (options.Path) args.Add("--path");
if (!string.IsNullOrEmpty(options.ConfigFile)) { args.Add("--config-file"); args.Add(options.ConfigFile!); }
}

// Add all keys to remove
args.AddRange(keys);

await this.RunCommandAsync(args, null, null, null, null, cancellationToken).ConfigureAwait(false);
}
}
}
Loading
Loading