Adding flow API command

This provide a hidden Velopack Flow API command. This allows for easier scripting directly against the API as it leveraging the built in auth.
This commit is contained in:
Kevin Bost
2024-09-15 21:39:21 -07:00
committed by Caelan
parent f61ebbd9ff
commit b828850b45
12 changed files with 157 additions and 15 deletions

View File

@@ -34,7 +34,7 @@ public class PublishTask : MSBuildAsyncTask
AllowInteractiveLogin = false,
VelopackBaseUrl = ServiceUrl,
ApiKey = ApiKey
}, cancellationToken).ConfigureAwait(false)) {
}, false, cancellationToken).ConfigureAwait(false)) {
Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
return true;
}

View File

@@ -4,6 +4,8 @@ using NuGet.Versioning;
using Microsoft.Extensions.Logging;
using System.Text;
using System.IO;
using Markdig.Helpers;
#if NET6_0_OR_GREATER
@@ -17,11 +19,15 @@ namespace Velopack.Packaging.Flow;
public interface IVelopackFlowServiceClient
{
Task<bool> LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken);
Task<bool> LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken);
Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<Profile?> GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<string> InvokeEndpointAsync(VelopackServiceOptions? options, string endpointUri,
string method,
string? body,
CancellationToken cancellationToken);
Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os, CancellationToken cancellationToken);
}
@@ -33,10 +39,12 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
private AuthConfiguration? AuthConfiguration { get; set; }
public async Task<bool> LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken)
public async Task<bool> LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken)
{
options ??= new VelopackLoginOptions();
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl);
if (!suppressOutput) {
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl);
}
var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken);
@@ -45,7 +53,9 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
if (!string.IsNullOrWhiteSpace(options.ApiKey)) {
HttpClient.DefaultRequestHeaders.Authorization = new(HmacHelper.HmacScheme, options.ApiKey);
var profile = await GetProfileAsync(options, cancellationToken);
Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName());
if (!suppressOutput) {
Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName());
}
return true;
} else {
AuthenticationResult? rv = null;
@@ -63,7 +73,9 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken);
var profile = await GetProfileAsync(options, cancellationToken);
Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
if (!suppressOutput) {
Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
}
return true;
} else {
Logger.LogError("Failed to login to Velopack");
@@ -94,6 +106,35 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
return await HttpClient.GetFromJsonAsync<Profile>(endpoint, cancellationToken);
}
public async Task<string> InvokeEndpointAsync(
VelopackServiceOptions? options,
string endpointUri,
string method,
string? body,
CancellationToken cancellationToken)
{
AssertAuthenticated();
var endpoint = GetEndpoint(endpointUri, options?.VelopackBaseUrl);
HttpRequestMessage request = new(new HttpMethod(method), endpoint);
if (body is not null) {
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken);
#if NET6_0_OR_GREATER
string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
#else
string responseBody = await response.Content.ReadAsStringAsync();
#endif
if (response.IsSuccessStatusCode) {
return responseBody;
} else {
throw new InvalidOperationException($"Failed to invoke endpoint {endpointUri} with status code {response.StatusCode}{Environment.NewLine}{responseBody}");
}
}
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl,
RuntimeOs os, CancellationToken cancellationToken)
{
@@ -164,7 +205,10 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
var endpoint = GetEndpoint("v1/releaseGroups/create", velopackBaseUrl);
var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
response.EnsureSuccessStatusCode();
if (!response.IsSuccessStatusCode) {
string content = await response.Content.ReadAsStringAsync(cancellationToken);
throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}");
}
return await response.Content.ReadFromJsonAsync<ReleaseGroup>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}");

View File

@@ -36,5 +36,10 @@ public static class HttpClientExtensions
var json = await content.ReadAsStringAsync();
return Newtonsoft.Json.JsonConvert.DeserializeObject<TValue>(json);
}
public static async Task<string> ReadAsStringAsync(this HttpContent content, CancellationToken _)
{
return await content.ReadAsStringAsync();
}
}
#endif

View File

@@ -0,0 +1,39 @@
#nullable enable
using System.Net.Http;
using Serilog.Core;
namespace Velopack.Vpk.Commands.Flow;
public class ApiCommand : VelopackServiceCommand
{
public string Method { get; private set; } = "";
public string Endpoint { get; private set; } = "";
public string? Body { get; private set; }
public ApiCommand()
: base("api", "Invoke velopack flow API endpoints")
{
AddOption<string>(v => Method = v, "--method", "-m")
.SetDescription("The HTTP method for the endpoint")
.SetArgumentHelpName("METHOD")
.SetRequired()
.SetDefault(HttpMethod.Get.Method);
AddOption<string>(v => Endpoint = v, "--endpoint", "-e")
.SetDescription("The relative URI for the endpoint")
.SetArgumentHelpName("URI")
.SetRequired();
AddOption<string>(v => Body = v, "--body", "-b")
.SetDescription("The body of the HTTP message")
.SetArgumentHelpName("BODY");
}
public override void Initialize(LoggingLevelSwitch logLevelSwitch)
{
base.Initialize(logLevelSwitch);
logLevelSwitch.MinimumLevel = Serilog.Events.LogEventLevel.Warning;
}
}

