diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3833731f1..1f67f49a5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,6 +13,8 @@
+
+
@@ -70,4 +72,4 @@
-
+
\ No newline at end of file
diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
index a0067ac6f..e9c9926f9 100644
--- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
+++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
@@ -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;
@@ -18,4 +19,5 @@ namespace Elastic.Documentation.Serialization;
[JsonSerializable(typeof(LinkRegistry))]
[JsonSerializable(typeof(LinkRegistryEntry))]
[JsonSerializable(typeof(DocumentationDocument))]
+[JsonSerializable(typeof(FrozenDictionary))]
public sealed partial class SourceGenerationContext : JsonSerializerContext;
diff --git a/src/tooling/docs-assembler/Building/AssemblerBuilder.cs b/src/tooling/docs-assembler/Building/AssemblerBuilder.cs
index 5ec32f453..0f60bebb4 100644
--- a/src/tooling/docs-assembler/Building/AssemblerBuilder.cs
+++ b/src/tooling/docs-assembler/Building/AssemblerBuilder.cs
@@ -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;
@@ -85,6 +87,8 @@ public async Task BuildAllAsync(FrozenDictionary await e.StopAsync(ctx));
await Task.WhenAll(tasks);
}
@@ -143,4 +147,16 @@ private void SetFeatureFlags(AssemblerDocumentationSet set)
set.DocumentationSet.Configuration.Features.Set(configurationFeatureFlag.Key, configurationFeatureFlag.Value);
}
}
+
+ private async Task OutputRedirectsAsync(Dictionary redirects, Cancel ctx)
+ {
+ var uniqueRedirects = redirects
+ .Where(x => !x.Key.TrimEnd('/').Equals(x.Value.TrimEnd('/')))
+ .ToFrozenDictionary();
+ 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);
+ }
}
diff --git a/src/tooling/docs-assembler/Cli/DeployCommands.cs b/src/tooling/docs-assembler/Cli/DeployCommands.cs
index 6a021b17d..0cb88d09c 100644
--- a/src/tooling/docs-assembler/Cli/DeployCommands.cs
+++ b/src/tooling/docs-assembler/Cli/DeployCommands.cs
@@ -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;
namespace Documentation.Assembler.Cli;
@@ -94,4 +101,112 @@ public async Task Apply(
await collector.StopAsync(ctx);
return collector.Errors;
}
+
+ /// Refreshes the redirects mapping in Cloudfront's KeyValueStore
+ /// The environment to build
+ /// Path to the redirects mapping pre-generated by docs-assembler
+ ///
+ [Command("update-redirects")]
+ [ConsoleAppFilter]
+ [ConsoleAppFilter]
+ public async Task 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();
+
+ 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
+ .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;
+
+ eTag = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toPut, batchSize, "Puts", ctx);
+ _ = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toDelete, batchSize, "Deletes", ctx);
+
+ await collector.StopAsync(ctx);
+ return collector.Errors;
+ }
+
+ private static async Task ProcessBatchUpdatesAsync(
+ IAmazonCloudFrontKeyValueStore kvsClient,
+ string kvsArn,
+ string eTag,
+ IEnumerable