169 lines
4.8 KiB
Go
169 lines
4.8 KiB
Go
package vendor
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/m3tam3re/agent-lib/internal/discovery"
|
||
|
|
"github.com/m3tam3re/agent-lib/internal/lockfile"
|
||
|
|
)
|
||
|
|
|
||
|
|
// CollisionError names both parties of a deployed-name clash.
|
||
|
|
type CollisionError struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
DeployedID string `json:"deployed_id"`
|
||
|
|
Source string `json:"source"`
|
||
|
|
OtherOwner string `json:"other_owner"`
|
||
|
|
OtherID string `json:"other_id"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *CollisionError) Error() string {
|
||
|
|
return fmt.Sprintf("source %q: %s %q collides with %s %s %q",
|
||
|
|
e.Source, e.Type, e.DeployedID, e.OtherOwner, e.Type, e.OtherID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// deployedIndex maps type -> deployed name -> owner description, built from
|
||
|
|
// the work repository's own items and every lockfile source except skip.
|
||
|
|
type deployedIndex map[string]map[string]owner
|
||
|
|
|
||
|
|
type owner struct {
|
||
|
|
source string
|
||
|
|
id string
|
||
|
|
}
|
||
|
|
|
||
|
|
func ownDeployed(workDir string) (deployedIndex, error) {
|
||
|
|
items, err := discovery.Scan(&discovery.FsTree{Root: workDir}, discovery.Config{})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
idx := deployedIndex{}
|
||
|
|
for _, it := range items {
|
||
|
|
idx.add(it.Type, it.UpstreamID, owner{source: "own", id: it.UpstreamID})
|
||
|
|
}
|
||
|
|
return idx, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func lockfileDeployed(lf *lockfile.Lockfile, skip string) deployedIndex {
|
||
|
|
idx := deployedIndex{}
|
||
|
|
for name, src := range lf.Sources {
|
||
|
|
if name == skip {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for typ, ids := range src.Inventory {
|
||
|
|
for _, id := range ids {
|
||
|
|
idx.add(typ, deployedName(id, src.Renames), owner{source: name, id: id})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return idx
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d deployedIndex) add(typ, deployed string, o owner) {
|
||
|
|
if d[typ] == nil {
|
||
|
|
d[typ] = map[string]owner{}
|
||
|
|
}
|
||
|
|
d[typ][deployed] = o
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d deployedIndex) find(typ, deployed string) (owner, bool) {
|
||
|
|
o, ok := d[typ][deployed]
|
||
|
|
return o, ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func mergeIndex(a, b deployedIndex) deployedIndex {
|
||
|
|
out := deployedIndex{}
|
||
|
|
for typ, m := range a {
|
||
|
|
for dep, o := range m {
|
||
|
|
out.add(typ, dep, o)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for typ, m := range b {
|
||
|
|
for dep, o := range m {
|
||
|
|
out.add(typ, dep, o)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// checkCollisions verifies that every selected item of source deploys into
|
||
|
|
// the flat namespace without clashing — against own items, against other
|
||
|
|
// sources, and within the source itself (renames included). It performs no
|
||
|
|
// filesystem mutation.
|
||
|
|
func checkCollisions(workDir string, lf *lockfile.Lockfile, source string, selected []discovery.Item, renames map[string]string) error {
|
||
|
|
own, err := ownDeployed(workDir)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("scanning own items: %w", err)
|
||
|
|
}
|
||
|
|
existing := mergeIndex(own, lockfileDeployed(lf, source))
|
||
|
|
|
||
|
|
seen := map[string]string{}
|
||
|
|
for _, it := range selected {
|
||
|
|
deployed := deployedName(it.UpstreamID, renames)
|
||
|
|
if deployed == "" {
|
||
|
|
return fmt.Errorf("source %q: rename of %q maps to an empty name", source, it.UpstreamID)
|
||
|
|
}
|
||
|
|
if prev, dup := seen[deployed]; dup {
|
||
|
|
return &CollisionError{
|
||
|
|
Type: it.Type, DeployedID: deployed, Source: source,
|
||
|
|
OtherOwner: "source " + source, OtherID: prev,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
seen[deployed] = it.UpstreamID
|
||
|
|
if o, clash := existing.find(it.Type, deployed); clash {
|
||
|
|
return &CollisionError{
|
||
|
|
Type: it.Type, DeployedID: deployed, Source: source,
|
||
|
|
OtherOwner: o.source, OtherID: o.id,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// validateCollisions re-checks the deployed namespace across own items and
|
||
|
|
// all lockfile sources — the offline equivalent used by validate.
|
||
|
|
func validateCollisions(workDir string, lf *lockfile.Lockfile) []ValidationError {
|
||
|
|
own, err := ownDeployed(workDir)
|
||
|
|
if err != nil {
|
||
|
|
return []ValidationError{{Rule: "collision", Message: fmt.Sprintf("scanning own items: %v", err)}}
|
||
|
|
}
|
||
|
|
idx := mergeIndex(own, deployedIndex{})
|
||
|
|
var errs []ValidationError
|
||
|
|
for _, name := range sortedSourceNames(lf) {
|
||
|
|
src := lf.Sources[name]
|
||
|
|
for typ, ids := range src.Inventory {
|
||
|
|
for _, id := range ids {
|
||
|
|
deployed := deployedName(id, src.Renames)
|
||
|
|
if prev, dup := idx.find(typ, deployed); dup {
|
||
|
|
errs = append(errs, ValidationError{
|
||
|
|
Source: name,
|
||
|
|
Rule: "collision",
|
||
|
|
Message: fmt.Sprintf("source %q: %s %q (upstream %q) collides with %s %q",
|
||
|
|
name, typ, deployed, id, prev.source, prev.id),
|
||
|
|
})
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
idx.add(typ, deployed, owner{source: name, id: id})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Slice(errs, func(i, j int) bool { return errs[i].Message < errs[j].Message })
|
||
|
|
return errs
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseRenames converts --rename upstream-id=deployed-name flag values.
|
||
|
|
func ParseRenames(pairs []string) (map[string]string, error) {
|
||
|
|
if len(pairs) == 0 {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
out := map[string]string{}
|
||
|
|
for _, p := range pairs {
|
||
|
|
upstream, deployed, ok := strings.Cut(p, "=")
|
||
|
|
if !ok || strings.TrimSpace(upstream) == "" || strings.TrimSpace(deployed) == "" {
|
||
|
|
return nil, fmt.Errorf("invalid --rename %q (want upstream-id=deployed-name)", p)
|
||
|
|
}
|
||
|
|
out[strings.TrimSpace(upstream)] = strings.TrimSpace(deployed)
|
||
|
|
}
|
||
|
|
return out, nil
|
||
|
|
}
|