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

Create initial prototype of dotnet-user-secrets with MSBuild support #178

Merged
merged 3 commits into from
Oct 7, 2016
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
public class CommandLineOptions
{
public string Id { get; set; }
public bool IsVerbose { get; set; }
public bool IsHelp { get; set; }
public string Project { get; set; }
Expand All @@ -33,6 +34,11 @@ public static CommandLineOptions Parse(string[] args, TextWriter output)
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true);

// the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
// in the current project
var optionId = app.Option("--id", "The user secret id to use.",
Copy link
Contributor

Choose a reason for hiding this comment

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

When would project evaluation fail? Trying to determine if we really need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When would project evaluation fail?

At the moment, the question is "When does project evaluation not fail?" :trollface:

Bad XML, missing imports, invalid syntax, etc... there are lots of reasons why it's good to have a an escape hatch. Plus, some may prefer to just us --id and avoid the MSBuild complexity.

CommandOptionType.SingleValue, inherited: true);

var options = new CommandLineOptions();
app.Command("set", c => SetCommand.Configure(c, options));
app.Command("remove", c => RemoveCommand.Configure(c, options));
Expand All @@ -48,6 +54,7 @@ public static CommandLineOptions Parse(string[] args, TextWriter output)
return null;
}

options.Id = optionId.Value();
options.IsHelp = app.IsShowingInformation;
options.IsVerbose = optionVerbose.HasValue();
options.Project = optionProject.Value();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.ProjectModel;

