Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated MS Task with swagger/swashbuckle install #6

Merged
merged 13 commits into from
Nov 2, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bld/
[Oo]bj/
[Ll]og/
[Ll]ogs/
[Tt]ool/

# Visual Studio 2015/2017 cache/options directory
.vs/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Workleap.OpenApi.MSBuild.Exceptions;
ruvyas marked this conversation as resolved.
Show resolved Hide resolved

public class OpenApiTaskFailedException : Exception
{
public OpenApiTaskFailedException(string message) : base(message)
{
}
}
8 changes: 8 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ILoggerWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Build.Utilities;

namespace Workleap.OpenApi.MSBuild;

public interface ILoggerWrapper
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
TaskLoggingHelper Helper { get; set; }
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/IProcessWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild;

public interface IProcessWrapper
{
public Task<int> RunProcessAsync(string filename, string[] arguments, CancellationToken cancellationToken);
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ISpectralManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild;

public interface ISpectralManager
{
public Task InstallSpectralAsync(CancellationToken cancellationToken);
}
10 changes: 10 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Workleap.OpenApi.MSBuild;

public interface ISwaggerManager
{
Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken);

Task InstallSwaggerCliAsync(CancellationToken cancellationToken);

Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken);
}
13 changes: 13 additions & 0 deletions src/Workleap.OpenApi.MSBuild/LoggerWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Build.Utilities;

namespace Workleap.OpenApi.MSBuild;

internal sealed class LoggerWrapper : ILoggerWrapper
{
public LoggerWrapper(TaskLoggingHelper helper)
{
this.Helper = helper;
}

public TaskLoggingHelper Helper { get; set; }
}
25 changes: 25 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ProcessWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using CliWrap;
using CliWrap.Buffered;

namespace Workleap.OpenApi.MSBuild;

internal sealed class ProcessWrapper : IProcessWrapper
{
private readonly string _workingDirectory;

public ProcessWrapper(string workingDirectory)
{
this._workingDirectory = workingDirectory;
}

public async Task<int> RunProcessAsync(string filename, string[] arguments, CancellationToken cancellationToken)
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
var result = await Cli.Wrap(filename)
.WithWorkingDirectory(this._workingDirectory)
.WithValidation(CommandResultValidation.None)
.WithArguments(arguments)
.ExecuteBufferedAsync(cancellationToken);

return result.ExitCode;
}
}
140 changes: 140 additions & 0 deletions src/Workleap.OpenApi.MSBuild/SpectralManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;
using Workleap.OpenApi.MSBuild.Exceptions;

namespace Workleap.OpenApi.MSBuild;

internal sealed class SpectralManager : ISpectralManager
{
private const string SpectralVersion = "6.11.0";

private readonly IProcessWrapper _processWrapper;
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
private readonly ILoggerWrapper _loggerWrapper;
private readonly string _openApiToolsDirectoryPath;
private readonly string _toolDirectory;

public SpectralManager(IProcessWrapper processWrapper, ILoggerWrapper loggerWrapper, string openApiToolsDirectoryPath)
{
this._processWrapper = processWrapper;
this._loggerWrapper = loggerWrapper;
this._openApiToolsDirectoryPath = openApiToolsDirectoryPath;
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
this._toolDirectory = Path.Combine(openApiToolsDirectoryPath, $"spectral/{SpectralVersion}");
Zav marked this conversation as resolved.
Show resolved Hide resolved
}

public async Task InstallSpectralAsync(CancellationToken cancellationToken)
{
this.CreateRequiredDirectories();
var executableFileName = this.GetSpectralFileName();

var url = $"https://github.com/stoplightio/spectral/releases/download/v{SpectralVersion}/{executableFileName}";

await this.DownloadFileAsync(url, $"{this._toolDirectory}/{executableFileName}", cancellationToken);
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
}

private void CreateRequiredDirectories()
{
Directory.CreateDirectory(Path.Combine(this._openApiToolsDirectoryPath, "spectral"));
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
Directory.CreateDirectory(this._toolDirectory);
}

private string GetSpectralFileName()
{
var osType = GetOperatingSystem();
var architecture = GetArchitecture();

if (osType == "linux")
{
var distro = File.Exists("/etc/os-release") ? File.ReadAllText("/etc/os-release") : string.Empty;
Zav marked this conversation as resolved.
Show resolved Hide resolved
if (distro.Contains("Alpine Linux"))
{
osType = "alpine";
this._loggerWrapper.Helper.LogMessage(MessageImportance.Low, "Installing on Alpine Linux.");
Zav marked this conversation as resolved.
Show resolved Hide resolved
Zav marked this conversation as resolved.
Show resolved Hide resolved
}
}

var fileName = $"spectral-{osType}-{architecture}";

if (osType == "win")
{
fileName = "spectral.exe";
this._loggerWrapper.Helper.LogMessage(MessageImportance.Low, "Installing on Windows.");
Zav marked this conversation as resolved.
Show resolved Hide resolved
}

return fileName;
}

private async Task DownloadFileAsync(string url, string destination, CancellationToken cancellationToken)
{
if (File.Exists(destination))
Zav marked this conversation as resolved.
Show resolved Hide resolved
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
this._loggerWrapper.Helper.LogMessage(MessageImportance.Low, "File already exist");

return;
}

using var httpClient = new HttpClient();
Zav marked this conversation as resolved.
Show resolved Hide resolved
Zav marked this conversation as resolved.
Show resolved Hide resolved
using var response = await httpClient.GetAsync(url, cancellationToken);
Zav marked this conversation as resolved.
Show resolved Hide resolved
response.EnsureSuccessStatusCode();
Zav marked this conversation as resolved.
Show resolved Hide resolved
Zav marked this conversation as resolved.
Show resolved Hide resolved

if (!response.IsSuccessStatusCode)
{
using var retryResponse = await httpClient.GetAsync(url, cancellationToken);
Zav marked this conversation as resolved.
Show resolved Hide resolved

if (retryResponse.IsSuccessStatusCode)
{
await SaveFileFromResponseAsync(destination, retryResponse, cancellationToken);
}
else
{
throw new OpenApiTaskFailedException("Spectral could not be installed.");
Zav marked this conversation as resolved.
Show resolved Hide resolved
}
}
else
{
await SaveFileFromResponseAsync(destination, response, cancellationToken);
}
}

