|
4 | 4 |
|
5 | 5 | using System.Diagnostics.CodeAnalysis;
|
6 | 6 | using System.IO.Abstractions;
|
| 7 | +using System.Text.Json; |
7 | 8 | using Actions.Core.Services;
|
| 9 | +using Amazon.CloudFront; |
| 10 | +using Amazon.CloudFrontKeyValueStore; |
| 11 | +using Amazon.CloudFrontKeyValueStore.Model; |
8 | 12 | using Amazon.S3;
|
9 | 13 | using Amazon.S3.Transfer;
|
10 | 14 | using ConsoleAppFramework;
|
11 | 15 | using Documentation.Assembler.Deploying;
|
| 16 | +using Elastic.Documentation.Serialization; |
12 | 17 | using Elastic.Documentation.Tooling.Diagnostics.Console;
|
| 18 | +using Elastic.Documentation.Tooling.Filters; |
13 | 19 | using Microsoft.Extensions.Logging;
|
14 | 20 |
|
15 | 21 | namespace Documentation.Assembler.Cli;
|
16 | 22 |
|
| 23 | +internal enum KvsOperation |
| 24 | +{ |
| 25 | + Puts, |
| 26 | + Deletes |
| 27 | +} |
| 28 | + |
17 | 29 | internal sealed class DeployCommands(ILoggerFactory logger, ICoreService githubActionsService)
|
18 | 30 | {
|
19 | 31 | [SuppressMessage("Usage", "CA2254:Template should be a static expression")]
|
@@ -94,4 +106,112 @@ public async Task<int> Apply(
|
94 | 106 | await collector.StopAsync(ctx);
|
95 | 107 | return collector.Errors;
|
96 | 108 | }
|
| 109 | + |
| 110 | + /// <summary>Refreshes the redirects mapping in Cloudfront's KeyValueStore</summary> |
| 111 | + /// <param name="environment">The environment to build</param> |
| 112 | + /// <param name="redirectsFile">Path to the redirects mapping pre-generated by docs-assembler</param> |
| 113 | + /// <param name="ctx"></param> |
| 114 | + [Command("update-redirects")] |
| 115 | + [ConsoleAppFilter<StopwatchFilter>] |
| 116 | + [ConsoleAppFilter<CatchExceptionFilter>] |
| 117 | + public async Task<int> UpdateRedirects( |
| 118 | + string environment, |
| 119 | + string redirectsFile = ".artifacts/assembly/redirects.json", |
| 120 | + Cancel ctx = default) |
| 121 | + { |
| 122 | + AssignOutputLogger(); |
| 123 | + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService) |
| 124 | + { |
| 125 | + NoHints = true |
| 126 | + }.StartAsync(ctx); |
| 127 | + |
| 128 | + if (!File.Exists(redirectsFile)) |
| 129 | + { |
| 130 | + collector.EmitError(redirectsFile, "Redirects mapping does not exist."); |
| 131 | + await collector.StopAsync(ctx); |
| 132 | + return collector.Errors; |
| 133 | + } |
| 134 | + |
| 135 | + ConsoleApp.Log("Parsing redirects mapping"); |
| 136 | + var jsonContent = await File.ReadAllTextAsync(redirectsFile, ctx); |
| 137 | + var sourcedRedirects = JsonSerializer.Deserialize(jsonContent, SourceGenerationContext.Default.DictionaryStringString); |
| 138 | + |
| 139 | + if (sourcedRedirects is null) |
| 140 | + { |
| 141 | + collector.EmitError(redirectsFile, "Redirects mapping is invalid."); |
| 142 | + await collector.StopAsync(ctx); |
| 143 | + return collector.Errors; |
| 144 | + } |
| 145 | + |
| 146 | + var kvsName = $"elastic-docs-v3-{environment}-redirects-kvs"; |
| 147 | + |
| 148 | + var cfClient = new AmazonCloudFrontClient(); |
| 149 | + var kvsClient = new AmazonCloudFrontKeyValueStoreClient(); |
| 150 | + |
| 151 | + ConsoleApp.Log("Describing KVS"); |
| 152 | + var describeResponse = await cfClient.DescribeKeyValueStoreAsync(new Amazon.CloudFront.Model.DescribeKeyValueStoreRequest { Name = kvsName }, ctx); |
| 153 | + |
| 154 | + var kvsArn = describeResponse.KeyValueStore.ARN; |
| 155 | + var eTag = describeResponse.ETag; |
| 156 | + var existingRedirects = new HashSet<string>(); |
| 157 | + |
| 158 | + var listKeysRequest = new ListKeysRequest { KvsARN = kvsArn }; |
| 159 | + ListKeysResponse listKeysResponse; |
| 160 | + |
| 161 | + do |
| 162 | + { |
| 163 | + listKeysResponse = await kvsClient.ListKeysAsync(listKeysRequest, ctx); |
| 164 | + foreach (var item in listKeysResponse.Items) |
| 165 | + _ = existingRedirects.Add(item.Key); |
| 166 | + listKeysRequest.NextToken = listKeysResponse.NextToken; |
| 167 | + } |
| 168 | + while (!string.IsNullOrEmpty(listKeysResponse.NextToken)); |
| 169 | + |
| 170 | + var toPut = sourcedRedirects |
| 171 | + .Select(kvp => new PutKeyRequestListItem { Key = kvp.Key, Value = kvp.Value }); |
| 172 | + var toDelete = existingRedirects |
| 173 | + .Except(sourcedRedirects.Keys) |
| 174 | + .Select(k => new DeleteKeyRequestListItem { Key = k }); |
| 175 | + |
| 176 | + ConsoleApp.Log("Updating redirects in KVS"); |
| 177 | + const int batchSize = 50; |
| 178 | + |
| 179 | + eTag = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toPut, batchSize, KvsOperation.Puts, ctx); |
| 180 | + _ = await ProcessBatchUpdatesAsync(kvsClient, kvsArn, eTag, toDelete, batchSize, KvsOperation.Deletes, ctx); |
| 181 | + |
| 182 | + await collector.StopAsync(ctx); |
| 183 | + return collector.Errors; |
| 184 | + } |
| 185 | + |
| 186 | + private static async Task<string> ProcessBatchUpdatesAsync( |
| 187 | + IAmazonCloudFrontKeyValueStore kvsClient, |
| 188 | + string kvsArn, |
| 189 | + string eTag, |
| 190 | + IEnumerable<object> items, |
| 191 | + int batchSize, |
| 192 | + KvsOperation operation, |
| 193 | + Cancel ctx) |
| 194 | + { |
| 195 | + var enumerable = items.ToList(); |
| 196 | + for (var i = 0; i < enumerable.Count; i += batchSize) |
| 197 | + { |
| 198 | + var batch = enumerable.Skip(i).Take(batchSize); |
| 199 | + var updateRequest = new UpdateKeysRequest |
| 200 | + { |
| 201 | + KvsARN = kvsArn, |
| 202 | + IfMatch = eTag |
| 203 | + }; |
| 204 | + |
| 205 | + if (operation is KvsOperation.Puts) |
| 206 | + updateRequest.Puts = batch.Cast<PutKeyRequestListItem>().ToList(); |
| 207 | + else if (operation is KvsOperation.Deletes) |
| 208 | + updateRequest.Deletes = batch.Cast<DeleteKeyRequestListItem>().ToList(); |
| 209 | + |
| 210 | + var update = await kvsClient.UpdateKeysAsync(updateRequest, ctx); |
| 211 | + eTag = update.ETag; |
| 212 | + } |
| 213 | + |
| 214 | + return eTag; |
| 215 | + } |
| 216 | + |
97 | 217 | }
|
0 commit comments