namespace Microsoft.Extensions.SecretManager.Tools
{
internal class GracefulProjectFinder : MsBuildProjectFinder
{
public GracefulProjectFinder(string directory)
: base(directory)
{
}

protected override Exception FileDoesNotExist(string filePath)
=> new GracefulException(Resources.FormatError_ProjectPath_NotFound(filePath));

protected override Exception MultipleProjectsFound(string directory)
=> new GracefulException($"Multiple MSBuild project files found in '{directory}'. Specify which to use with the --project option.");

protected override Exception NoProjectsFound(string directory)
=> new GracefulException($"Could not find a MSBuild project file in '{directory}'. Specify which project to use with the --project option.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.ProjectModel;

namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
public static class ProjectContextExtensions
{
public static string GetUserSecretsId(this IProjectContext context)
{
var userSecretsId = context.FindProperty("UserSecretsId");

if (string.IsNullOrEmpty(userSecretsId))
{
throw new GracefulException(Resources.FormatError_ProjectMissingId(context.ProjectFullPath));
}

return userSecretsId;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public SecretsStore(string userSecretsId, ILogger logger)
}

_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);

logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);

_secrets = new ConfigurationBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
<dependencies>
<group targetFramework=".NETCoreApp1.0">
<!-- MUST BE alphabetical -->
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" />
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_2$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_4$" />
<dependency id="Newtonsoft.Json" version="$dep_5$" />
<dependency id="System.Runtime.Serialization.Primitives" version="$dep_6$" />
<dependency id="Microsoft.Build.Runtime" version="$dep_1$" />
<dependency id="Microsoft.DotNet.Cli.Utils" version="$dep_2$" />
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_3$" />
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_4$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_5$" />
<dependency id="Microsoft.NETCore.App" version="$dep_6$" />
<dependency id="NuGet.Versioning" version="$dep_7$" />
</group>
</dependencies>
</metadata>
Expand Down
96 changes: 38 additions & 58 deletions src/Microsoft.Extensions.SecretManager.Tools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.ProjectModel;
using Microsoft.Build.Exceptions;

namespace Microsoft.Extensions.SecretManager.Tools
{
Expand All @@ -20,16 +19,23 @@ public class Program
private CommandOutputProvider _loggerProvider;
private readonly TextWriter _consoleOutput;
private readonly string _workingDirectory;
// TODO this is only for testing. Can remove this when this project builds with CLI preview3
private readonly MsBuildContext _msBuildContext;

public Program()
: this(Console.Out, Directory.GetCurrentDirectory())
public static int Main(string[] args)
{
HandleDebugFlag(ref args);

int rc;
new Program(Console.Out, Directory.GetCurrentDirectory(), MsBuildContext.FromCurrentDotNetSdk()).TryRun(args, out rc);
return rc;
}

internal Program(TextWriter consoleOutput, string workingDirectory)
internal Program(TextWriter consoleOutput, string workingDirectory, MsBuildContext msbuildContext)
{
_consoleOutput = consoleOutput;
_workingDirectory = workingDirectory;
_msBuildContext = msbuildContext;

var loggerFactory = new LoggerFactory();
CommandOutputProvider = new CommandOutputProvider();
Expand Down Expand Up @@ -65,15 +71,6 @@ public CommandOutputProvider CommandOutputProvider
}
}

public static int Main(string[] args)
{
HandleDebugFlag(ref args);

int rc;
new Program().TryRun(args, out rc);
return rc;
}

[Conditional("DEBUG")]
private static void HandleDebugFlag(ref string[] args)
{
Expand Down Expand Up @@ -103,6 +100,11 @@ public bool TryRun(string[] args, out int returnCode)
{
if (exception is GracefulException)
{
if (exception.InnerException != null)
{
Logger.LogInformation(exception.InnerException.Message);
}

Logger.LogError(exception.Message);
}
else
Expand Down Expand Up @@ -134,59 +136,37 @@ internal int RunInternal(params string[] args)
CommandOutputProvider.LogLevel = LogLevel.Debug;
}

var userSecretsId = ResolveUserSecretsId(options);
var userSecretsId = !string.IsNullOrEmpty(options.Id)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: change order for clarity:

var userSecretsId = string.IsNullOrEmpty(options.Id) ? ResolveIdFromProject(options.Project) : options.Id;

? options.Id
: ResolveIdFromProject(options.Project);

var store = new SecretsStore(userSecretsId, Logger);
options.Command.Execute(store, Logger);
return 0;
}

private string ResolveUserSecretsId(CommandLineOptions options)
internal string ResolveIdFromProject(string projectPath)
{
var projectPath = options.Project ?? _workingDirectory;

if (!Path.IsPathRooted(projectPath))
{
projectPath = Path.Combine(_workingDirectory, projectPath);
}

if (!projectPath.EndsWith("project.json", StringComparison.OrdinalIgnoreCase))
{
projectPath = Path.Combine(projectPath, "project.json");
}

var fileInfo = new PhysicalFileInfo(new FileInfo(projectPath));
var finder = new GracefulProjectFinder(_workingDirectory);
var projectFile = finder.FindMsBuildProject(projectPath);

if (!fileInfo.Exists)
{
throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath));
}

Logger.LogDebug(Resources.Message_Project_File_Path, fileInfo.PhysicalPath);
return ReadUserSecretsId(fileInfo);
}
Logger.LogDebug(Resources.Message_Project_File_Path, projectFile);

// TODO can use runtime API when upgrading to 1.1
private string ReadUserSecretsId(IFileInfo fileInfo)
{
if (fileInfo == null || !fileInfo.Exists)
try
{
throw new GracefulException($"Could not find file '{fileInfo.PhysicalPath}'");
var project = new MsBuildProjectContextBuilder()
.UseMsBuild(_msBuildContext)
.AsDesignTimeBuild()
.WithBuildTargets(Array.Empty<string>())
.WithProjectFile(projectFile)
.WithTargetFramework("") // TFM doesn't matter
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to set it then? Seems like this should be a default

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tool doesn't need to worry about TFMs. We just want to read a global property. The alternative would be .BuildAllTargetFrameworks().First()

.Build();

return project.GetUserSecretsId();
Copy link
Contributor

Choose a reason for hiding this comment

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

Make debugging easier

var id = project.GetUserSecretsId();
return id;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how does this make debugging easier?

}

using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(streamReader))
catch (InvalidProjectFileException ex)
{
var obj = JObject.Load(jsonReader);

var userSecretsId = obj.Value<string>("userSecretsId");

if (string.IsNullOrEmpty(userSecretsId))
{
throw new GracefulException($"Could not find 'userSecretsId' in json file '{fileInfo.PhysicalPath}'");
}

return userSecretsId;
throw new GracefulException(Resources.FormatError_ProjectFailedToLoad(projectFile), ex);
}
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading