Documentation
¶
Overview ¶
Package versioncheck reports whether a newer GoModel release exists.
It reads a plain-text manifest on a daily schedule and on the first dashboard visit of each day. The request carries the running version, the distribution name, an anonymous install identifier, and the dashboard's own hostname; it never carries API keys, provider credentials, model names, prompts, or usage data.
Every outbound request is jittered so gateways started together — a Helm rollout, a restarted docker-compose stack — do not query in lockstep.
Index ¶
- Constants
- func DueToday(value string, now time.Time) bool
- func IsNewer(current, latest string) bool
- func LeaksQueryInCleartext(raw string) bool
- func NewVisit(id string, now time.Time) string
- func SafeURL(raw string) string
- func SplitVisit(value string) (date, id string)
- type Beacon
- type Checker
- type Config
- type Identity
- type InstallIDSource
- type Status
- type Store
Constants ¶
const CookieMaxAge = 31536000
CookieMaxAge keeps a browser's id stable for a year.
const CookieName = "gomodel_version_check"
CookieName is the browser cookie holding the visit marker. Its value is YYYY-MM-DD-{id}: the day this browser last checked, plus a random id generated on the first visit. Dashboard JavaScript reads the date half to decide whether today's check has already happened, so the cookie is deliberately not HttpOnly.
const DefaultURL = "https://gomodel.enterpilot.io/version"
DefaultURL is the public release manifest served by the GoModel website. The channel file ("core.txt" or "pro.txt") is appended to it.
const InstallIDKey = "install_id"
InstallIDKey is the key the identifier is kept under in the deployment's database, alongside the runtime settings.
Variables ¶
This section is empty.
Functions ¶
func DueToday ¶
DueToday reports whether a browser presenting this cookie value has yet to check in on the given day.
func IsNewer ¶
IsNewer reports whether latest is a later release than current.
It is deliberately conservative: an unparseable or non-release local version ("dev", a bare commit) never reports an update, and build metadata is ignored, so a Pro build stamped "1.0.0+core.0.1.81" compares as 1.0.0. Within one release number a prerelease ranks below the final release ("1.0.0-rc1" < "1.0.0"), matching semantic versioning.
func LeaksQueryInCleartext ¶
LeaksQueryInCleartext reports whether a configured manifest URL would put its query string on the wire unencrypted. A private mirror may authenticate with a query token, and over plain HTTP that token is readable by anything on the path — redacting it from logs does not help there.
Reported rather than rejected: the URL is a deliberate operator choice, an internal mirror on a trusted network is a legitimate setup, and refusing to start the gateway over a non-essential update check would be a worse outcome than telling the operator what they have configured.
func NewVisit ¶
NewVisit builds a cookie value for today, reusing id when the browser already has one and minting a fresh one otherwise.
The day is UTC on both sides of the cookie: the dashboard reads the same value back, and a browser in a different timezone to the gateway would otherwise disagree about when a new day starts.
func SafeURL ¶
safeURL identifies the manifest host for errors and logs without quoting anything an operator may have embedded in the configured URL. A private mirror can carry a secret in userinfo, in the query, or in the path itself, so only the scheme and host survive.
func SplitVisit ¶
SplitVisit separates a cookie value into its date and id halves. An empty or malformed value yields empty strings, which callers treat as a first visit.
The id must be exactly the canonical UUID NewVisit generates. /version is unauthenticated and the id is echoed into an outbound request header and back into Set-Cookie, so an unvalidated one would let any caller put text of their choosing into both. Anything else is discarded and replaced with a fresh id, which is also the right answer for a cookie corrupted in transit.
Types ¶
type Beacon ¶
type Beacon struct {
UserAgent string
AcceptLanguage string
ClientHints map[string]string
Visit string
}
Beacon carries the allowlisted slice of a dashboard visit that travels with an update check.
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker owns the cached manifest result and the request budget.
func New ¶
New builds a Checker for the given distribution. It never returns an error: a disabled or misconfigured check degrades to reporting the local version.
func (*Checker) Refresh ¶
Refresh fetches the manifest and updates the cache. It returns the current status even when the request is throttled, out of budget, or fails, so callers can always answer with the local version.
type Config ¶
type Config struct {
Enabled bool
URL string
App string
Version string
InstallID string
// InstallIDFunc, when set, supplies the identifier per request instead
// of InstallID. An Identity uses it to keep retrying its database until
// it has confirmed which id this deployment has.
InstallIDFunc func(context.Context) string
Interval time.Duration
Timeout time.Duration
MaxDailyChecks int
// Client overrides the HTTP client, for tests.
Client *http.Client
// Now overrides the clock, for tests.
Now func() time.Time
}
Config configures a Checker. Zero values fall back to package defaults.
type Identity ¶ added in v0.1.85
type Identity struct {
// contains filtered or unexported fields
}
Identity resolves and remembers the stable, anonymous identifier for this deployment. It is a UUID that encodes nothing about the host, the operator, or the configuration, and only ever leaves the process on an update check.
"This deployment" is defined by whatever survives longest, in this order:
- The database, when a store is given. A gateway's database outlives its container (it is on a volume or on another host), so the identifier lives there first, and replicas sharing one database count as one.
- The install-id file in the data directory. The canonical location before the database was used; an existing id migrates to the database unchanged, so upgrading never creates a new deployment.
- An HMAC of the operator's secret, when one is configured. Nothing on disk survived (a container recreated without a volume) but the configuration did, and the same configuration is the same deployment.
- A random UUID.
Whichever step wins is written to the database and the file, so the copies converge and the next start takes the shortest path. The database write is insert-if-absent: when two replicas initialise at once, or the file and the database disagree, the database's value wins for everyone. The file is the copy that gets recreated by accident, the database the one that gets migrated on purpose.
A store that errors is not a store that is empty. The candidate from the file or the fallbacks is used for now, and the database is asked again on the next call, so an outage at startup can never mint a new identity or hide the real one for the life of the process.
func NewIdentity ¶ added in v0.1.85
NewIdentity prepares a resolver. store may be nil (no database) and secret may be empty (no derived fallback). Nothing is read until Resolve or ID.
func (*Identity) ID ¶ added in v0.1.85
ID returns the identifier for a request, resolving it on first use.
func (*Identity) Resolve ¶ added in v0.1.85
func (i *Identity) Resolve(ctx context.Context) (string, InstallIDSource)
Resolve returns the identifier and where it came from. Once the database has confirmed the value (or there is no database to ask) the answer is fixed; until then each call asks again, but keeps returning the same provisional value rather than minting another.
type InstallIDSource ¶ added in v0.1.85
type InstallIDSource string
InstallIDSource names where a resolved identifier came from, for the startup log.
const ( // SourceDatabase: read from the deployment's database. SourceDatabase InstallIDSource = "database" // SourceFile: read from the install-id file in the data directory. SourceFile InstallIDSource = "file" // SourceDerived: computed from the operator's secret because nothing was // stored; stable as long as the secret is. SourceDerived InstallIDSource = "derived" // SourceGenerated: freshly minted; nothing durable was available. SourceGenerated InstallIDSource = "generated" )
type Status ¶
type Status struct {
App string `json:"app"`
Version string `json:"version"`
Latest string `json:"latest,omitempty"`
UpdateAvailable bool `json:"update_available"`
CheckedAt string `json:"checked_at,omitempty"`
Enabled bool `json:"enabled"`
}
Status is the snapshot the /version endpoint serves. It is safe to expose: it contains only release metadata.
type Store ¶ added in v0.1.85
type Store interface {
Get(ctx context.Context, key string) (value string, found bool, err error)
// SetDefault stores value only when key has no value yet, and returns
// whatever is stored afterwards. It must be atomic across instances.
SetDefault(ctx context.Context, key, value string) (string, error)
}
Store is the durable key/value the identifier is kept in. It is satisfied by runtimesettings.Store: the identifier is per-deployment state exactly like a runtime setting, so it shares the table.