Skip to content

Commit

Permalink
Merge pull request #1006 from github/reclaimation-error-handling
Browse files Browse the repository at this point in the history
  • Loading branch information
hfishback01 authored Jun 26, 2023
2 parents 083bbbf + 9a971d1 commit b87d7b4
Show file tree
Hide file tree
Showing 14 changed files with 537 additions and 136 deletions.
1 change: 1 addition & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Add additional error handling to `reclaim-mannequin` process
- Removed ability to use `gh gei` to migrate from ADO -> GH. You must use `gh ado2gh` to do this now. This was long since obsolete, but was still available via hidden args - which have now been removed.
- Add `bbs2gh inventory-report` command to write data available for migrations in CSV form
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class ReclaimMannequinCommandArgs : CommandArgs
public string MannequinId { get; set; }
public string TargetUser { get; set; }
public bool Force { get; set; }
public bool NoPrompt { get; set; }
[Secret]
public string GithubPat { get; set; }
public bool SkipInvitation { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public ReclaimMannequinCommandBase() : base(
Description = "Map the user even if it was previously mapped"
};

public virtual Option<bool> NoPrompt { get; } = new("--no-prompt")
{
Description = "Overrides all prompts and warnings with 'Y' value."
};

public virtual Option<string> GithubPat { get; } = new("--github-pat")
{
Description = "Personal access token of the GitHub target. Overrides GH_PAT environment variable."
Expand Down Expand Up @@ -83,7 +88,7 @@ public override ReclaimMannequinCommandHandler BuildHandler(ReclaimMannequinComm
var reclaimService = new ReclaimService(githubApi, log);
var confirmationService = sp.GetRequiredService<ConfirmationService>();

return new ReclaimMannequinCommandHandler(log, reclaimService, confirmationService);
return new ReclaimMannequinCommandHandler(log, reclaimService, confirmationService, githubApi);
}

protected void AddOptions()
Expand All @@ -94,6 +99,7 @@ protected void AddOptions()
AddOption(MannequinId);
AddOption(TargetUser);
AddOption(Force);
AddOption(NoPrompt);
AddOption(GithubPat);
AddOption(SkipInvitation);
AddOption(Verbose);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ public class ReclaimMannequinCommandHandler : ICommandHandler<ReclaimMannequinCo
private readonly OctoLogger _log;
private readonly ReclaimService _reclaimService;
private readonly ConfirmationService _confirmationService;
private readonly GithubApi _githubApi;

internal Func<string, bool> FileExists = path => File.Exists(path);
internal Func<string, string[]> GetFileContent = path => File.ReadLines(path).ToArray();

public ReclaimMannequinCommandHandler(OctoLogger log, ReclaimService reclaimService, ConfirmationService confirmationService)
public ReclaimMannequinCommandHandler(OctoLogger log, ReclaimService reclaimService, ConfirmationService confirmationService, GithubApi githubApi)
{
_log = log;
_reclaimService = reclaimService;
_confirmationService = confirmationService;
_githubApi = githubApi;
}

public async Task Handle(ReclaimMannequinCommandArgs args)
Expand All @@ -29,6 +31,24 @@ public async Task Handle(ReclaimMannequinCommandArgs args)
throw new ArgumentNullException(nameof(args));
}

if (args.SkipInvitation)
{
// Check if user is admin to EMU org
var login = await _githubApi.GetLoginName();

var membership = await _githubApi.GetOrgMembershipForUser(args.GithubOrg, login);

if (membership != "admin")
{
throw new OctoshiftCliException($"User {login} is not an org admin and is not eligible to reclaim mannequins with the --skip-invitation feature.");
}

if (!args.NoPrompt)
{
_confirmationService.AskForConfirmation("Reclaiming mannequins with the --skip-invitation option is immediate and irreversible. Are you sure you wish to continue? [y/N]");
}
}

if (!string.IsNullOrEmpty(args.Csv))
{
_log.LogInformation("Reclaiming Mannequins with CSV...");
Expand All @@ -38,24 +58,14 @@ public async Task Handle(ReclaimMannequinCommandArgs args)
throw new OctoshiftCliException($"File {args.Csv} does not exist.");
}

//TODO: Get verbiage approved
if (args.SkipInvitation)
{
_ = _confirmationService.AskForConfirmation("Reclaiming mannequins with the --skip-invitation option is immediate and irreversible. Are you sure you wish to continue? (y/n)");
}

await _reclaimService.ReclaimMannequins(GetFileContent(args.Csv), args.GithubOrg, args.Force, args.SkipInvitation);
}
else
{
if (args.SkipInvitation)
{
throw new OctoshiftCliException($"--csv must be specified to skip reclaimation email");
}

_log.LogInformation("Reclaiming Mannequin...");

await _reclaimService.ReclaimMannequin(args.MannequinUser, args.MannequinId, args.TargetUser, args.GithubOrg, args.Force);
await _reclaimService.ReclaimMannequin(args.MannequinUser, args.MannequinId, args.TargetUser, args.GithubOrg, args.Force, args.SkipInvitation);
}
}
}
39 changes: 21 additions & 18 deletions src/Octoshift/Services/ConfirmationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,64 @@ namespace OctoshiftCLI.Services
{
public class ConfirmationService
{
# region Variables

private readonly Action<string> _writeToConsoleOut;
private readonly Action<string, ConsoleColor> _writeToConsoleOut;
private readonly Func<ConsoleKey> _readConsoleKey;
private readonly Action<int> _cancelCommand;

#endregion

#region Constructors
public ConfirmationService()
{
_writeToConsoleOut = msg => Console.WriteLine(msg);
_writeToConsoleOut = (msg, outputColor) =>
{
var currentColor = Console.ForegroundColor;
Console.ForegroundColor = outputColor;
Console.WriteLine(msg);
Console.ForegroundColor = currentColor;
};
_readConsoleKey = ReadKey;
_cancelCommand = code => Environment.Exit(code);
}

// Constructor designed to allow for testing console methods
public ConfirmationService(Action<string> writeToConsoleOut, Func<ConsoleKey> readConsoleKey)
public ConfirmationService(Action<string, ConsoleColor> writeToConsoleOut, Func<ConsoleKey> readConsoleKey, Action<int> cancelCommand)
{
_writeToConsoleOut = writeToConsoleOut;
_readConsoleKey = readConsoleKey;
_cancelCommand = cancelCommand;
}

#endregion

#region Functions
public bool AskForConfirmation(string confirmationPrompt, string cancellationErrorMessage = "")
public virtual bool AskForConfirmation(string confirmationPrompt, string cancellationErrorMessage = "")
{
ConsoleKey response;
do
{
_writeToConsoleOut(confirmationPrompt);
_writeToConsoleOut(confirmationPrompt, ConsoleColor.Yellow);
response = _readConsoleKey();
if (response != ConsoleKey.Enter)
{
_writeToConsoleOut("");
_writeToConsoleOut("", ConsoleColor.White);
}

} while (response is not ConsoleKey.Y and not ConsoleKey.N);

if (response == ConsoleKey.Y)
{
_writeToConsoleOut("Confirmation Recorded. Proceeding...");
_writeToConsoleOut("Confirmation Recorded. Proceeding...", ConsoleColor.White);
return true;
}
else
{
_writeToConsoleOut("Canceling Command...");
throw new OctoshiftCliException($"Command Cancelled. {cancellationErrorMessage}");
_writeToConsoleOut($"Command Cancelled. {cancellationErrorMessage}", ConsoleColor.White);
_cancelCommand(0);
}
return false;
}

private ConsoleKey ReadKey()
{
return Console.ReadKey(false).Key;
}
#endregion
}
}

