1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6 "bnema.dev/ermite/internal/domain"
7 "context"
8 "crypto/ed25519"
9 "time"
10)
11
12// MirrorStore keeps the mirror configuration, state and snapshots (kvstore implements it).
13type MirrorStore interface {
14 GetGlobal(ctx context.Context) (domain.MirrorGlobalConfig, error) // zero value, nil error when unset
15 PutGlobal(ctx context.Context, g domain.MirrorGlobalConfig) error
16 GetRepoConfig(ctx context.Context, repo domain.RepoName) (domain.MirrorRepoConfig, error) // domain.ErrNotFound when none
17 PutRepoConfig(ctx context.Context, c domain.MirrorRepoConfig) error // ErrNotFound when the repository is gone (guardRepoKey)
18 DeleteRepoConfig(ctx context.Context, repo domain.RepoName) error // missing is not an error
19 ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error) // by repo name; with ListStatus, what ListStatus of the service reads
20 GetStatus(ctx context.Context, repo domain.RepoName) (domain.MirrorStatus, error) // ErrNotFound when none
21 PutStatus(ctx context.Context, s domain.MirrorStatus) error
22 ListStatus(ctx context.Context) ([]domain.MirrorStatus, error) // stored statuses only
23 GetSnapshot(ctx context.Context, repo domain.RepoName) (domain.MirrorSnapshot, error) // ErrNotFound when none
24 PutSnapshot(ctx context.Context, repo domain.RepoName, s domain.MirrorSnapshot) error
25}
26
27// MirrorKeyStore keeps the sealed seed of the mirror key. The use case seals/opens it with
28// SecretSealer (AAD domain.MirrorKeyPath), as OIDC does.
29type MirrorKeyStore interface {
30 // GetMirrorKey returns the sealed seed and its creation time, or ErrNotFound.
31 GetMirrorKey(ctx context.Context) (sealed []byte, createdAt time.Time, err error)
32 // PutMirrorKey replaces the key; ifAbsent makes it fail with ErrAlreadyExists when one exists.
33 PutMirrorKey(ctx context.Context, sealed []byte, createdAt time.Time, ifAbsent bool) error
34}
35
36// MirrorPush is one push request to the transport.
37type MirrorPush struct {
38 Repo domain.RepoName
39 URL string
40 Refspecs []string // "<ref>:<ref>", no "+", from domain.MirrorRefspecs
41 PrivateKey ed25519.PrivateKey // written to a 0600 file for the duration of the push
42}
43type MirrorRefStatus string
44
45const (
46 MirrorRefOK MirrorRefStatus = "ok" // porcelain ' ' or '*'
47 MirrorRefUpToDate MirrorRefStatus = "up_to_date" // '='
48 MirrorRefRejected MirrorRefStatus = "rejected" // '!'
49)
50
51type MirrorRefOutcome struct {
52 Ref string
53 Status MirrorRefStatus
54 Reason string
55}
56type MirrorPushResult struct {
57 Refs []MirrorRefOutcome
58 Duration time.Duration
59}
60
61// MirrorPusher runs `git push --porcelain` (gitpush implements it). A failed push returns a
62// *domain.MirrorError (classified); ctx cancellation is returned as is. A result with a rejected
63// ref and err == nil never happens: a rejection is a MirrorError of class diverged.
64type MirrorPusher interface {
65 Push(ctx context.Context, req MirrorPush) (MirrorPushResult, error)
66}
67
68// MirrorTrigger is what Pushes.Record tells the mirrors. *usecase.Mirrors implements it.
69type MirrorTrigger interface {
70 OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate) // never blocks
71}
72
73// MirrorWarner is what the post-receive hook and the web ask. *usecase.Mirrors implements it.
74type MirrorWarner interface {
75 // Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>")
76 // and p has repo:admin on repo: the remote and the reason are for admins only.
77 Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string
78 // Badge is the state and reason for a viewer: reason is "" unless p may administer the repository.
79 Badge(ctx context.Context, p domain.Principal, repo domain.RepoName) (domain.MirrorState, string)
80}
81type MirrorSyncResult struct {
82 Repo domain.RepoName
83 Status domain.MirrorStatus
84 Queued bool
85 NoOp bool
86 Err error // SyncAll only: why this repository failed; the other repositories still ran
87}
88
89// MirrorKeyRotation is the outcome of RotateKey; Previous is zero when no key existed.
90type MirrorKeyRotation struct {
91 Key domain.MirrorPublicKey
92 Previous domain.MirrorPublicKey
93}
94
95// MirrorService is the inbound port of the API. Authorization: key show needs a non-anonymous
96// principal; key rotate, global config and ListStatus need HasOwnerRights(); per-repo config and
97// Sync need Allows(ScopeRepoAdmin, repo); GetStatus needs read access to the repository.
98type MirrorService interface {
99 ShowKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) // generates lazily
100 RotateKey(ctx context.Context, p domain.Principal) (MirrorKeyRotation, error)
101 GetGlobalConfig(ctx context.Context, p domain.Principal) (domain.MirrorGlobalConfig, error)
102 SetGlobalConfig(ctx context.Context, p domain.Principal, patch MirrorGlobalPatch) (domain.MirrorGlobalConfig, error)
103 SetRepoMirror(ctx context.Context, p domain.Principal, c domain.MirrorRepoConfig) (domain.MirrorTarget, error)
104 RemoveRepoMirror(ctx context.Context, p domain.Principal, repo domain.RepoName) error
105 SetRepoExcluded(ctx context.Context, p domain.Principal, repo domain.RepoName, excluded bool) (domain.MirrorTarget, error)
106 GetStatus(ctx context.Context, p domain.Principal, repo domain.RepoName) (MirrorStatusView, error)
107 ListStatus(ctx context.Context, p domain.Principal) ([]MirrorStatusView, error)
108 Sync(ctx context.Context, p domain.Principal, repo domain.RepoName, force bool) (MirrorSyncResult, error)
109 // SyncAll skips disabled mirrors and continues past failures, reported per repository in Err.
110 SyncAll(ctx context.Context, p domain.Principal, force bool) ([]MirrorSyncResult, error)
111}
112type MirrorGlobalPatch struct {
113 Enabled *bool
114 URLTemplate *string
115 Cron *string
116 AllBranches *bool
117}
118type MirrorStatusView struct {
119 Target domain.MirrorTarget
120 Status domain.MirrorStatus
121 NextRunAt time.Time
122}