private static async Task SaveFileFromResponseAsync(string destination, HttpResponseMessage response, CancellationToken cancellationToken)
{
using var fileTarget = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None);
using var fileStream = await response.Content.ReadAsStreamAsync();

await fileStream.CopyToAsync(fileTarget, 1024, cancellationToken);
Zav marked this conversation as resolved.
Show resolved Hide resolved
}

private static string GetOperatingSystem()
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "macos";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "win";
}

return "unknown";
}

private static string GetArchitecture()
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
{
if (RuntimeInformation.OSArchitecture == Architecture.X64)
{
return "x64";
}

if (RuntimeInformation.OSArchitecture == Architecture.Arm64)
{
return "arm64";
}

return "unknown";
}
}
69 changes: 69 additions & 0 deletions src/Workleap.OpenApi.MSBuild/SwaggerManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;
using Workleap.OpenApi.MSBuild.Exceptions;

namespace Workleap.OpenApi.MSBuild;

internal sealed class SwaggerManager : ISwaggerManager
{
private const string SwaggerVersion = "6.5.0";
private readonly IProcessWrapper _processWrapper;
private readonly ILoggerWrapper _loggerWrapper;
private readonly string _openApiWebApiAssemblyPath;
private readonly string _swaggerDirectory;

public SwaggerManager(IProcessWrapper processWrapper, ILoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, string openApiWebApiAssemblyPath)
{
this._processWrapper = processWrapper;
this._loggerWrapper = loggerWrapper;
this._openApiWebApiAssemblyPath = openApiWebApiAssemblyPath;
this._swaggerDirectory = Path.Combine(openApiToolsDirectoryPath, $"swagger/{SwaggerVersion}");
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
}

public async Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken)
{
var swaggerExePath = Path.Combine(this._swaggerDirectory, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "swagger.exe" : "swagger");

foreach (var documentName in openApiSwaggerDocumentNames)
{
var outputOpenApiSpecName = $"openapi-{documentName.ToLowerInvariant()}.yaml";

var outputOpenApiSpecPath = Path.Combine(this._swaggerDirectory, outputOpenApiSpecName);
Zav marked this conversation as resolved.
Show resolved Hide resolved

await this.GenerateOpenApiSpecAsync(swaggerExePath, outputOpenApiSpecPath, documentName, cancellationToken);
}
}

public async Task InstallSwaggerCliAsync(CancellationToken cancellationToken)
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
var retryCount = 0;
while (retryCount < 2)
{
var exitCode = await this._processWrapper.RunProcessAsync("dotnet", new[] { "tool", "update", "Swashbuckle.AspNetCore.Cli", "--tool-path", this._swaggerDirectory, "--version", "6.5.0" }, cancellationToken);
ruvyas marked this conversation as resolved.
Show resolved Hide resolved

if (exitCode != 0 && retryCount != 1)
{
this._loggerWrapper.Helper.LogWarning("Swashbuckle download failed. Retrying once more...");
retryCount++;
continue;
}

if (retryCount == 1 && exitCode != 0)
{
throw new OpenApiTaskFailedException("Swashbuckle CLI could not be installed.");
}

break;
}
}

