Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -354,33 +354,9 @@ public static async Task<bool> ValidateFusionConfigurationAsync(
throw Exit(
"Your request is ready for the composition. Run `fusion-configuration publish start`.");

case IFusionConfigurationValidationFailed { Errors: var errors }:
case IFusionConfigurationValidationFailed validationFailed:
var errorTree = new Tree("");

foreach (var error in errors)
{
switch (error)
{
case ISchemaVersionChangeViolationError e:
errorTree.AddSchemaVersionChangeViolations(e);
break;
case IInvalidGraphQLSchemaError e:
errorTree.AddGraphQLSchemaErrors(e);
break;
case IPersistedQueryValidationError e:
errorTree.AddPersistedQueryValidationErrorsWithClients(e);
break;
case IOpenApiCollectionValidationError e:
errorTree.AddOpenApiCollectionValidationErrors(e);
break;
case IMcpFeatureCollectionValidationError e:
errorTree.AddMcpFeatureCollectionValidationErrors(e);
break;
case IUnexpectedProcessingError e:
errorTree.AddErrorMessage(e.Message);
break;
}
}
errorTree.AddFusionConfigurationValidationErrors(validationFailed);

activity.Fail(errorTree, "Fusion configuration failed validation.");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
#if !NET9_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.IO.Compression;
using ChilliCream.Nitro.Client;
using ChilliCream.Nitro.Client.FusionConfiguration;
using ChilliCream.Nitro.Client.Schemas;
using ChilliCream.Nitro.CommandLine.Commands.Schemas;
using ChilliCream.Nitro.CommandLine.FusionCompatibility;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Services;
using ChilliCream.Nitro.CommandLine.Services.Sessions;
using HotChocolate.Fusion.Packaging;
using static ChilliCream.Nitro.CommandLine.ThrowHelper;

namespace ChilliCream.Nitro.CommandLine.Commands.Fusion;

Expand Down Expand Up @@ -49,7 +46,6 @@ private static async Task<int> ExecuteAsync(
{
var console = services.GetRequiredService<INitroConsole>();
var fusionConfigurationClient = services.GetRequiredService<IFusionConfigurationClient>();
var schemasClient = services.GetRequiredService<ISchemasClient>();
var fileSystem = services.GetRequiredService<IFileSystem>();
var sessionService = services.GetRequiredService<ISessionService>();

Expand Down Expand Up @@ -150,53 +146,71 @@ async Task<int> ValidateWithSourceSchemaFiles()

async Task<int> ValidateAsync(INitroConsoleActivity activity, Stream archiveStream)
{
IDisposable disposableArchive;
Stream schemaStream;
var result = await fusionConfigurationClient.StartFusionConfigurationValidationAsync(
apiId,
stageName,
archiveStream,
source: null,
ct);

if (IsFarFormat(archiveStream))
if (result.Errors?.Count > 0)
{
var archive = FusionArchive.Open(archiveStream, leaveOpen: true);

schemaStream = await LoadSchemaFile(archive, ct);
foreach (var error in result.Errors)
{
var errorMessage = error switch
{
IUnauthorizedOperation err => err.Message,
IInvalidSourceMetadataInputError err => err.Message,
IApiNotFoundError err => throw new NitroClientNotFoundException(err.Message),
IError err => Messages.UnexpectedMutationError(err),
_ => Messages.UnexpectedMutationError()
};

console.Error.WriteErrorLine(errorMessage);
}

disposableArchive = archive;
throw new ExitException();
}
else
{
var package = FusionGraphPackage.Open(archiveStream, FileAccess.Read);

schemaStream = await LoadSchemaFile(package, ct);
var requestId = result.Id;

disposableArchive = package;
if (string.IsNullOrWhiteSpace(requestId))
{
throw MutationReturnedNoData();
}

try
activity.Update($"Validation request created. {$"(ID: {requestId.EscapeMarkup()})".Dim()}");

await foreach (var @event in fusionConfigurationClient
.SubscribeToFusionConfigurationValidationAsync(requestId, ct))
{
var validationResult = await SchemaHelpers.ValidateSchemaAsync(
activity,
console,
schemasClient,
apiId,
stageName,
schemaStream,
source: null,
ct);

if (validationResult is SchemaValidationResult.Failed failed)
switch (@event)
{
activity.Fail(failed.Details, "Fusion configuration failed validation.");
case IFusionConfigurationValidationFailed validationFailed:
var errorTree = new Tree("");
errorTree.AddFusionConfigurationValidationErrors(validationFailed);

throw new ExitException("Fusion configuration failed validation.");
}
activity.Fail(errorTree, "Fusion configuration failed validation.");

activity.Success("Fusion configuration passed validation.");
throw Exit("Fusion configuration failed validation.");

return ExitCodes.Success;
}
finally
{
disposableArchive.Dispose();
case IFusionConfigurationValidationSuccess:
activity.Success("Fusion configuration passed validation.");

return ExitCodes.Success;

case IOperationInProgress:
case IValidationInProgress:
activity.Update(Messages.Validating);
break;

default:
activity.Update(Messages.UnknownServerResponse, ActivityUpdateKind.Warning);
break;
}
}

throw Exit(Messages.UnknownServerResponse);
}

INitroConsoleActivity StartActivity()
Expand All @@ -206,46 +220,4 @@ INitroConsoleActivity StartActivity()
"Failed to validate the Fusion configuration.");
}
}

