Skip to content

Real redirects support #1397

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<PackageVersion Include="Amazon.Lambda.S3Events" Version="3.1.0" />
<PackageVersion Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4" />
<PackageVersion Include="Amazon.Lambda.SQSEvents" Version="2.2.0" />
<PackageVersion Include="AWSSDK.CloudFront" Version="4.0.0.10" />
<PackageVersion Include="AWSSDK.CloudFrontKeyValueStore" Version="4.0.0.9" />
<PackageVersion Include="AWSSDK.Core" Version="4.0.0.2" />
<PackageVersion Include="AWSSDK.SQS" Version="4.0.0.1" />
<PackageVersion Include="AWSSDK.S3" Version="4.0.0.1" />
Expand Down Expand Up @@ -70,4 +72,4 @@
</PackageVersion>
<PackageVersion Include="xunit.v3" Version="2.0.2" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using System.Text.Json.Serialization;
using Elastic.Documentation.Links;
using Elastic.Documentation.Search;
Expand All @@ -18,4 +19,5 @@ namespace Elastic.Documentation.Serialization;
[JsonSerializable(typeof(LinkRegistry))]
[JsonSerializable(typeof(LinkRegistryEntry))]
[JsonSerializable(typeof(DocumentationDocument))]
[JsonSerializable(typeof(FrozenDictionary<string, string>))]
public sealed partial class SourceGenerationContext : JsonSerializerContext;
16 changes: 16 additions & 0 deletions src/tooling/docs-assembler/Building/AssemblerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using System.Text.Json;
using Documentation.Assembler.Exporters;
using Documentation.Assembler.Navigation;
using Elastic.Documentation.Legacy;
using Elastic.Documentation.Links;
using Elastic.Documentation.Serialization;
using Elastic.Markdown;
using Elastic.Markdown.Exporters;
using Elastic.Markdown.Links.CrossLinks;
Expand Down Expand Up @@ -85,6 +87,8 @@ public async Task BuildAllAsync(FrozenDictionary<string, AssemblerDocumentationS
}
}

await OutputRedirectsAsync(redirects, ctx);

tasks = markdownExporters.Select(async e => await e.StopAsync(ctx));
await Task.WhenAll(tasks);
}
Expand Down Expand Up @@ -143,4 +147,16 @@ private void SetFeatureFlags(AssemblerDocumentationSet set)
set.DocumentationSet.Configuration.Features.Set(configurationFeatureFlag.Key, configurationFeatureFlag.Value);
}
}

private async Task OutputRedirectsAsync(Dictionary<string, string> redirects, Cancel ctx)
{
var uniqueRedirects = redirects
.Where(x => !x.Key.TrimEnd('/').Equals(x.Value.TrimEnd('/')))
Copy link
Member

Choose a reason for hiding this comment

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

Can we ensure CollectRedirects never adds these self referential redirects?

.ToFrozenDictionary();
Copy link
Member

Choose a reason for hiding this comment

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

nit: FrozenDictionary primary usecase is for dictionaries that live throughout the lifetime of the application a simple ToDictionary() suffices here and is slighltly faster to construct. nano-optimization nit though.

Copy link
Member

Choose a reason for hiding this comment

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

That way we can also rely on the regular dictionary serialization.

var redirectsFile = context.WriteFileSystem.FileInfo.New(Path.Combine(context.OutputDirectory.FullName, "redirects.json"));
_logger.LogInformation("Writing {Count} resolved redirects to {Path}", uniqueRedirects.Count, redirectsFile.FullName);

var redirectsJson = JsonSerializer.Serialize(uniqueRedirects, SourceGenerationContext.Default.FrozenDictionaryStringString);
await context.WriteFileSystem.File.WriteAllTextAsync(redirectsFile.FullName, redirectsJson, ctx);
}
}
115 changes: 115 additions & 0 deletions src/tooling/docs-assembler/Cli/DeployCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@

using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.Text.Json;
using Actions.Core.Services;
using Amazon.CloudFront;
using Amazon.CloudFrontKeyValueStore;
using Amazon.CloudFrontKeyValueStore.Model;
using Amazon.S3;
using Amazon.S3.Transfer;
using ConsoleAppFramework;
using Documentation.Assembler.Deploying;
using Elastic.Documentation.Serialization;
using Elastic.Documentation.Tooling.Diagnostics.Console;
using Elastic.Documentation.Tooling.Filters;
using Microsoft.Extensions.Logging;
using DescribeKeyValueStoreRequest = Amazon.CloudFront.Model.DescribeKeyValueStoreRequest;
Copy link
Member

Choose a reason for hiding this comment

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

super nit: why the type alias here?


namespace Documentation.Assembler.Cli;

Expand Down Expand Up @@ -94,4 +101,112 @@ public async Task<int> Apply(
await collector.StopAsync(ctx);
return collector.Errors;
}

/// <summary>Refreshes the redirects mapping in Cloudfront's KeyValueStore</summary>
/// <param name="environment">The environment to build</param>
/// <param name="redirectsFile">Path to the redirects mapping pre-generated by docs-assembler</param>
/// <param name="ctx"></param>
[Command("update-redirects")]
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiousity do you know how long this takes?