public async Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken)
{
var exitCode = await this._processWrapper.RunProcessAsync(swaggerExePath, new[] { "tofile", "--output", outputOpenApiSpecPath, "--yaml", this._openApiWebApiAssemblyPath, documentName }, cancellationToken);

if (exitCode != 0)
{
throw new OpenApiTaskFailedException("OpenApi file could not be created.");
Zav marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
62 changes: 54 additions & 8 deletions src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using Microsoft.Build.Framework;
using System.Runtime.InteropServices;
using CliWrap;
using CliWrap.Buffered;
using Microsoft.Build.Framework;
using Workleap.OpenApi.MSBuild.Exceptions;

namespace Workleap.OpenApi.MSBuild;

Expand All @@ -24,14 +28,56 @@ public sealed class ValidateOpenApiTask : CancelableAsyncTask
[Required]
public string[] OpenApiSpecificationFiles { get; set; } = Array.Empty<string>();

protected override Task<bool> ExecuteAsync(CancellationToken cancellationToken)
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
this.Log.LogMessage(MessageImportance.High, "OpenApiWebApiAssemblyPath = '{0}'", this.OpenApiWebApiAssemblyPath);
this.Log.LogMessage(MessageImportance.High, "OpenApiToolsDirectoryPath = '{0}'", this.OpenApiToolsDirectoryPath);
this.Log.LogMessage(MessageImportance.High, "OpenApiSpectralRulesetUrl = '{0}'", this.OpenApiSpectralRulesetUrl);
this.Log.LogMessage(MessageImportance.High, "OpenApiSwaggerDocumentNames = '{0}'", string.Join(", ", this.OpenApiSwaggerDocumentNames));
this.Log.LogMessage(MessageImportance.High, "OpenApiSpecificationFiles = '{0}'", string.Join(", ", this.OpenApiSpecificationFiles));
var loggerWrapper = new LoggerWrapper(this.Log);
var processWrapper = new ProcessWrapper(this.OpenApiToolsDirectoryPath);
var swaggerManager = new SwaggerManager(processWrapper, loggerWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiToolsDirectoryPath);
var spectralManager = new SpectralManager(processWrapper, loggerWrapper, this.OpenApiToolsDirectoryPath);

return Task.FromResult(true);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiWebApiAssemblyPath), this.OpenApiWebApiAssemblyPath);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiToolsDirectoryPath), this.OpenApiToolsDirectoryPath);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSpectralRulesetUrl), this.OpenApiSpectralRulesetUrl);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSwaggerDocumentNames), string.Join(", ", this.OpenApiSwaggerDocumentNames));
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSpecificationFiles), string.Join(", ", this.OpenApiSpecificationFiles));

if (this.OpenApiSpecificationFiles.Length != this.OpenApiSwaggerDocumentNames.Length)
{
this.Log.LogWarning("OpenApiSpecificationFiles and OpenApiSwaggerDocumentNames should have the same lenght", this.OpenApiWebApiAssemblyPath);
Zav marked this conversation as resolved.
Show resolved Hide resolved

return false;
}

try
{
await this.GeneratePublicNugetSource();

// Install Swagger CLI
await swaggerManager.InstallSwaggerCliAsync(cancellationToken);

// await swaggerManager.RunSwaggerAsync(this.OpenApiSwaggerDocumentNames, cancellationToken);
Zav marked this conversation as resolved.
Show resolved Hide resolved

// Install spectral
await spectralManager.InstallSpectralAsync(cancellationToken);

// Install oasdiff
}
catch (OpenApiTaskFailedException e)
{
this.Log.LogWarning("OpenApi validation could not be done. {0}", e.Message);
Zav marked this conversation as resolved.
Show resolved Hide resolved
}

return true;
}

private async Task GeneratePublicNugetSource()
{
Directory.CreateDirectory(this.OpenApiToolsDirectoryPath);

if (!File.Exists(Path.Combine(this.OpenApiToolsDirectoryPath, "nuget.config")))
{
var path = Path.Combine(this.OpenApiToolsDirectoryPath, "nuget.config");
File.WriteAllText(path, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n <clear />\n <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\n </packageSources>\n</configuration>");
}
}
}