f1bcb8cd3a
repospawner turns JSON new-repo requests into terraform-git pull requests via kubernetes Jobs, follows those PRs to merge and optionally activates the repository in Woodpecker.
143 lines
3.8 KiB
Go
143 lines
3.8 KiB
Go
package jobs
|
|
|
|
import (
|
|
"git.unkin.net/unkin/repospawner/internal/store"
|
|
)
|
|
|
|
// Action is the next Job the reconciler should create for a request.
|
|
type Action string
|
|
|
|
const (
|
|
// ActionNone means the request is either waiting on a running Job or done.
|
|
ActionNone Action = ""
|
|
// ActionCreateWatch means the pull request exists and wants following.
|
|
ActionCreateWatch Action = "watch"
|
|
// ActionCreateWoodpecker means the merge landed and CI wants enabling.
|
|
ActionCreateWoodpecker Action = "woodpecker"
|
|
)
|
|
|
|
// Advance folds the observed Jobs for one request into its next state and the
|
|
// Job that should be created next. It is deliberately pure: the reconciler
|
|
// supplies the observations and performs the action.
|
|
func Advance(r store.Request, views map[Type]View) (store.Request, Action) {
|
|
if pr, ok := views[TypePR]; ok {
|
|
r = applyPR(r, pr)
|
|
}
|
|
watch, watching := views[TypeWatch]
|
|
if watching {
|
|
r = applyWatch(r, watch)
|
|
}
|
|
if r.State == store.StateFailed || r.State == store.StateClosed {
|
|
return r, ActionNone
|
|
}
|
|
|
|
// The pull request is open but nothing is following it yet.
|
|
if r.PRNumber > 0 && !watching && r.State != store.StateMerged &&
|
|
r.State != store.StateEnablingCI && r.State != store.StateReady {
|
|
r.State = store.StatePROpen
|
|
return r, ActionCreateWatch
|
|
}
|
|
|
|
if r.State != store.StateMerged && r.State != store.StateEnablingCI && r.State != store.StateReady {
|
|
return r, ActionNone
|
|
}
|
|
if !r.Woodpecker {
|
|
r.State = store.StateReady
|
|
return r, ActionNone
|
|
}
|
|
wp, ok := views[TypeWoodpecker]
|
|
if !ok {
|
|
if r.State == store.StateReady {
|
|
// The enablement Job already succeeded and its TTL expired.
|
|
return r, ActionNone
|
|
}
|
|
r.State = store.StateEnablingCI
|
|
return r, ActionCreateWoodpecker
|
|
}
|
|
return applyWoodpecker(r, wp), ActionNone
|
|
}
|
|
|
|
func applyPR(r store.Request, v View) store.Request {
|
|
switch {
|
|
case v.Succeeded:
|
|
var res PRResult
|
|
if !DecodeResult(v.Result, &res) || res.PRURL == "" {
|
|
r.State = store.StateFailed
|
|
r.Error = "pull request job finished without reporting a pull request"
|
|
return r
|
|
}
|
|
r.PRNumber, r.PRURL = res.PRNumber, res.PRURL
|
|
if r.State == store.StateOpeningPR || r.State == "" {
|
|
r.State = store.StatePROpen
|
|
}
|
|
case v.Failed:
|
|
r.State = store.StateFailed
|
|
r.Error = resultError(v.Result, "pull request job failed")
|
|
default:
|
|
if r.State == "" {
|
|
r.State = store.StateOpeningPR
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func applyWatch(r store.Request, v View) store.Request {
|
|
switch {
|
|
case v.Succeeded:
|
|
var res WatchResult
|
|
if !DecodeResult(v.Result, &res) {
|
|
r.State = store.StateFailed
|
|
r.Error = "watch job finished without reporting an outcome"
|
|
return r
|
|
}
|
|
switch {
|
|
case res.Merged:
|
|
if r.State != store.StateEnablingCI && r.State != store.StateReady {
|
|
r.State = store.StateMerged
|
|
}
|
|
case res.Closed:
|
|
r.State = store.StateClosed
|
|
default:
|
|
r.State = store.StateFailed
|
|
r.Error = resultError(v.Result, "watch job reported neither merge nor close")
|
|
}
|
|
case v.Failed:
|
|
r.State = store.StateFailed
|
|
r.Error = resultError(v.Result, "watch job failed")
|
|
default:
|
|
r.State = store.StatePROpen
|
|
}
|
|
return r
|
|
}
|
|
|
|
func applyWoodpecker(r store.Request, v View) store.Request {
|
|
switch {
|
|
case v.Succeeded:
|
|
var res WoodpeckerResult
|
|
if !DecodeResult(v.Result, &res) || !res.Enabled {
|
|
r.State = store.StateFailed
|
|
r.Error = resultError(v.Result, "woodpecker enablement job reported no activation")
|
|
return r
|
|
}
|
|
r.CIEnabled = true
|
|
r.State = store.StateReady
|
|
case v.Failed:
|
|
r.State = store.StateFailed
|
|
r.Error = resultError(v.Result, "woodpecker enablement job failed")
|
|
default:
|
|
r.State = store.StateEnablingCI
|
|
}
|
|
return r
|
|
}
|
|
|
|
// resultError prefers the job's own error string over the generic fallback.
|
|
func resultError(raw []byte, fallback string) string {
|
|
var res struct {
|
|
Error string `json:"error"`
|
|
}
|
|
if DecodeResult(raw, &res) && res.Error != "" {
|
|
return res.Error
|
|
}
|
|
return fallback
|
|
}
|