Files
mesh-controller/cmd/mesh-controller/forge_read.go
T

144 lines
4.9 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"path"
"slices"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// What the forge says of a change's manifests (novox/hq ADR 0502): read through the forge's module on the bus, once per
// commit. Kept by release 6 for the pull request's declared config; the delivery plan's reads of it are gone.
// changeSource is one change to a repository whose manifests are read: the commit after it and, when known, what it
// replaced (a pull request's base commit or branch). An empty Old is the first parent of New: the merge's base.
type changeSource struct {
Owner, Repo string
New, Old string
Paths []string
Truncated bool
Removed []string
// Since is the first commit of the repository in the change, when the change is several merges: what it replaced
// is the first parent of that one, so a verb an earlier merge added is added by the change too.
Since string
}
// manifestAt reads a repository's module.json at a ref from the forge; the bool says it exists there. A variable so a
// test needs no forge.
var manifestAt = func(ctx context.Context, owner, repo, file, ref string) (catalogue.Manifest, bool, error) {
var found struct {
Result struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
} `json:"result"`
}
missing, err := askForgeApi(ctx, "/repos/"+owner+"/"+repo+"/contents/"+escapedPath(file)+"?ref="+url.QueryEscape(ref), &found)
if err != nil {
return catalogue.Manifest{}, false, err
}
if missing {
return catalogue.Manifest{}, false, nil
}
body := []byte(found.Result.Content)
if found.Result.Encoding == "base64" {
if body, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(found.Result.Content, "\n", "")); err != nil {
return catalogue.Manifest{}, false, fmt.Errorf("%s/%s %s at %s cannot be read: %w", owner, repo, file, ref, err)
}
}
var m catalogue.Manifest
if err := json.Unmarshal(body, &m); err != nil {
return catalogue.Manifest{}, false, fmt.Errorf("%s/%s %s at %s is not a manifest: %w", owner, repo, file, ref, err)
}
return m, true, nil
}
func escapedPath(p string) string {
parts := strings.Split(p, "/")
for i := range parts {
parts[i] = url.PathEscape(parts[i])
}
return strings.Join(parts, "/")
}
// askForgeApi makes one read of the forge's API through its module's tool on the bus. It answers true when the forge
// says there is no such thing (404), and an error for anything else it cannot answer.
func askForgeApi(ctx context.Context, apiPath string, into any) (bool, error) {
js, err := aBus()
if err != nil {
return false, err
}
defer js.Close()
raw, _ := json.Marshal(map[string]any{"path": apiPath})
answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_api", raw, 30*time.Second)
if err != nil {
return false, err
}
if answer.Error != "" {
if strings.Contains(answer.Error, " 404 ") {
return true, nil
}
return false, fmt.Errorf("the forge did not answer %s: %s", apiPath, answer.Error)
}
if err := json.Unmarshal(answer.Result, into); err != nil {
return false, fmt.Errorf("the forge's answer to %s cannot be read: %w", apiPath, err)
}
return false, nil
}
type manifestRead struct {
manifest catalogue.Manifest
exists bool
}
func manifestCached(ctx context.Context, owner, repo, file, ref string) (catalogue.Manifest, bool, error) {
key := strings.Join([]string{owner, repo, file, ref}, "\x00")
if len(ref) == 40 {
manifestsReadMu.Lock()
got, ok := manifestsRead[key]
manifestsReadMu.Unlock()
if ok {
return got.manifest, got.exists, nil
}
}
m, exists, err := manifestAt(ctx, owner, repo, file, ref)
if err == nil && len(ref) == 40 {
manifestsReadMu.Lock()
if len(manifestsRead) > 512 {
clear(manifestsRead)
}
manifestsRead[key] = manifestRead{m, exists}
manifestsReadMu.Unlock()
}
return m, exists, err
}
// manifestsRead keeps what the forge said of a manifest at a commit, which never changes: one ask per commit.
var (
manifestsRead = map[string]manifestRead{}
manifestsReadMu sync.Mutex
)
// truncatedPaths are the manifests a change that names only a beginning of its files is read as changing: those of the
// modules it moves that are built from its repository. Said, never skipped: no edge may be lost to a long list.
func truncatedPaths(entries []inventory.Entry, owner, repo string, moved []string) []string {
var out []string
for _, e := range entries {
if slices.Contains(moved, e.Manifest.Module) && sameRepository(e.Source.Repository, link.SourceMoved{Owner: owner, Repo: repo}) {
out = append(out, path.Join(strings.Trim(e.Source.Path, "/"), moduleManifestFile))
}
}
fmt.Printf(" %s/%s changed more files than were listed: the manifests of its modules (%s) are read as changed\n",
owner, repo, strings.Join(out, ", "))
return out
}