|
| 1 | +// Licensed to Elasticsearch B.V under one or more agreements. |
| 2 | +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. |
| 3 | +// See the LICENSE file in the project root for more information |
| 4 | + |
| 5 | +using System.IO.Abstractions; |
| 6 | +using System.Text.Json; |
| 7 | +using ConsoleAppFramework; |
| 8 | +using Documentation.Assembler.Deploying.Serialization; |
| 9 | +using Elastic.Documentation.Diagnostics; |
| 10 | +using Elastic.Documentation.Tooling.ExternalCommands; |
| 11 | + |
| 12 | +namespace Documentation.Assembler.Deploying; |
| 13 | + |
| 14 | +internal enum KvsOperation |
| 15 | +{ |
| 16 | + Puts, |
| 17 | + Deletes |
| 18 | +} |
| 19 | + |
| 20 | +public class AwsCloudFrontKeyValueStoreProxy(DiagnosticsCollector collector, IDirectoryInfo workingDirectory) : ExternalCommandExecutor(collector, workingDirectory) |
| 21 | +{ |
| 22 | + public void UpdateRedirects(string kvsName, IReadOnlyDictionary<string, string> sourcedRedirects) |
| 23 | + { |
| 24 | + var (kvsArn, eTag) = DescribeKeyValueStore(kvsName); |
| 25 | + if (string.IsNullOrEmpty(kvsArn) || string.IsNullOrEmpty(eTag)) |
| 26 | + return; |
| 27 | + |
| 28 | + var existingRedirects = ListAllKeys(kvsArn); |
| 29 | + |
| 30 | + var toPut = sourcedRedirects |
| 31 | + .Select(kvp => new PutKeyRequestListItem { Key = kvp.Key, Value = kvp.Value }); |
| 32 | + var toDelete = existingRedirects |
| 33 | + .Except(sourcedRedirects.Keys) |
| 34 | + .Select(k => new DeleteKeyRequestListItem { Key = k }); |
| 35 | + |
| 36 | + eTag = ProcessBatchUpdates(kvsArn, eTag, toPut, KvsOperation.Puts); |
| 37 | + _ = ProcessBatchUpdates(kvsArn, eTag, toDelete, KvsOperation.Deletes); |
| 38 | + } |
| 39 | + |
| 40 | + private (string? Arn, string? ETag) DescribeKeyValueStore(string kvsName) |
| 41 | + { |
| 42 | + ConsoleApp.Log("Describing KeyValueStore"); |
| 43 | + try |
| 44 | + { |
| 45 | + var json = Capture("aws", "cloudfront", "describe-key-value-store", "--name", kvsName, "|", "jq", "-c"); |
| 46 | + var describeResponse = JsonSerializer.Deserialize<DescribeKeyValueStoreResponse>(json, AwsCloudFrontKeyValueStoreJsonContext.Default.DescribeKeyValueStoreResponse); |
| 47 | + if (describeResponse?.ETag is not null && describeResponse.KeyValueStore is { ARN.Length: > 0 }) |
| 48 | + return (describeResponse.KeyValueStore.ARN, describeResponse.ETag); |
| 49 | + |
| 50 | + Collector.EmitError("", "Could not deserialize the DescribeKeyValueStoreResponse"); |
| 51 | + return (null, null); |
| 52 | + } |
| 53 | + catch (Exception e) |
| 54 | + { |
| 55 | + Collector.EmitError("", "An error occurred while describing the KeyValueStore", e); |
| 56 | + return (null, null); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + private HashSet<string> ListAllKeys(string kvsArn) |
| 61 | + { |
| 62 | + ConsoleApp.Log("Acquiring existing redirects"); |
| 63 | + var allKeys = new HashSet<string>(); |
| 64 | + string[] baseArgs = ["cloudfront-keyvaluestore", "list-keys", "--kvs-arn", kvsArn]; |
| 65 | + string? nextToken = null; |
| 66 | + try |
| 67 | + { |
| 68 | + do |
| 69 | + { |
| 70 | + var json = Capture("aws", [.. baseArgs, .. nextToken is not null ? (string[])["--starting-token", nextToken] : [], "|", "jq", "-c"]); |
| 71 | + var response = JsonSerializer.Deserialize<ListKeysResponse>(json, AwsCloudFrontKeyValueStoreJsonContext.Default.ListKeysResponse); |
| 72 | + |
| 73 | + if (response?.Items != null) |
| 74 | + { |
| 75 | + foreach (var item in response.Items) |
| 76 | + _ = allKeys.Add(item.Key); |
| 77 | + } |
| 78 | + |
| 79 | + nextToken = response?.NextToken; |
| 80 | + } while (!string.IsNullOrEmpty(nextToken)); |
| 81 | + } |
| 82 | + catch (Exception e) |
| 83 | + { |
| 84 | + Collector.EmitError("", "An error occurred while acquiring existing redirects in the KeyValueStore", e); |
| 85 | + return []; |
| 86 | + } |
| 87 | + return allKeys; |
| 88 | + } |
| 89 | + |
| 90 | + |
| 91 | + private string ProcessBatchUpdates( |
| 92 | + string kvsArn, |
| 93 | + string eTag, |
| 94 | + IEnumerable<object> items, |
| 95 | + KvsOperation operation) |
| 96 | + { |
| 97 | + const int batchSize = 50; |
| 98 | + ConsoleApp.Log($"Processing {items.Count()} items in batches of {batchSize} for {operation} update operation."); |
| 99 | + try |
| 100 | + { |
| 101 | + foreach (var batch in items.Chunk(batchSize)) |
| 102 | + { |
| 103 | + var payload = operation switch |
| 104 | + { |
| 105 | + KvsOperation.Puts => JsonSerializer.Serialize(batch.Cast<PutKeyRequestListItem>().ToList(), |
| 106 | + AwsCloudFrontKeyValueStoreJsonContext.Default.ListPutKeyRequestListItem), |
| 107 | + KvsOperation.Deletes => JsonSerializer.Serialize(batch.Cast<DeleteKeyRequestListItem>().ToList(), |
| 108 | + AwsCloudFrontKeyValueStoreJsonContext.Default.ListDeleteKeyRequestListItem), |
| 109 | + _ => string.Empty |
| 110 | + }; |
| 111 | + var responseJson = Capture(false, 1, "aws", "cloudfront-keyvaluestore", "update-keys", "--kvs-arn", kvsArn, "--if-match", eTag, |
| 112 | + $"--{operation.ToString().ToLowerInvariant()}", "--payload", payload, "|", "jq", "-c"); |
| 113 | + var updateResponse = JsonSerializer.Deserialize<UpdateKeysResponse>(responseJson, AwsCloudFrontKeyValueStoreJsonContext.Default.UpdateKeysResponse); |
| 114 | + |
| 115 | + if (string.IsNullOrEmpty(updateResponse?.ETag)) |
| 116 | + throw new Exception("Failed to get new ETag after update operation."); |
| 117 | + |
| 118 | + eTag = updateResponse.ETag; |
| 119 | + } |
| 120 | + } |
| 121 | + catch (Exception e) |
| 122 | + { |
| 123 | + Collector.EmitError("", $"An error occurred while performing a {operation} update to the KeyValueStore", e); |
| 124 | + } |
| 125 | + return eTag; |
| 126 | + } |
| 127 | +} |
0 commit comments