pkg/models/origin.go
Rama main · réplica de lectura del repositorio original.
package models
import "strconv"
// Origin is where a recorded test or call came from, under the proven switch;
// nil when recorded without it, and then nothing is
// written.
//
// Only a record under the switch sets one. A file's origin is read and carried
// whatever the switch: every reader (YAML, JSON, gob) takes it, and every
// rewrite of a mock or a test case (--remove-unused-mocks, the noise
// write-back, templatize, normalize, an update) writes it back, so a run
// without the switch never strips what a run with it recorded. Only its use
// (routing, the reason lines) is under the switch.
//
// A mock's origin is written as its document's top-level origin block
// (yaml.NetworkTrafficDoc.Origin), a test case's likewise: Mock.Origin and
// TestCase.Origin are yaml "-" because the document, not the model, is what a
// recording's file holds.
type Origin struct {
// Clock names the clock of the stamp that opens the record: a test case's
// window start (HTTPReq/GrpcReq.Timestamp), a mock's ReqTimestampMock.
// Equal non-empty values are one clock. "" is untrusted (a fallback, a
// substituted or post-hole stamp) and never routes. Values are
// fakeconn.ClockID's: "proxy:<agent run>", "ktime:<boot id>.<anchor>".
Clock string `json:"clock,omitempty" yaml:"clock,omitempty" bson:"clock,omitempty"`
// Comm and Exe are, on a mock, the process that made the call: its comm
// and argv[0]; for a program a go run built, whose argv[0] is a path in
// the go tool's build dir that differs on every run, that go run and its
// target in place of argv[0] (`go run .`), never its build flags. No
// more of its command line is ever stored.
Comm string `json:"comm,omitempty" yaml:"comm,omitempty" bson:"comm,omitempty"`
Exe string `json:"exe,omitempty" yaml:"exe,omitempty" bson:"exe,omitempty"`
// Unnamed is why Comm and Exe are empty on a mock or a call whose
// process keploy saw but would not name: UnnamedPIDNamespace. PID is
// then that process's kernel pid, which a log line gives instead of a
// name (ProcessText). Neither is ever written to a recording's file, a
// test case's or a report (yaml and bson "-", and Persisted, which every
// mock, test-case and report writer writes): the agent hands them to
// keploy record with the mock (gob) and to the replay with the call
// (json); the log lines that name the process say both, a committed line
// why alone (FileProcessText).
Unnamed string `json:"unnamed,omitempty" yaml:"-" bson:"-"`
PID uint32 `json:"pid,omitempty" yaml:"-" bson:"-"`
// Cmdline is, on a call only (UnmatchedCall.Source, never a mock), the
// whole command line of the process that made it, redacted as keploy's
// log of it is (containerkey.RedactArgs), and PID its kernel pid then
// too, so keploy mock's local receipt can name the process that made a
// call no test was charged with (Receipt.UnchargedMisses). Like Unnamed
// and PID, it only travels: no recording's file, test case's or
// report holds it (yaml and bson "-", and Persisted).
Cmdline string `json:"cmdline,omitempty" yaml:"-" bson:"-"`
}
// UnnamedPIDNamespace is Origin.Unnamed when keploy runs in a PID namespace
// of its own (utils.KernelPidsAreLocal false): the kernel pid its proxy is
// told names another process in keploy's /proc, or none, so keploy names
// none rather than guess.
const UnnamedPIDNamespace = "pid_namespace"
// pidNamespaceWhy is why keploy names no process it was told of by a
// kernel pid (UnnamedPIDNamespace), and what has it named.
const pidNamespaceWhy = "keploy runs in a PID namespace of its own, where the kernel's process numbers name other processes or none, so it reads no process by one; run keploy in the host PID namespace (--pid=host, or hostPID in Kubernetes) to have it named. keploy's own docker and compose modes run its agent in a PID namespace of its own (the application's), where it names no process yet"
// PIDNamespaceReason says why keploy names no process for kernel pid pid
// (UnnamedPIDNamespace), and how to have it named, as a log line says it.
func PIDNamespaceReason(pid uint32) string {
return "kernel pid " + strconv.FormatUint(uint64(pid), 10) + ": " + pidNamespaceWhy
}
// ProcessText names the process o says made a call, as a log line names it:
// "`sqs` (`./sqs`)" (comm, then argv[0] when it differs); for one keploy saw
// but would not name, "a process keploy could not name (" and
// PIDNamespaceReason ")", its kernel pid with it; "" when o names none,
// which the caller says its own way.
func (o *Origin) ProcessText() string {
return o.processText(true)
}
// FileProcessText is ProcessText as a committed line (mappings.yaml's
// reasons) names the process: for one keploy would not name, why without its
// kernel pid, which no committed file holds (a pid differs
// from one recording to the next).
func (o *Origin) FileProcessText() string {
return o.processText(false)
}
func (o *Origin) processText(withPID bool) string {
if o == nil {
return ""
}
switch {
case o.Comm == "" && o.Exe == "":
if o.Unnamed == UnnamedPIDNamespace {
if !withPID {
return "a process keploy could not name (" + pidNamespaceWhy + ")"
}
return "a process keploy could not name (" + PIDNamespaceReason(o.PID) + ")"
}
return ""
case o.Exe == "" || o.Exe == o.Comm:
return "`" + o.Comm + "`"
case o.Comm == "":
return "`" + o.Exe + "`"
}
return "`" + o.Comm + "` (`" + o.Exe + "`)"
}
// Persisted is what of o a file holds: o without Unnamed, PID and Cmdline,
// which only travel; nil when nothing is left, so a mock whose process keploy
// would not name and whose clock is unset is written with no origin, as one
// recorded without the switch is.
func (o *Origin) Persisted() *Origin {
if o == nil || o.Unnamed == "" && o.PID == 0 && o.Cmdline == "" {
return o
}
p := Origin{Clock: o.Clock, Comm: o.Comm, Exe: o.Exe}
if p == (Origin{}) {
return nil
}
return &p
}
// PersistedCalls is calls as a file holds them (a test run's report): each
// Source as Persisted leaves it, without what only travels. changed says
// whether any did; calls itself is returned when none did, else a copy:
// calls is never edited.
func PersistedCalls(calls []UnmatchedCall) (out []UnmatchedCall, changed bool) {
for i := range calls {
if p := calls[i].Source.Persisted(); p != calls[i].Source {
if !changed {
out, changed = append([]UnmatchedCall(nil), calls...), true
}
out[i].Source = p
}
}
if !changed {
return calls, false
}
return out, true
}
// clone is a copy of o that shares nothing with it; nil for nil.
func (o *Origin) clone() *Origin {
if o == nil {
return nil
}
c := *o
return &c
}
// clock is o's Clock, "" for a nil o: the clock of a record without an
// origin is untrusted.
func (o *Origin) clock() string {
if o == nil {
return ""
}
return o.Clock
}