View File

@@ -0,0 +1,26 @@
using System.Threading;
using Serilog.Core;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public class ApiCommandRunner(IVelopackFlowServiceClient Client) : ICommand<ApiOptions>
{
public async Task Run(ApiOptions options)
{
CancellationToken token = CancellationToken.None;
if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
}, true, token)) {
return;
}
string response = await Client.InvokeEndpointAsync(options, options.Endpoint, options.Method, options.Body, token);
Console.WriteLine(response);
}
}

View File

@@ -0,0 +1,11 @@
#nullable enable
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public sealed class ApiOptions : VelopackServiceOptions
{
public string Endpoint { get; set; } = "";
public string Method { get; set; } = "";
public string? Body { get; set; }
}

View File

@@ -12,6 +12,6 @@ public class LoginCommandRunner(IVelopackFlowServiceClient Client) : ICommand<Lo
await Client.LoginAsync(new() {
VelopackBaseUrl = options.VelopackBaseUrl,
ApiKey = options.ApiKey,
}, CancellationToken.None);
}, false, CancellationToken.None);
}
}

View File

@@ -15,7 +15,7 @@ public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand<
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
}, token)) {
}, false, token)) {
return;
}

View File

@@ -1,5 +1,6 @@
using Humanizer;
using Microsoft.Extensions.Configuration;
using Serilog.Core;
namespace Velopack.Vpk.Commands;
@@ -70,6 +71,9 @@ public class BaseCommand : CliCommand
}
}
public virtual void Initialize(LoggingLevelSwitch logLevelSwitch)
{ }
public ParseResult ParseAndApply(string command, IConfiguration config = null, RuntimeOs? targetOs = null)
{
var x = Parse(command);

View File

@@ -7,11 +7,11 @@ using Spectre.Console.Rendering;
namespace Velopack.Vpk.Logging;
public class MySpectreConsoleSink : ILogEventSink
public class SpectreConsoleSink : ILogEventSink
{
private readonly string _dirtmp;
public MySpectreConsoleSink()
public SpectreConsoleSink()
{
_dirtmp = Path.GetTempPath();
}
@@ -53,13 +53,13 @@ public class MySpectreConsoleSink : ILogEventSink
}
}
public static class MySpectreConsoleSinkExtensions
public static class SpectreConsoleSinkExtensions
{
public static LoggerConfiguration Spectre(
this LoggerSinkConfiguration loggerConfiguration,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch levelSwitch = null)
{
return loggerConfiguration.Sink(new MySpectreConsoleSink(), restrictedToMinimumLevel, levelSwitch);
return loggerConfiguration.Sink(new SpectreConsoleSink(), restrictedToMinimumLevel, levelSwitch);
}
}

View File

@@ -36,6 +36,7 @@ public static partial class OptionMapper
public static partial LoginOptions ToOptions(this LoginCommand cmd);
public static partial LogoutOptions ToOptions(this LogoutCommand cmd);
public static partial PublishOptions ToOptions(this PublishCommand cmd);
public static partial ApiOptions ToOptions(this ApiCommand cmd);
private static DirectoryInfo StringToDirectoryInfo(string t)
{

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Velopack.Deployment;
using Velopack.Packaging.Abstractions;
@@ -156,6 +157,10 @@ public class Program
HideCommand(rootCommand.AddCommand<LogoutCommand, LogoutCommandRunner, LogoutOptions>(provider));
HideCommand(rootCommand.AddCommand<PublishCommand, PublishCommandRunner, PublishOptions>(provider));
var flowCommand = new CliCommand("flow", "Commands for interacting with Velopack Flow.") { Hidden = true };
HideCommand(flowCommand.AddCommand<ApiCommand, ApiCommandRunner, ApiOptions>(provider));
rootCommand.Add(flowCommand);
var cli = new CliConfiguration(rootCommand);
return await cli.InvokeAsync(args);
@@ -173,8 +178,11 @@ public class Program
private static void SetupLogging(IHostApplicationBuilder builder, bool verbose, bool legacyConsole)
{
var levelSwitch = new LoggingLevelSwitch {
MinimumLevel = verbose ? LogEventLevel.Debug : LogEventLevel.Information
};
var conf = new LoggerConfiguration()
.MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information)
.MinimumLevel.ControlledBy(levelSwitch)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning);
@@ -186,6 +194,7 @@ public class Program
builder.Services.AddSingleton<IFancyConsole, SpectreConsole>();
conf.WriteTo.Spectre();
}
builder.Services.AddSingleton(levelSwitch);
builder.Services.AddSingleton<IConsole>(sp => sp.GetRequiredService<IFancyConsole>());
Log.Logger = conf.CreateLogger();
@@ -247,6 +256,9 @@ public static class ProgramCommandExtensions
var console = provider.GetRequiredService<IFancyConsole>();
var config = provider.GetRequiredService<IConfiguration>();
var defaults = provider.GetRequiredService<VelopackDefaults>();
var logLevelSwitch = provider.GetRequiredService<LoggingLevelSwitch>();
command.Initialize(logLevelSwitch);
logger.LogInformation($"[bold]{Program.INTRO}[/]");
var updateCheck = new UpdateChecker(logger);