private static async Task<Stream> LoadSchemaFile(FusionArchive archive, CancellationToken ct)
{
var latestVersion = await archive.GetLatestSupportedGatewayFormatAsync(ct);
var configuration = await archive.TryGetGatewayConfigurationAsync(latestVersion, ct);

if (configuration is null)
{
throw new InvalidOperationException(
$"Failed to retrieve gateway configuration from the Fusion archive (format version: {latestVersion}). "
+ "The archive may be corrupted, unsupported, or missing required configuration.");
}

return await configuration.OpenReadSchemaAsync(ct);
}

private static async Task<Stream> LoadSchemaFile(FusionGraphPackage package, CancellationToken ct)
{
var schemaNode = await package.GetSchemaAsync(ct);

var schemaFileStream = new MemoryStream();
await using var streamWriter = new StreamWriter(schemaFileStream, leaveOpen: true);
await streamWriter.WriteAsync(schemaNode.ToString());
await streamWriter.FlushAsync(ct);
schemaFileStream.Position = 0;

return schemaFileStream;
}

public static bool IsFarFormat(Stream stream)
{
try
{
using var zip = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);

return zip.GetEntry("archive-metadata.json") is not null;
}
catch
{
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,44 @@ static string GetMcpEntityNodeHeading(IMcpFeatureCollectionValidationEntity enti
}
}

public static IHasTreeNodes AddFusionConfigurationValidationErrors(
this IHasTreeNodes node,
IFusionConfigurationValidationFailed validationFailed)
{
foreach (var error in validationFailed.Errors)
{
switch (error)
{
case ISchemaVersionChangeViolationError e:
node.AddSchemaVersionChangeViolations(e);
break;
case IInvalidGraphQLSchemaError e:
node.AddGraphQLSchemaErrors(e);
break;
case IPersistedQueryValidationError e:
node.AddPersistedQueryValidationErrorsWithClients(e);
break;
case IOpenApiCollectionValidationError e:
node.AddOpenApiCollectionValidationErrors(e);
break;
case IMcpFeatureCollectionValidationError e:
node.AddMcpFeatureCollectionValidationErrors(e);
break;
case IUnexpectedProcessingError e:
node.AddErrorMessage(e.Message);
break;
case IProcessingTimeoutError e:
node.AddErrorMessage(e.Message);
break;
case IReadyTimeoutError e:
node.AddErrorMessage(e.Message);
break;
}
Comment thread
tobias-tengler marked this conversation as resolved.
Resolved
}

return node;
}

public static IHasTreeNodes AddStagePublishedDependencies(
this IHasTreeNodes node,
IStagesHavePublishedDependenciesError error)
Expand Down
Loading
Loading