Skip to content
Merged
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
9 changes: 6 additions & 3 deletions src/tooling/docs-assembler/Cli/DeployCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private void AssignOutputLogger()
/// <param name="out"> The file to write the plan to</param>
/// <param name="deleteThreshold"> The percentage of deletions allowed in the plan as percentage of total files to sync</param>
/// <param name="ctx"></param>
[Command("plan")]
public async Task<int> Plan(
string environment,
string s3BucketName,
Expand All @@ -58,7 +59,7 @@ public async Task<int> Plan(
var s3Client = new AmazonS3Client();
var planner = new AwsS3SyncPlanStrategy(logFactory, s3Client, s3BucketName, assembleContext);
var plan = await planner.Plan(deleteThreshold, ctx);
_logger.LogInformation("Total files to sync: {TotalFiles}", plan.TotalSyncRequests);
_logger.LogInformation("Remote listing completed: {RemoteListingCompleted}", plan.RemoteListingCompleted);
_logger.LogInformation("Total files to delete: {DeleteCount}", plan.DeleteRequests.Count);
_logger.LogInformation("Total files to add: {AddCount}", plan.AddRequests.Count);
_logger.LogInformation("Total files to update: {UpdateCount}", plan.UpdateRequests.Count);
Expand All @@ -70,7 +71,7 @@ public async Task<int> Plan(
if (!validationResult.Valid)
{
await githubActionsService.SetOutputAsync("plan-valid", "false");
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");
collector.EmitError(@out, $"Plan is invalid, {validationResult}, delete ratio: {validationResult.DeleteRatio}, remote listing completed: {plan.RemoteListingCompleted}");
await collector.StopAsync(ctx);
return collector.Errors;
}
Expand All @@ -93,6 +94,7 @@ public async Task<int> Plan(
/// <param name="s3BucketName">The S3 bucket name to deploy to</param>
/// <param name="planFile">The path to the plan file to apply</param>
/// <param name="ctx"></param>
[Command("apply")]
public async Task<int> Apply(string environment, string s3BucketName, string planFile, Cancel ctx = default)
{
AssignOutputLogger();
Expand All @@ -116,6 +118,7 @@ public async Task<int> Apply(string environment, string s3BucketName, string pla
}
var planJson = await File.ReadAllTextAsync(planFile, ctx);
var plan = SyncPlan.Deserialize(planJson);
_logger.LogInformation("Remote listing completed: {RemoteListingCompleted}", plan.RemoteListingCompleted);
_logger.LogInformation("Total files to sync: {TotalFiles}", plan.TotalSyncRequests);
_logger.LogInformation("Total files to delete: {DeleteCount}", plan.DeleteRequests.Count);
_logger.LogInformation("Total files to add: {AddCount}", plan.AddRequests.Count);
Expand All @@ -133,7 +136,7 @@ public async Task<int> Apply(string environment, string s3BucketName, string pla
var validationResult = validator.Validate(plan);
if (!validationResult.Valid)
{
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");
collector.EmitError(planFile, $"Plan is invalid, {validationResult}, delete ratio: {validationResult.DeleteRatio}, remote listing completed: {plan.RemoteListingCompleted}");
await collector.StopAsync(ctx);
return collector.Errors;
}
Expand Down
41 changes: 37 additions & 4 deletions src/tooling/docs-assembler/Deploying/AwsS3SyncPlanStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private bool IsSymlink(string path)

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

return new SyncPlan
{
RemoteListingCompleted = readToCompletion,
DeleteThresholdDefault = deleteThreshold,
TotalRemoteFiles = remoteObjects.Count,
TotalSourceFiles = localObjects.Length,
Expand All @@ -167,24 +168,56 @@ await Parallel.ForEachAsync(localObjects, ctx, async (localFile, token) =>
};
}

private async Task<Dictionary<string, S3Object>> ListObjects(Cancel ctx = default)
private async Task<(bool readToCompletion, Dictionary<string, S3Object> objects)> ListObjects(Cancel ctx = default)
{
var listBucketRequest = new ListObjectsV2Request
{
BucketName = bucketName,
MaxKeys = 1000,
MaxKeys = 1000
};
var objects = new List<S3Object>();
var bucketExists = await S3BucketExists(ctx);
if (!bucketExists)
{
context.Collector.EmitGlobalError("Bucket does not exist, cannot list objects");
return (false, objects.ToDictionary(o => o.Key));
}

var readToCompletion = true;
ListObjectsV2Response response;
do
{
response = await s3Client.ListObjectsV2Async(listBucketRequest, ctx);
if (response is null or { S3Objects: null })
{
if (response?.IsTruncated == true)
{
context.Collector.EmitGlobalError("Failed to list objects in S3 to completion");
readToCompletion = false;
}
break;
}
objects.AddRange(response.S3Objects);
listBucketRequest.ContinuationToken = response.NextContinuationToken;
} while (response.IsTruncated == true);

return objects.ToDictionary(o => o.Key);
return (readToCompletion, objects.ToDictionary(o => o.Key));
}

private async Task<bool> S3BucketExists(Cancel ctx)
{
//https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_Scenario_DoesBucketExist_section.html
try
{
_ = await s3Client.GetBucketAclAsync(new GetBucketAclRequest
{
BucketName = bucketName
}, ctx);
return true;
}
catch
{
return false;
}
}
}
4 changes: 4 additions & 0 deletions src/tooling/docs-assembler/Deploying/DocsSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public record SyncPlan
[JsonPropertyName("deletion_threshold_default")]
public required float? DeleteThresholdDefault { get; init; }

/// The user-specified delete threshold
[JsonPropertyName("remote_listing_completed")]
public required bool RemoteListingCompleted { get; init; }

/// The total number of source files that were located in the build output
[JsonPropertyName("total_source_files")]
public required int TotalSourceFiles { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public PlanValidationResult Validate(SyncPlan plan)
_logger.LogInformation("Using user-specified delete threshold of {Threshold}", plan.DeleteThresholdDefault);

var deleteThreshold = plan.DeleteThresholdDefault ?? 0.2f;
if (!plan.RemoteListingCompleted)
{
_logger.LogError("Remote files were not read to completion, cannot validate deployment plan");
return new(false, 1.0f, deleteThreshold);
}

if (plan.TotalSourceFiles == 0)
{
_logger.LogError("No files to sync");
Expand All @@ -30,7 +36,7 @@ public PlanValidationResult Validate(SyncPlan plan)
}
// if the total remote files are less than or equal to 100, we enforce a higher ratio of 0.8
// this allows newer assembled documentation to be in a higher state of flux
if (plan.TotalRemoteFiles <= 100)
else if (plan.TotalRemoteFiles <= 100)
{
_logger.LogInformation("Plan has less than 100 total remote files ensuring delete threshold is at minimum 0.8");
deleteThreshold = Math.Max(deleteThreshold, 0.8f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ public async Task TestApply()
var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, checkoutDirectory);
var plan = new SyncPlan
{
RemoteListingCompleted = true,
DeleteThresholdDefault = null,
TotalRemoteFiles = 0,
TotalSourceFiles = 5,
Expand Down
Loading