[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task<int> UpdateRedirects(
string environment,
string redirectsFile = ".artifacts/assembly/redirects.json",
Cancel ctx = default)
{
AssignOutputLogger();
await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService)
{
NoHints = true
}.StartAsync(ctx);

if (!File.Exists(redirectsFile))
{
collector.EmitError(redirectsFile, "Redirects mapping does not exist.");
await collector.StopAsync(ctx);
return collector.Errors;
}

ConsoleApp.Log("Parsing redirects mapping");
var jsonContent = await File.ReadAllTextAsync(redirectsFile, ctx);
var sourcedRedirects = JsonSerializer.Deserialize(jsonContent, SourceGenerationContext.Default.FrozenDictionaryStringString);

if (sourcedRedirects is null)
{
collector.EmitError(redirectsFile, "Redirects mapping is invalid.");
await collector.StopAsync(ctx);
return collector.Errors;
}

var kvsName = $"elastic-docs-v3-{environment}-redirects-kvs";

var cfClient = new AmazonCloudFrontClient();
var kvsClient = new AmazonCloudFrontKeyValueStoreClient();

ConsoleApp.Log("Describing KVS");
var describeResponse = await cfClient.DescribeKeyValueStoreAsync(new DescribeKeyValueStoreRequest { Name = kvsName }, ctx);

var kvsArn = describeResponse.KeyValueStore.ARN;
var eTag = describeResponse.ETag;
var existingRedirects = new HashSet<string>();

var listKeysRequest = new ListKeysRequest { KvsARN = kvsArn };
ListKeysResponse listKeysResponse;

do
{
listKeysResponse = await kvsClient.ListKeysAsync(listKeysRequest, ctx);
foreach (var item in listKeysResponse.Items)
_ = existingRedirects.Add(item.Key);
listKeysRequest.NextToken = listKeysResponse.NextToken;
}
while (!string.IsNullOrEmpty(listKeysResponse.NextToken));

var toPut = sourcedRedirects
Copy link
Member

Choose a reason for hiding this comment

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

Should we add some error if this happens to go over the limit?

If it goes over do we need N keyvalue stores?

.Select(kvp => new PutKeyRequestListItem { Key = kvp.Key, Value = kvp.Value });
var toDelete = existingRedirects
.Except(sourcedRedirects.Keys)
.Select(k => new DeleteKeyRequestListItem { Key = k });

ConsoleApp.Log("Updating redirects in KVS");
const int batchSize = 500;
Copy link
Member

Choose a reason for hiding this comment

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

Just out of curiousity, did you play with this to observe overall runtime of the command?


eTag = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toPut, batchSize, "Puts", ctx);
Copy link
Member

Choose a reason for hiding this comment

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

No magic strings Puts Deletes please!

_ = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toDelete, batchSize, "Deletes", ctx);

await collector.StopAsync(ctx);
return collector.Errors;
}

private static async Task<string> ProcessBatchUpdatesAsync(
IAmazonCloudFrontKeyValueStore kvsClient,
string kvsArn,
string eTag,
IEnumerable<object> items,
int batchSize,
string operation,
Cancel ctx)
{
var enumerable = items.ToList();
for (var i = 0; i < enumerable.Count; i += batchSize)
{
var batch = enumerable.Skip(i).Take(batchSize);
var updateRequest = new UpdateKeysRequest
{
KvsARN = kvsArn,
IfMatch = eTag
};

if (operation.Equals("Puts", StringComparison.InvariantCulture))
updateRequest.Puts = batch.Cast<PutKeyRequestListItem>().ToList();
else if (operation.Equals("Deletes", StringComparison.InvariantCulture))
updateRequest.Deletes = batch.Cast<DeleteKeyRequestListItem>().ToList();

var update = await kvsClient.UpdateKeysAsync(updateRequest, ctx);
eTag = update.ETag;
}

return eTag;
}

}
4 changes: 4 additions & 0 deletions src/tooling/docs-assembler/Cli/RepositoryCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public async Task<int> BuildAll(

await cloner.WriteLinkRegistrySnapshot(checkoutResult.LinkRegistrySnapshot, ctx);

var redirectsPath = Path.Combine(assembleContext.OutputDirectory.FullName, "redirects.json");
if (File.Exists(redirectsPath))
await githubActionsService.SetOutputAsync("redirects_artifact_path", redirectsPath);
Copy link
Member

Choose a reason for hiding this comment

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

This will also need to be declared in the github action as output, will satisfy IDE checks nicely :)


var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, assembleContext.OutputDirectory);
sitemapBuilder.Generate();

Expand Down
2 changes: 2 additions & 0 deletions src/tooling/docs-assembler/docs-assembler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.CloudFront" />
<PackageReference Include="AWSSDK.CloudFrontKeyValueStore" />
<PackageReference Include="AWSSDK.S3"/>
<PackageReference Include="ConsoleAppFramework.Abstractions"/>
<PackageReference Include="ConsoleAppFramework" />
Expand Down
Loading