Skip to content

Commit 9dddf81

Browse files
authored
Track if remote listing completed succesully when validating incremental deploy plan (#1817)
* Track if remote listing completed succesully when validating incremental deploy plan * Ensure we can incrementally deploy to empty remote s3 bucket
1 parent fdc4425 commit 9dddf81

File tree

5 files changed

+55
-8
lines changed

5 files changed

+55
-8
lines changed

src/tooling/docs-assembler/Cli/DeployCommands.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ private void AssignOutputLogger()
4040
/// <param name="out"> The file to write the plan to</param>
4141
/// <param name="deleteThreshold"> The percentage of deletions allowed in the plan as percentage of total files to sync</param>
4242
/// <param name="ctx"></param>
43+
[Command("plan")]
4344
public async Task<int> Plan(
4445
string environment,
4546
string s3BucketName,
@@ -58,7 +59,7 @@ public async Task<int> Plan(
5859
var s3Client = new AmazonS3Client();
5960
var planner = new AwsS3SyncPlanStrategy(logFactory, s3Client, s3BucketName, assembleContext);
6061
var plan = await planner.Plan(deleteThreshold, ctx);
61-
_logger.LogInformation("Total files to sync: {TotalFiles}", plan.TotalSyncRequests);
62+
_logger.LogInformation("Remote listing completed: {RemoteListingCompleted}", plan.RemoteListingCompleted);
6263
_logger.LogInformation("Total files to delete: {DeleteCount}", plan.DeleteRequests.Count);
6364
_logger.LogInformation("Total files to add: {AddCount}", plan.AddRequests.Count);
6465
_logger.LogInformation("Total files to update: {UpdateCount}", plan.UpdateRequests.Count);
@@ -70,7 +71,7 @@ public async Task<int> Plan(
7071
if (!validationResult.Valid)
7172
{
7273
await githubActionsService.SetOutputAsync("plan-valid", "false");
73-
collector.EmitError(@out, $"Plan is invalid, delete ratio: {validationResult.DeleteRatio}, threshold: {validationResult.DeleteThreshold} over {plan.TotalRemoteFiles:N0} remote files while plan has {plan.DeleteRequests:N0} deletions");
74+
collector.EmitError(@out, $"Plan is invalid, {validationResult}, delete ratio: {validationResult.DeleteRatio}, remote listing completed: {plan.RemoteListingCompleted}");
7475
await collector.StopAsync(ctx);
7576
return collector.Errors;
7677
}
@@ -93,6 +94,7 @@ public async Task<int> Plan(
9394
/// <param name="s3BucketName">The S3 bucket name to deploy to</param>
9495
/// <param name="planFile">The path to the plan file to apply</param>
9596
/// <param name="ctx"></param>
97+
[Command("apply")]
9698
public async Task<int> Apply(string environment, string s3BucketName, string planFile, Cancel ctx = default)
9799
{
98100
AssignOutputLogger();
@@ -116,6 +118,7 @@ public async Task<int> Apply(string environment, string s3BucketName, string pla
116118
}
117119
var planJson = await File.ReadAllTextAsync(planFile, ctx);
118120
var plan = SyncPlan.Deserialize(planJson);
121+
_logger.LogInformation("Remote listing completed: {RemoteListingCompleted}", plan.RemoteListingCompleted);
119122
_logger.LogInformation("Total files to sync: {TotalFiles}", plan.TotalSyncRequests);
120123
_logger.LogInformation("Total files to delete: {DeleteCount}", plan.DeleteRequests.Count);
121124
_logger.LogInformation("Total files to add: {AddCount}", plan.AddRequests.Count);
@@ -133,7 +136,7 @@ public async Task<int> Apply(string environment, string s3BucketName, string pla
133136
var validationResult = validator.Validate(plan);
134137
if (!validationResult.Valid)
135138
{
136-
collector.EmitError(planFile, $"Plan is invalid, delete ratio: {validationResult.DeleteRatio}, threshold: {validationResult.DeleteThreshold} over {plan.TotalRemoteFiles:N0} remote files while plan has {plan.DeleteRequests:N0} deletions");
139+
collector.EmitError(planFile, $"Plan is invalid, {validationResult}, delete ratio: {validationResult.DeleteRatio}, remote listing completed: {plan.RemoteListingCompleted}");
137140
await collector.StopAsync(ctx);
138141
return collector.Errors;
139142
}

src/tooling/docs-assembler/Deploying/AwsS3SyncPlanStrategy.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private bool IsSymlink(string path)
9292

9393
public async Task<SyncPlan> Plan(float? deleteThreshold, Cancel ctx = default)
9494
{
95-
var remoteObjects = await ListObjects(ctx);
95+
var (readToCompletion, remoteObjects) = await ListObjects(ctx);
9696
var localObjects = context.OutputDirectory.GetFiles("*", SearchOption.AllDirectories)
9797
.Where(f => !IsSymlink(f.FullName))
9898
.ToArray();
@@ -156,6 +156,7 @@ await Parallel.ForEachAsync(localObjects, ctx, async (localFile, token) =>
156156

157157
return new SyncPlan
158158
{
159+
RemoteListingCompleted = readToCompletion,
159160
DeleteThresholdDefault = deleteThreshold,
160161
TotalRemoteFiles = remoteObjects.Count,
161162
TotalSourceFiles = localObjects.Length,
@@ -167,24 +168,56 @@ await Parallel.ForEachAsync(localObjects, ctx, async (localFile, token) =>
167168
};
168169
}
169170

170-
private async Task<Dictionary<string, S3Object>> ListObjects(Cancel ctx = default)
171+
private async Task<(bool readToCompletion, Dictionary<string, S3Object> objects)> ListObjects(Cancel ctx = default)
171172
{
172173
var listBucketRequest = new ListObjectsV2Request
173174
{
174175
BucketName = bucketName,
175-
MaxKeys = 1000,
176+
MaxKeys = 1000
176177
};
177178
var objects = new List<S3Object>();
179+
var bucketExists = await S3BucketExists(ctx);
180+
if (!bucketExists)
181+
{
182+
context.Collector.EmitGlobalError("Bucket does not exist, cannot list objects");
183+
return (false, objects.ToDictionary(o => o.Key));
184+
}
185+
186+
var readToCompletion = true;
178187
ListObjectsV2Response response;
179188
do
180189
{
181190
response = await s3Client.ListObjectsV2Async(listBucketRequest, ctx);
182191
if (response is null or { S3Objects: null })
192+
{
193+
if (response?.IsTruncated == true)
194+
{
195+
context.Collector.EmitGlobalError("Failed to list objects in S3 to completion");
196+
readToCompletion = false;
197+
}
183198
break;
199+
}
184200
objects.AddRange(response.S3Objects);
185201
listBucketRequest.ContinuationToken = response.NextContinuationToken;
186202
} while (response.IsTruncated == true);
187203

188-
return objects.ToDictionary(o => o.Key);
204+
return (readToCompletion, objects.ToDictionary(o => o.Key));
205+
}
206+
207+
private async Task<bool> S3BucketExists(Cancel ctx)
208+
{
209+
//https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_Scenario_DoesBucketExist_section.html
210+
try
211+
{
212+
_ = await s3Client.GetBucketAclAsync(new GetBucketAclRequest
213+
{
214+
BucketName = bucketName
215+
}, ctx);
216+
return true;
217+
}
218+
catch
219+
{
220+
return false;
221+
}
189222
}
190223
}

src/tooling/docs-assembler/Deploying/DocsSync.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public record SyncPlan
5555
[JsonPropertyName("deletion_threshold_default")]
5656
public required float? DeleteThresholdDefault { get; init; }
5757

58+
/// The user-specified delete threshold
59+
[JsonPropertyName("remote_listing_completed")]
60+
public required bool RemoteListingCompleted { get; init; }
61+
5862
/// The total number of source files that were located in the build output
5963
[JsonPropertyName("total_source_files")]
6064
public required int TotalSourceFiles { get; init; }

src/tooling/docs-assembler/Deploying/DocsSyncPlanValidator.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public PlanValidationResult Validate(SyncPlan plan)
1616
_logger.LogInformation("Using user-specified delete threshold of {Threshold}", plan.DeleteThresholdDefault);
1717

1818
var deleteThreshold = plan.DeleteThresholdDefault ?? 0.2f;
19+
if (!plan.RemoteListingCompleted)
20+
{
21+
_logger.LogError("Remote files were not read to completion, cannot validate deployment plan");
22+
return new(false, 1.0f, deleteThreshold);
23+
}
24+
1925
if (plan.TotalSourceFiles == 0)
2026
{
2127
_logger.LogError("No files to sync");
@@ -30,7 +36,7 @@ public PlanValidationResult Validate(SyncPlan plan)
3036
}
3137
// if the total remote files are less than or equal to 100, we enforce a higher ratio of 0.8
3238
// this allows newer assembled documentation to be in a higher state of flux
33-
if (plan.TotalRemoteFiles <= 100)
39+
else if (plan.TotalRemoteFiles <= 100)
3440
{
3541
_logger.LogInformation("Plan has less than 100 total remote files ensuring delete threshold is at minimum 0.8");
3642
deleteThreshold = Math.Max(deleteThreshold, 0.8f);

tests/docs-assembler.Tests/src/docs-assembler.Tests/DocsSyncTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ public async Task TestApply()
235235
var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, checkoutDirectory);
236236
var plan = new SyncPlan
237237
{
238+
RemoteListingCompleted = true,
238239
DeleteThresholdDefault = null,
239240
TotalRemoteFiles = 0,
240241
TotalSourceFiles = 5,

0 commit comments

Comments
 (0)