// 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) }