Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Add an init command for adding a UserSecretsId #500

Merged
merged 10 commits into from
Nov 5, 2018
1 change: 1 addition & 0 deletions src/dotnet-user-secrets/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static CommandLineOptions Parse(string[] args, IConsole console)
app.Command("remove", c => RemoveCommand.Configure(c, options));
app.Command("list", c => ListCommand.Configure(c, options));
app.Command("clear", c => ClearCommand.Configure(c, options));
app.Command("init", c => InitCommandFactory.Configure(c, options));

// Show help information if no subcommand/option was specified.
app.OnExecute(() => app.ShowHelp());
Expand Down
126 changes: 126 additions & 0 deletions src/dotnet-user-secrets/Internal/InitCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Extensions.CommandLineUtils;

namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
// Workaround used to handle the fact that the options have not been parsed at configuration time
public class InitCommandFactory : ICommand
{
public CommandLineOptions Options { get; }

internal static void Configure(CommandLineApplication command, CommandLineOptions options)
{
command.Description = "Set a user secrets ID to enable secret storage";
command.HelpOption();

command.OnExecute(() =>
{
options.Command = new InitCommandFactory(options);
});
}

public InitCommandFactory(CommandLineOptions options)
{
Options = options;
}

public void Execute(CommandContext context)
{
new InitCommand(Options.Id, Options.Project).Execute(context);
}

public void Execute(CommandContext context, string workingDirectory)
{
new InitCommand(Options.Id, Options.Project).Execute(context, workingDirectory);
}
}

public class InitCommand : ICommand
{
public string OverrideId { get; }
public string ProjectPath { get; }
public string WorkingDirectory { get; private set; } = Directory.GetCurrentDirectory();

public InitCommand(string id, string project)
{
OverrideId = id;
ProjectPath = project;
}

public void Execute(CommandContext context, string workingDirectory)
{
WorkingDirectory = workingDirectory;
Execute(context);
}

public void Execute(CommandContext context)
{
var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory);

// Load the project file as XML
var projectDocument = XDocument.Load(projectPath);

// Accept the `--id` CLI option to the main app
string newSecretsId = string.IsNullOrWhiteSpace(OverrideId)
? Guid.NewGuid().ToString()
: OverrideId;

// Confirm secret ID does not contain invalid characters
if (Path.GetInvalidPathChars().Any(invalidChar => newSecretsId.Contains(invalidChar)))
{
throw new ArgumentException(Resources.FormatError_InvalidSecretsId(newSecretsId));
}

var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault();

// Check if a UserSecretsId is already set
if (existingUserSecretsId != default)
{
// Only set the UserSecretsId if the user specified an explicit value
if (string.IsNullOrWhiteSpace(OverrideId))
{
context.Reporter.Output(Resources.FormatMessage_ProjectAlreadyInitialized(projectPath));
return;
}

existingUserSecretsId.SetValue(newSecretsId);
}
else
{
// Find the first non-conditional PropertyGroup
var propertyGroup = projectDocument.Root.DescendantNodes()
.FirstOrDefault(node => node is XElement el
&& el.Name == "PropertyGroup"
&& el.Attributes().All(attr =>
attr.Name != "Condition")) as XElement;

// No valid property group, create a new one
if (propertyGroup == null)
{
propertyGroup = new XElement("PropertyGroup");
projectDocument.Root.AddFirst(propertyGroup);
}

// Add UserSecretsId element
propertyGroup.Add(new XElement("UserSecretsId", newSecretsId));
}

projectDocument.Save(projectPath);

context.Reporter.Output(Resources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath));
}

private static string ResolveProjectPath(string name, string path)
{
var finder = new MsBuildProjectFinder(path);
return finder.FindMsBuildProject(name);
}
}
}
6 changes: 6 additions & 0 deletions src/dotnet-user-secrets/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ internal int RunInternal(params string[] args)

var reporter = CreateReporter(options.IsVerbose);

if (options.Command is InitCommandFactory initCmd)
{
initCmd.Execute(new CommandContext(null, reporter, _console), _workingDirectory);
return 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In retrospec, Command.Execute should probably have returned int so the command can set an exit code. I'd be happy to also take a PR that made this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay - I looked at that but ran into the fact the interface had the expectation. At first glance though, I don't know how I'd safely modify the interface to allow a status code return without causing a breaking change (but that's a separate conversation).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have my blessing to make a breaking change on ICommand. For projects like this, the ICommand type is an internal implementation detail of these console tools. No one should be compiling an app against dotnet-user-secrets.dll.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ICommand in this instance is from CommandLineUtils, so not part of this tool - do you still want that change?

}

string userSecretsId;
try
{
Expand Down
Loading