60 changes: 55 additions & 5 deletions src/Octoshift/Services/GithubApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,48 @@ public virtual async Task RemoveTeamMember(string org, string teamSlug, string m
await _retryPolicy.Retry(() => _client.DeleteAsync(url));
}

public virtual async Task<string> GetLoginName()
{
var url = $"{_apiUrl}/graphql";

var payload = new
{
query = "query{viewer{login}}"
};

try
{
return await _retryPolicy.Retry(async () =>
{
var data = await _client.PostGraphQLAsync(url, payload);
return (string)data["data"]["viewer"]["login"];
});
}
catch (Exception ex)
{
throw new OctoshiftCliException($"Failed to lookup the login for current user", ex);
}
}

public virtual async Task<string> GetOrgMembershipForUser(string org, string member)
{
var url = $"{_apiUrl}/orgs/{org}/memberships/{member}";

try
{
var response = await _client.GetAsync(url);

var data = JObject.Parse(response);

return (string)data["role"];
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) // Not a member
{
return null;
}
}

public virtual async Task<bool> DoesRepoExist(string org, string repo)
{
var url = $"{_apiUrl}/repos/{org.EscapeDataString()}/{repo.EscapeDataString()}";
Expand Down Expand Up @@ -729,7 +771,7 @@ ... on User {
return data.ToObject<CreateAttributionInvitationResult>();
}

public virtual async Task<ReattributeMannequinToUserResult> ReclaimMannequinsSkipInvitation(string orgId, string mannequinId, string targetUserId)
public virtual async Task<ReattributeMannequinToUserResult> ReclaimMannequinSkipInvitation(string orgId, string mannequinId, string targetUserId)
{
var url = $"{_apiUrl}/graphql";
var mutation = "mutation($orgId: ID!,$sourceId: ID!,$targetId: ID!)";
Expand Down Expand Up @@ -758,10 +800,18 @@ ... on User {
variables = new { orgId, sourceId = mannequinId, targetId = targetUserId }
};

var response = await _client.PostAsync(url, payload);
var data = JObject.Parse(response);

return data.ToObject<ReattributeMannequinToUserResult>();
try
{
return await _retryPolicy.Retry(async () =>
{
var data = await _client.PostGraphQLAsync(url, payload);
return data.ToObject<ReattributeMannequinToUserResult>();
});
}
catch (OctoshiftCliException ex) when (ex.Message.Contains("Field 'reattributeMannequinToUser' doesn't exist on type 'Mutation'"))
{
throw new OctoshiftCliException($"Reclaiming mannequins with the--skip - invitation flag is not enabled for your GitHub organization.For more details, contact GitHub Support.", ex);
}
}

public virtual async Task<IEnumerable<GithubSecretScanningAlert>> GetSecretScanningAlertsForRepository(string org, string repo)
Expand Down
Loading

0 comments on commit b87d7b4

Please sign in to comment.