diff --git a/Gitea.Declarative.Lib/Gitea.Declarative.Lib.fsproj b/Gitea.Declarative.Lib/Gitea.Declarative.Lib.fsproj index 10a1102..b543b82 100644 --- a/Gitea.Declarative.Lib/Gitea.Declarative.Lib.fsproj +++ b/Gitea.Declarative.Lib/Gitea.Declarative.Lib.fsproj @@ -24,6 +24,7 @@ + diff --git a/Gitea.Declarative.Lib/Gitea.fs b/Gitea.Declarative.Lib/Gitea.fs index cf0ccd8..b46dc27 100644 --- a/Gitea.Declarative.Lib/Gitea.fs +++ b/Gitea.Declarative.Lib/Gitea.fs @@ -198,3 +198,102 @@ module Gitea = ) |> Async.Parallel |> fun a -> async.Bind (a, Array.iter id >> async.Return) + + let reconcileUserErrors + (log : ILogger) + (getUserInput : string -> string) + (client : Gitea.Client) + (m : Map>) + = + let userInputLock = obj () + + m + |> Map.toSeq + |> Seq.map (fun (User user, err) -> + match err with + | AlignmentError.DoesNotExist desired -> + async { + let rand = Random () + + let pwd = + Array.init 15 (fun _ -> rand.Next (65, 65 + 25) |> byte) + |> System.Text.Encoding.ASCII.GetString + + let options = Gitea.CreateUserOption () + options.Email <- desired.Email + options.Username <- user + options.FullName <- user + + options.Visibility <- + match desired.Visibility with + | None -> "public" + | Some v -> v + + options.LoginName <- user + options.MustChangePassword <- Some true + options.Password <- pwd + let! _ = client.AdminCreateUser options |> Async.AwaitTask + + lock + userInputLock + (fun () -> + log.LogCritical ( + "Created user {User} with password {Password}, which you must now change", + user, + pwd + ) + ) + + return () + } + | AlignmentError.UnexpectedlyPresent -> + async { + lock + userInputLock + (fun () -> + let answer = + UserInput.getDefaultNo getUserInput $"User %s{user} unexpectedly present. Remove?" + + if answer then + client.AdminDeleteUser(user).Result + else + log.LogCritical ("Refusing to delete user {User}, who is unexpectedly present.", user) + ) + } + | AlignmentError.ConfigurationDiffers (desired, actual) -> + let updates = UserInfo.Resolve desired actual + + async { + lock + userInputLock + (fun () -> + let body = Gitea.EditUserOption () + + for update in updates do + match update with + | UserInfoUpdate.Admin (desired, _) -> body.Admin <- desired + | UserInfoUpdate.Email (desired, _) -> body.Email <- desired + | UserInfoUpdate.Visibility (desired, _) -> body.Visibility <- desired + | UserInfoUpdate.Website (desired, actual) -> + // Per https://github.com/go-gitea/gitea/issues/17126, + // the website parameter can't currently be edited. + // This is a bug that is unlikely to be fixed. + let actual = + match actual with + | None -> "" + | Some uri -> uri.ToString () + + log.LogCritical ( + "User {User} has conflicting website, desired {DesiredWebsite}, existing {ActualWebsite}, which a bug in Gitea means can't be reconciled via the API.", + user, + desired, + actual + ) + + body.LoginName <- user + client.AdminEditUser(user, body).Result |> ignore + ) + } + ) + |> Async.Parallel + |> fun a -> async.Bind (a, Array.iter id >> async.Return) diff --git a/Gitea.Declarative.Lib/UserInput.fs b/Gitea.Declarative.Lib/UserInput.fs new file mode 100644 index 0000000..46c5347 --- /dev/null +++ b/Gitea.Declarative.Lib/UserInput.fs @@ -0,0 +1,15 @@ +namespace Gitea.Declarative + +[] +module internal UserInput = + + let rec getDefaultNo (getUserInput : string -> string) (message : string) : bool = + let answer = getUserInput $"{message} (y/N): " + + match answer with + | "y" + | "Y" -> true + | "n" + | "N" + | "" -> false + | _ -> getDefaultNo getUserInput message diff --git a/Gitea.Declarative/Program.fs b/Gitea.Declarative/Program.fs index fbb232a..f99c34f 100644 --- a/Gitea.Declarative/Program.fs +++ b/Gitea.Declarative/Program.fs @@ -40,116 +40,9 @@ module Program = m |> Map.iter (fun (User u) errMap -> errMap |> Map.iter (fun (RepoName r) err -> printfn $"%s{u}: %s{r}: {err}")) - let rec getUserInputDefaultNo (getUserInput : unit -> string) (message : string) : bool = - Console.Write $"${message} (y/N): " - let answer = getUserInput () - - match answer with - | "y" - | "Y" -> true - | "n" - | "N" - | "" -> false - | _ -> getUserInputDefaultNo getUserInput message - - let reconcileUserErrors - (log : ILogger) - (getUserInput : unit -> string) - (client : Gitea.Client) - (m : Map>) - = - let userInputLock = obj () - - m - |> Map.toSeq - |> Seq.map (fun (User user, err) -> - match err with - | AlignmentError.DoesNotExist desired -> - async { - let rand = Random () - - let pwd = - Array.init 15 (fun _ -> rand.Next (65, 65 + 25) |> byte) - |> System.Text.Encoding.ASCII.GetString - - let options = Gitea.CreateUserOption () - options.Email <- desired.Email - options.Username <- user - options.FullName <- user - - options.Visibility <- - match desired.Visibility with - | None -> "public" - | Some v -> v - - options.LoginName <- user - options.MustChangePassword <- Some true - options.Password <- pwd - let! _ = client.AdminCreateUser options |> Async.AwaitTask - - lock - userInputLock - (fun () -> - log.LogCritical ( - "Created user {User} with password {Password}, which you must now change", - user, - pwd - ) - ) - - return () - } - | AlignmentError.UnexpectedlyPresent -> - async { - lock - userInputLock - (fun () -> - let answer = - getUserInputDefaultNo getUserInput $"User %s{user} unexpectedly present. Remove?" - - if answer then - client.AdminDeleteUser(user).Result - else - log.LogCritical ("Refusing to delete user {User}, who is unexpectedly present.", user) - ) - } - | AlignmentError.ConfigurationDiffers (desired, actual) -> - let updates = UserInfo.Resolve desired actual - - async { - lock - userInputLock - (fun () -> - let body = Gitea.EditUserOption () - - for update in updates do - match update with - | UserInfoUpdate.Admin (desired, _) -> body.Admin <- desired - | UserInfoUpdate.Email (desired, _) -> body.Email <- desired - | UserInfoUpdate.Visibility (desired, _) -> body.Visibility <- desired - | UserInfoUpdate.Website (desired, actual) -> - // Per https://github.com/go-gitea/gitea/issues/17126, - // the website parameter can't currently be edited. - // This is a bug that is unlikely to be fixed. - let actual = - match actual with - | None -> "" - | Some uri -> uri.ToString () - - log.LogCritical ( - "User {User} has conflicting website, desired {DesiredWebsite}, existing {ActualWebsite}, which a bug in Gitea means can't be reconciled via the API.", - user, - desired, - actual - ) - - body.LoginName <- user - client.AdminEditUser(user, body).Result |> ignore - ) - } - ) - |> Async.Parallel - |> fun a -> async.Bind (a, Array.iter id >> async.Return) + let getUserInput (s : string) : string = + Console.Write s + Console.ReadLine () [] let main argv = @@ -180,7 +73,7 @@ module Program = } use loggerProvider = new ConsoleLoggerProvider (options) - let logger = loggerProvider.CreateLogger "Gitea.App" + let logger = loggerProvider.CreateLogger "Gitea.Declarative" use client = new HttpClient () client.BaseAddress <- args.GiteaHost @@ -189,14 +82,14 @@ module Program = let client = Gitea.Client client task { - Console.WriteLine "Checking users..." + logger.LogInformation "Checking users..." let! userErrors = Gitea.checkUsers config client match userErrors with | Ok () -> () - | Error errors -> do! reconcileUserErrors logger Console.ReadLine client errors + | Error errors -> do! Gitea.reconcileUserErrors logger getUserInput client errors - Console.WriteLine "Checking repos..." + logger.LogInformation "Checking repos..." let! repoErrors = Gitea.checkRepos config client match repoErrors with