pkg/provenswitch/provenswitch.go
Rama main · réplica de lectura del repositorio original.
// Package provenswitch is the staging gate of the proven model: KEPLOY_PROVEN=1.
//
// Every row of the proven model (strict verdicts on misses and unconsumed test
// mocks, unread misses as an incomplete round, skip reasons, strict response
// comparison, and the rest of the design's build plan) lands behind this one
// switch, so it can merge piece by piece while today's behaviour stays exactly
// as it is for everyone who does not set it. The e2e lanes that exercise those
// rows set it. It is a staging gate, not a setting: the flip deletes this
// package together with every old path, and no user is meant to choose between
// the two behaviours.
//
// On is the ONE place the switch is read. keploy, enterprise and the
// integrations all call it, so a row cannot be on in one of them and off in
// another within one process.
//
// The agent is a separate process, and it does not inherit the variable: the
// native agent is started through sudo, which drops the caller's environment,
// and the agent container gets only the variables keploy passes it. So every
// launcher appends AgentArgs to the agent's argv, and the agent pins its switch
// from its --proven flag, on or off (Pin): an agent's own environment never
// decides, so an agent cannot run the new paths while the CLI that started it
// runs the old ones, or the reverse. Parsers run inside the agent, so they see
// the pinned switch too.
//
// The agent reports what it pinned on its health check (HealthHeader, State),
// and the CLI refuses an agent whose state is not its own (CheckAgent). A
// native or docker-run agent is checked at its bring-up, with the switch on or
// off, from the health answer that found it ready. A docker compose agent is
// checked before it is told to release the app, with the switch on only: with
// it off its switch is not read, so no agent is asked anything it was not asked
// before the switch existed. A compose agent is the one keploy wrote into the
// compose file, whose command carries the flag only when the switch is on.
package provenswitch
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
)
const (
// Env is the variable that turns the switch on for a keploy process:
// KEPLOY_PROVEN=1.
Env = "KEPLOY_PROVEN"
// AgentFlag is the hidden `keploy agent` flag that carries the switch from
// the CLI to the agent it starts (see AgentArgs).
AgentFlag = "proven"
// HealthHeader is the /agent/health response header in which an agent says
// whether its switch is on (State). An agent that does not send it predates
// the switch.
HealthHeader = "X-Keploy-Proven"
// VerifyEnv turns on, for a keploy run under the switch, the agent's
// exact view verify mode (pkg/agent/proxy, exactViewVerify): every read
// the view keeps materialised is recomputed and a difference panics.
// CI's lanes under the switch set it to 1; a user's run leaves it unset.
VerifyEnv = "KEPLOY_EXACT_VIEW_VERIFY"
// VerifyFlag is the hidden `keploy agent` flag that carries VerifyEnv to
// the agent, which, like KEPLOY_PROVEN, the agent does not inherit.
VerifyFlag = "exact-view-verify"
)
// ErrAgentSwitch marks an agent this keploy must not run against: the proven
// switch it reports is not this keploy's (CheckAgent). The bring-up of a native
// or docker-run agent fails with it. The docker compose readiness step returns
// it with the switch on (with it off it reads nothing), and record, replay and
// the runner stop on it (errors.Is). A compose agent that compose starts again
// mid-run is started from the same generated file, with the same flag.
var ErrAgentSwitch = errors.New("keploy's agent does not have this keploy's proven switch")
// The pinned states. unpinned (the zero value) means On reads the variable.
const (
unpinned int32 = iota
pinnedOff
pinnedOn
)
var pinned atomic.Int32
// On reports whether the proven model's rows are on in this process. A process
// whose switch was pinned (an agent, from its --proven flag) answers with what
// was pinned. Any other process reads KEPLOY_PROVEN, on every call, so the
// answer is its environment as it is now (and a test can set the switch with
// t.Setenv): on for a value strconv.ParseBool reads as true ("1", "true", …),
// off otherwise. A value it does not read at all is off here; keploy's own
// commands refuse it before they start (Validate).
func On() bool {
switch pinned.Load() {
case pinnedOn:
return true
case pinnedOff:
return false
}
on, err := parse(os.Getenv(Env))
return err == nil && on
}
// Pin fixes this process's switch for the rest of its life, on or off, and
// stops it reading KEPLOY_PROVEN. The agent pins from its --proven flag, so the
// CLI that started it is the only thing that decides.
func Pin(on bool) {
if on {
pinned.Store(pinnedOn)
return
}
pinned.Store(pinnedOff)
}
// Unpin undoes Pin, so the process reads KEPLOY_PROVEN again. keploy itself
// never calls it: it is for a test that runs the agent's flag handling (which
// pins) in a process whose later tests set the switch with t.Setenv.
func Unpin() {
pinned.Store(unpinned)
}
// IgnoredEnv is, for a pinned process, a KEPLOY_PROVEN in its environment
// that says something other than what was pinned (a value that is set and does
// not read as the pinned state, unreadable values included), and whether there
// is one. The agent warns about it: its environment never decides its switch.
func IgnoredEnv() (string, bool) {
p := pinned.Load()
v := os.Getenv(Env)
if p == unpinned || strings.TrimSpace(v) == "" {
return "", false
}
on, err := parse(v)
if err == nil && on == (p == pinnedOn) {
return "", false
}
return v, true
}
// State is On as a health-check value: "on" or "off".
func State() string {
if On() {
return "on"
}
return "off"
}
// AgentArgs is what a launcher appends to the argv of the agent it starts: the
// --proven flag when the switch is on in this process, nothing otherwise. With
// the switch off the agent's argv is exactly what it was before the switch
// existed. Under the switch, KEPLOY_EXACT_VIEW_VERIFY=1 (VerifyEnv) adds
// --exact-view-verify, so the agent recomputes each read of its exact view.
func AgentArgs() []string {
if !On() {
return nil
}
if os.Getenv(VerifyEnv) == "1" {
return []string{"--" + AgentFlag, "--" + VerifyFlag}
}
return []string{"--" + AgentFlag}
}
// Validate refuses a KEPLOY_PROVEN that is set to something strconv.ParseBool
// does not read ("yes", "on", a typo): such a value would otherwise mean "off"
// without a word, and run a staging lane on the old paths. Unset or empty is
// fine. keploy's CmdConfigurator.ValidateFlags calls it, so every command
// whose flags it validates (record, test, mock, report and the others whose
// PreRunE runs it) refuses such a value before it runs; the agent does not,
// because its switch comes from its --proven flag alone. Commands that do not
// validate their flags there, and k8s-proxy, do not refuse it yet.
func Validate() error {
if _, err := parse(os.Getenv(Env)); err != nil {
return fmt.Errorf("%s=%q is not a value keploy reads: set it to 1 to turn the proven model's staging switch on, or unset it", Env, os.Getenv(Env))
}
return nil
}
// CheckAgent compares an agent's health-check report of its switch (the
// HealthHeader value; "" when the agent sent none) with this process's, and
// says why they cannot run together when they differ: the agent's parsers and
// this keploy's verdicts would be on different paths. An agent that sends no
// report predates the switch, which is only a problem when the switch is on.
// The error wraps ErrAgentSwitch and names both sides.
func CheckAgent(reported string) error {
local := State()
switch {
case reported == "" && local == "off":
return nil
case reported == "":
return fmt.Errorf("%w: the switch is on in this keploy (%s) and its agent reports none, so the agent predates it; run the agent of this keploy version", ErrAgentSwitch, Env)
case reported == local:
return nil
}
return fmt.Errorf("%w: the switch is %s in this keploy (%s) and %s in its agent. An agent takes it only from the --%s flag of what starts it: this keploy, or k8s-proxy for the agents it starts; an agent started by anything else (such as a k8s-proxy release that does not pass the flag) or by another keploy version has its own",
ErrAgentSwitch, local, Env, reported, AgentFlag)
}
// parse reads a value of KEPLOY_PROVEN. The empty string is off and no error.
func parse(v string) (bool, error) {
v = strings.TrimSpace(v)
if v == "" {
return false, nil
}
return strconv.ParseBool(v)
}