diff --git a/Gitea.Declarative.Lib/Gitea.fs b/Gitea.Declarative.Lib/Gitea.fs index 9f84e32..08db1b9 100644 --- a/Gitea.Declarative.Lib/Gitea.fs +++ b/Gitea.Declarative.Lib/Gitea.fs @@ -149,6 +149,16 @@ module Gitea = Error (Map.ofArray errors) } + let private createPushMirrorOption (target : Uri) (githubToken : string) : Gitea.CreatePushMirrorOption = + let options = Gitea.CreatePushMirrorOption () + options.SyncOnCommit <- Some true + options.RemoteAddress <- target.ToString () + options.RemoteUsername <- githubToken + options.RemotePassword <- githubToken + options.Interval <- "8h0m0s" + + options + let reconcileDifferingConfiguration (logger : ILogger) (client : IGiteaClient) @@ -369,13 +379,8 @@ module Gitea = | Some token -> async { logger.LogInformation ("Setting up push mirror on {User}:{Repo}", user, repoName) - let options = Gitea.CreatePushMirrorOption () - options.SyncOnCommit <- Some true - options.RemoteAddress <- (desired.GitHubAddress : Uri).ToString () - options.RemoteUsername <- token - options.RemotePassword <- token - options.Interval <- "8h0m0s" - let! _ = client.RepoAddPushMirror (user, repoName, options) |> Async.AwaitTask + let pushMirrorOption = createPushMirrorOption desired.GitHubAddress token + let! _ = client.RepoAddPushMirror (user, repoName, pushMirrorOption) |> Async.AwaitTask return () } | Some desired, Some actual -> @@ -721,3 +726,88 @@ module Gitea = ) |> Async.Parallel |> fun a -> async.Bind (a, Array.iter id >> async.Return) + + let toRefresh (client : IGiteaClient) : Async>> = + async { + let! users = + Array.getPaginated (fun page limit -> + client.AdminGetAllUsers (Some page, Some limit) |> Async.AwaitTask + ) + + let! results = + users + |> Seq.map (fun user -> + async { + let! repos = + Array.getPaginated (fun page count -> + client.UserListRepos (user.LoginName, Some page, Some count) |> Async.AwaitTask + ) + + let! pushMirrorResults = + repos + |> Seq.map (fun r -> + async { + let! mirrors = + Array.getPaginated (fun page count -> + Async.AwaitTask ( + client.RepoListPushMirrors ( + user.LoginName, + r.Name, + Some page, + Some count + ) + ) + ) + + return RepoName r.Name, mirrors + } + ) + |> Async.Parallel + + return User user.LoginName, Map.ofArray pushMirrorResults + } + ) + |> Async.Parallel + + return results |> Map.ofArray + } + + let refreshAuth + (logger : ILogger) + (client : IGiteaClient) + (githubToken : string) + (instructions : Map>) + : Async + = + instructions + |> Map.toSeq + |> Seq.collect (fun (User user, repos) -> + Map.toSeq repos + |> Seq.collect (fun (RepoName repoName, mirrors) -> + mirrors + |> Seq.map (fun mirror -> + async { + logger.LogInformation ( + "Refreshing push mirror on {User}:{Repo} to {PushMirrorRemote}", + user, + repoName, + mirror.RemoteAddress + ) + + let option = createPushMirrorOption (Uri mirror.RemoteAddress) githubToken + + option.Interval <- mirror.Interval + option.SyncOnCommit <- mirror.SyncOnCommit + + let! newMirror = Async.AwaitTask (client.RepoAddPushMirror (user, repoName, option)) + + let! deleteOldMirror = + Async.AwaitTask (client.RepoDeletePushMirror (user, repoName, mirror.RemoteName)) + + return () + } + ) + ) + ) + |> Async.Parallel + |> Async.map (Array.iter id) diff --git a/Gitea.Declarative.Lib/IGiteaClient.fs b/Gitea.Declarative.Lib/IGiteaClient.fs index 96a59bb..487c3e1 100644 --- a/Gitea.Declarative.Lib/IGiteaClient.fs +++ b/Gitea.Declarative.Lib/IGiteaClient.fs @@ -17,6 +17,8 @@ type IGiteaClient = loginName : string * userName : string * page : int64 option * count : int64 option -> Gitea.PushMirror array Task + abstract RepoDeletePushMirror : user : string * repo : string * remoteName : string -> unit Task + abstract RepoListBranchProtection : loginName : string * userName : string -> Gitea.BranchProtection array Task abstract RepoDeleteBranchProtection : user : string * repo : string * branch : string -> unit Task @@ -57,6 +59,9 @@ module IGiteaClient = member _.RepoListPushMirrors (loginName, userName, page, count) = client.RepoListPushMirrors (loginName, userName, page, count) + member _.RepoDeletePushMirror (loginName, repo, remoteName) = + client.RepoDeletePushMirror (loginName, repo, remoteName) + member _.RepoListBranchProtection (login, user) = client.RepoListBranchProtection (login, user) diff --git a/Gitea.Declarative/Gitea.Declarative.fsproj b/Gitea.Declarative/Gitea.Declarative.fsproj index 4c2a6fa..e88e449 100644 --- a/Gitea.Declarative/Gitea.Declarative.fsproj +++ b/Gitea.Declarative/Gitea.Declarative.fsproj @@ -18,8 +18,10 @@ + + diff --git a/Gitea.Declarative/Program.fs b/Gitea.Declarative/Program.fs index 9396af9..b6542f9 100644 --- a/Gitea.Declarative/Program.fs +++ b/Gitea.Declarative/Program.fs @@ -22,6 +22,9 @@ module Program = ArgsCrate.make OutputSchemaArgs.OfParse OutputSchema.run) "verify", ("Verify a `reconcile` configuration file", ArgsCrate.make VerifyArgs.OfParse Verify.run) + + "refresh-auth", + ("Refresh authentication for push mirrors", ArgsCrate.make RefreshAuthArgs.OfParse RefreshAuth.run) |] |> Map.ofArray diff --git a/Gitea.Declarative/Reconcile.fs b/Gitea.Declarative/Reconcile.fs index 492fa7b..94218de 100644 --- a/Gitea.Declarative/Reconcile.fs +++ b/Gitea.Declarative/Reconcile.fs @@ -2,10 +2,7 @@ namespace Gitea.Declarative open System open System.IO -open System.Net.Http open Argu -open Microsoft.Extensions.Logging.Console -open Microsoft.Extensions.Options open Microsoft.Extensions.Logging type RunArgsFragment = @@ -61,28 +58,12 @@ module Reconcile = let config = GiteaConfig.get args.ConfigFile - let options = - let options = ConsoleLoggerOptions () - - { new IOptionsMonitor with - member _.Get _ = options - member _.CurrentValue = options - - member _.OnChange _ = - { new IDisposable with - member _.Dispose () = () - } - } - async { - use loggerProvider = new ConsoleLoggerProvider (options) + use loggerProvider = Utils.createLoggerProvider () let logger = loggerProvider.CreateLogger "Gitea.Declarative" - use client = new HttpClient () - client.BaseAddress <- args.GiteaHost - client.DefaultRequestHeaders.Add ("Authorization", $"token {args.GiteaAdminApiToken}") - - let client = Gitea.Client client |> IGiteaClient.fromReal + use httpClient = Utils.createHttpClient args.GiteaHost args.GiteaAdminApiToken + let client = Gitea.Client httpClient |> IGiteaClient.fromReal logger.LogInformation "Checking users..." let! userErrors = Gitea.checkUsers config client diff --git a/Gitea.Declarative/RefreshAuth.fs b/Gitea.Declarative/RefreshAuth.fs new file mode 100644 index 0000000..94eae1d --- /dev/null +++ b/Gitea.Declarative/RefreshAuth.fs @@ -0,0 +1,66 @@ +namespace Gitea.Declarative + +open System +open Argu +open Microsoft.Extensions.Logging + +type RefreshAuthArgsFragment = + | [] Gitea_Host of string + | [] Gitea_Admin_Api_Token of + string + | [] GitHub_Api_Token of string + | Dry_Run + + interface IArgParserTemplate with + member s.Usage = + match s with + | Gitea_Host _ -> "the Gitea host, e.g. https://gitea.mydomain.com" + | Gitea_Admin_Api_Token _ -> + "a Gitea admin user's API token; can be read from the environment variable GITEA_ADMIN_API_TOKEN" + | GitHub_Api_Token _ -> + "a GitHub API token with read access to every desired sync-from-GitHub repo; can be read from the environment variable GITHUB_API_TOKEN" + | Dry_Run -> "don't actually update the mirrors" + +type RefreshAuthArgs = + { + GiteaHost : Uri + GiteaAdminApiToken : string + GitHubApiToken : string + DryRun : bool + } + + static member OfParse + (parsed : ParseResults) + : Result + = + try + { + GiteaHost = parsed.GetResult RefreshAuthArgsFragment.Gitea_Host |> Uri + GiteaAdminApiToken = parsed.GetResult RefreshAuthArgsFragment.Gitea_Admin_Api_Token + GitHubApiToken = parsed.GetResult RefreshAuthArgsFragment.GitHub_Api_Token + DryRun = parsed.TryGetResult RefreshAuthArgsFragment.Dry_Run |> Option.isSome + } + |> Ok + with :? ArguParseException as e -> + Error e + +[] +module RefreshAuth = + + let run (args : RefreshAuthArgs) : Async = + async { + use httpClient = Utils.createHttpClient args.GiteaHost args.GiteaAdminApiToken + let client = Gitea.Client httpClient |> IGiteaClient.fromReal + + use loggerProvider = Utils.createLoggerProvider () + let logger = loggerProvider.CreateLogger "Gitea.Declarative" + + let! instructions = Gitea.toRefresh client + + if args.DryRun then + logger.LogInformation ("Stopping due to --dry-run.") + else + do! Gitea.refreshAuth logger client args.GitHubApiToken instructions + + return 0 + } diff --git a/Gitea.Declarative/Utils.fs b/Gitea.Declarative/Utils.fs new file mode 100644 index 0000000..3581089 --- /dev/null +++ b/Gitea.Declarative/Utils.fs @@ -0,0 +1,32 @@ +namespace Gitea.Declarative + +open System +open System.Net.Http +open Microsoft.Extensions.Logging.Console +open Microsoft.Extensions.Options + +[] +module internal Utils = + + let createLoggerProvider () = + let options = + let options = ConsoleLoggerOptions () + + { new IOptionsMonitor with + member _.Get _ = options + member _.CurrentValue = options + + member _.OnChange _ = + { new IDisposable with + member _.Dispose () = () + } + } + + new ConsoleLoggerProvider (options) + + let createHttpClient (host : Uri) (apiKey : string) = + let client = new HttpClient () + client.BaseAddress <- host + client.DefaultRequestHeaders.Add ("Authorization", $"token {apiKey}") + + client diff --git a/Gitea.InMemory/Client.fs b/Gitea.InMemory/Client.fs index f967c04..83109d8 100644 --- a/Gitea.InMemory/Client.fs +++ b/Gitea.InMemory/Client.fs @@ -75,6 +75,8 @@ module Client = member _.RepoAddPushMirror (user, repo, createPushMirrorOption) = failwith "Not implemented" + member _.RepoDeletePushMirror (user, repo, remoteName) = failwith "Not implemented" + member _.RepoListPushMirrors (loginName, userName, page, count) = failwith "Not implemented" member _.RepoListBranchProtection (loginName, userName) = failwith "Not implemented" diff --git a/Gitea.InMemory/Domain.fs b/Gitea.InMemory/Domain.fs index 5506854..bf111bc 100644 --- a/Gitea.InMemory/Domain.fs +++ b/Gitea.InMemory/Domain.fs @@ -35,6 +35,7 @@ type GiteaClientMock = UserListRepos : string * int64 option * int64 option -> Gitea.Repository array Task RepoAddPushMirror : string * string * Gitea.CreatePushMirrorOption -> Gitea.PushMirror Task + RepoDeletePushMirror : string * string * string -> unit Task RepoListPushMirrors : string * string * int64 option * int64 option -> Gitea.PushMirror array Task RepoListBranchProtection : string * string -> Gitea.BranchProtection array Task @@ -64,6 +65,7 @@ type GiteaClientMock = UserListRepos = fun _ -> failwith "Unimplemented" RepoAddPushMirror = fun _ -> failwith "Unimplemented" + RepoDeletePushMirror = fun _ -> failwith "Unimplemented" RepoListPushMirrors = fun _ -> failwith "Unimplemented" RepoListBranchProtection = fun _ -> failwith "Unimplemented" @@ -96,6 +98,9 @@ type GiteaClientMock = member this.RepoListPushMirrors (loginName, userName, page, count) = this.RepoListPushMirrors (loginName, userName, page, count) + member this.RepoDeletePushMirror (loginName, userName, remoteName) = + this.RepoDeletePushMirror (loginName, userName, remoteName) + member this.RepoListBranchProtection (login, user) = this.RepoListBranchProtection (login, user)