Documentation
¶
Overview ¶
Package apilive is the committed record of what a booted GitLab says its own REST API is, and the one reader of it.
Every other oracle this repository holds about GitLab's REST API is a reading of text: the OpenAPI document GitLab commits to its own repository, or a scan of the Grape source that document is generated from. Both are downstream of the object that decides what a request returns, which is the Rails application with its classes loaded, and both lose the same thing when a name is not written down. GeoSiteStatus exposes its fields by iterating a constant assembled from two method calls: the source says "expose the loop variable", a scanner reads 26 fields, and GitLab sends 606. That is not a hole a better parser closes.
So this record is produced by asking the application. cmd/gen_api_live boots a released GitLab image, runs one script inside it, and writes what comes back here; every audit then reads this file with no Docker and no network, the way cmd/gen_graphql_schema's pin is read. The boot is a generator, never an audit: an audit that needed a container could not be a gate.
One thing evaluation does not give and the record therefore carries from source: a block condition is a Proc, and a Proc knows where it was written but not what it says. The generator reads those lines back from inside the same image, so a block condition arrives here both located and quoted. A hash or a symbol condition is the other way round: it holds what it tests, which the record keeps as its data or its option, and grape-entity keeps no location for it, so it arrives quoted and never located.
Index ¶
- Constants
- func LicensedFeatures(condition string) []string
- func NormalizePath(path string) string
- func OpenAPIName(rubyName string) string
- func Path(dir string) string
- func Write(dir string, doc Document) error
- type Condition
- type Document
- func (d Document) FieldCount() int
- func (d Document) FieldNames(entity string) []string
- func (d Document) Fields(entity string) (map[string]Field, bool)
- func (d Document) GateOf(field Field) Gate
- func (d Document) Names() []string
- func (d Document) NestedNames(entity string) map[string][]string
- func (d Document) Resolve(entity string) ([]Field, bool)
- func (d Document) RoutesByEntity() map[string][]Route
- func (d Document) Tier(features ...string) string
- type Entity
- type Field
- type Gate
- type Param
- type Route
- type Source
Constants ¶
const ( // TierPremium unlocks with any paid plan from Premium up. TierPremium = "premium" // TierUltimate unlocks with Ultimate only. TierUltimate = "ultimate" // TierGlobal is the table's GLOBAL_FEATURES: licensed, but not a tier, // since every paid plan carries it. TierGlobal = "global" )
Tier values, matching the licensed feature table's own three lists.
const DefaultDir = "docs/development"
DefaultDir is where the record lives, beside the other pinned records.
const EndpointPrefix = "/api/:version"
EndpointPrefix is what Grape mounts every route under. A caller writes /projects/:id and Grape holds /api/:version/projects/:id, so one of the two spellings has to give, and it is this one: the inventory records what the SDK sent, which is what a reader of a finding will go looking for.
const FileName = "gitlab-api-live.json"
FileName is the record's name on disk.
const SchemaVersion = 3
SchemaVersion is the shape of this record. A reader refuses a version it was not written for rather than guessing at a field that moved.
Version 2 records Field.Merge. The bump is not cosmetic: version 1 spelled a merged exposure exactly like a nested one, so a reader of an old record would resolve it into the wrong keys and never know, which is the one failure a version guard exists to stop.
Version 3 records what a hash or a symbol condition tests, Condition.Hash and Condition.Symbol. Version 2 had the hash key and never filled it, and had no key for a symbol at all, so every such condition reached the record as its kind alone: 41 of the 914 on 19.3.1-ee, each the only gate on its field. A reader of that record would take those 41 fields for unconditional, which is the same silent inversion version 2 was cut for.
Variables ¶
This section is empty.
Functions ¶
func LicensedFeatures ¶
LicensedFeatures names the features a condition asks a license about, in order of appearance and without repeats.
A symbol found here is not necessarily licensed: feature_available?(:issues) asks a project setting, not a plan. Document.Tier is what tells them apart, because only the licensed feature table can.
func NormalizePath ¶
NormalizePath trims the mount prefix off a route, leaving the path a caller writes.
Grape's own placeholders are already the spelling the request inventory records (:id, :user_id), so nothing else has to be rewritten. That is a property of asking the router rather than reading a generated document: the OpenAPI record spells the same placeholder {id} and had to be translated.
func OpenAPIName ¶
OpenAPIName is the entity's Ruby name in the spelling GitLab's OpenAPI document gives its schema, so a reader holding one can look up the other.
API::Entities::Ci::Variable becomes APIEntitiesCiVariable: the separators go and the segments keep their own casing.
Types ¶
type Condition ¶
type Condition struct {
// Kind is Grape's own class name for it: BlockCondition for a lambda,
// HashCondition for `if: {…}`, SymbolCondition for `if: :option`.
Kind string `json:"kind"`
// Inverse is true for `unless:`.
Inverse bool `json:"inverse,omitempty"`
// File and Line locate a block condition's lambda, repository relative.
// A hash or a symbol condition carries neither: grape-entity keeps the
// options it tests and nothing about where the exposure declaring it was
// written, and the exposure records no location of its own either.
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
// Text is those lines read back from inside the image and squeezed onto
// one. A Proc knows where it was written and not what it says, so this is
// the only way the condition arrives readable, and it is what a rule
// classifies on.
Text string `json:"text,omitempty"`
// Hash is a hash condition's own data, as Ruby inspects it: the options
// the presenter must be given, and the values they must hold.
Hash string `json:"hash,omitempty"`
// Symbol is a symbol condition's option, without its colon: the field is
// sent when the presenter is given that option with a truthy value, which
// is a choice of the endpoint rendering it and never of the entity.
Symbol string `json:"symbol,omitempty"`
}
Condition is one gate on an exposure.
func (Condition) Describe ¶ added in v3.1.0
Describe renders the condition the way a finding quotes it.
A block condition is its own text, which already reads as written. A symbol and a hash condition are spelled the way Grape declares them, `if: :option` and `if: {…}`, or `unless:` for an inverse one, because the bare option name reads as a field rather than as a gate. A condition carrying none of the three is never rendered as nothing: it is named by its kind, so a field it gates reads as gated by something unreadable rather than as gated by nothing, which is the opposite of what the record holds.
func (Condition) Readable ¶ added in v3.1.0
Readable reports whether the condition says what it tests: a block condition's text, a hash condition's data or a symbol condition's option.
One that carries none of them still gates its field, since Grape skips the exposure whenever it fails, but by something the record cannot name. That is what an introspection produces when it meets a condition kind it does not read, and it is what version 2 of the record did to every hash and symbol condition it held.
type Document ¶
type Document struct {
SchemaVersion int `json:"schema_version"`
Note string `json:"note"`
Source Source `json:"source"`
// Entities is every API::Entities class the instance had loaded, keyed by
// its Ruby name (API::Entities::Project). OpenAPIName translates that to
// the spelling the OpenAPI record uses, for a reader joining the two.
Entities map[string]Entity `json:"entities"`
// Routes is every endpoint Grape had mounted, in the order the router
// holds them.
Routes []Route `json:"routes"`
// Features maps a licensed feature symbol to the tier that unlocks it.
Features map[string]string `json:"features"`
}
Document is the committed record.
func Read ¶
Read loads the committed record.
A schema version this build was not written for is refused rather than decoded: the fields would parse and mean something else, which is the one failure a reader cannot detect later.
func (Document) FieldCount ¶
FieldCount totals the exposures across every entity, which is the figure a floor is set against: a record that lost half its fields is one an audit would read as GitLab having stopped sending them.
func (Document) FieldNames ¶
FieldNames lists the keys an entity sends, sorted, and nil for one this record does not hold. It is what an audit holds a response against.
func (Document) Fields ¶
Fields is the entity's exposures by the key GitLab sends, the last declaration of a name winning as Grape's own render does, and false for an entity this record does not hold.
Inheritance needs no resolving here: the introspection reads root_exposures, which is the class as it will render, with everything it inherits and everything an Enterprise module prepended already flattened in. The scanned record this replaced had to walk a parent chain to reach the same list, and could not see a prepend it had no file for. Merging is the one edge that still has to be followed, and Document.Resolve follows it.
func (Document) GateOf ¶
GateOf reads what a field's conditions demand, resolving the tier against this record's own licensed feature table.
Every condition contributes, a hash or a symbol one through the data it carries and one the record could not read through its kind, since Grape skips the exposure whenever any of them fails. Dropping one that carries no text is how 41 fields gated by an option or a license came to read as sent on every response.
func (Document) NestedNames ¶
NestedNames is, for each field of the entity that renders with an entity of its own, the keys that child sends.
This is the tree the record carries and a generated OpenAPI document flattens: `expose :author, using: Entities::UserBasic` says exactly which object sits under `author`, so a nested comparison joins on the edge rather than on a property name that happened to describe an object.
func (Document) Resolve ¶
Resolve is the entity's exposures in declaration order with every merged one replaced by the fields it contributes, and false for an entity this record does not hold.
A merged exposure sends its child's keys on the parent object and no key of its own, so it is the one place where the exposure list and the response differ. Reading it literally inverts both halves of a comparison at once: API::Entities::Member merges UserBasic, and an audit reading the raw list reported `user` as a key GitLab sends and we drop, while reporting the id, username, name, state and avatar_url we do publish as keys GitLab never sends.
A merged exposure's own conditions gate everything it contributes, since Grape skips the whole exposure when they fail, so they are carried onto each promoted field ahead of that field's own.
func (Document) RoutesByEntity ¶
RoutesByEntity indexes the endpoints each entity is annotated on, which is the join an audit walks: from a type, to the entity it models, to the endpoints that render it.
func (Document) Tier ¶
Tier resolves the highest tier the named features unlock at.
Ultimate wins over premium, and premium over global, because a field gated by two features needs the dearer plan. A feature the table does not list resolves to nothing: a project setting such as :issues is not a license.
type Entity ¶
type Entity struct {
// Error is set when the class refused to describe itself, which is a fact
// about that class rather than a reason to drop it: a silently missing
// entity reads as one GitLab does not have.
Error string `json:"error,omitempty"`
// Fields are the exposures in declaration order, with everything the
// class inherits already flattened in, because root_exposures answers for
// the class as it will render.
Fields []Field `json:"fields,omitempty"`
}
Entity is one Grape entity as the loaded class describes itself.
type Field ¶
type Field struct {
// Name is the key GitLab sends.
Name string `json:"name"`
// Attribute is the method the value comes from, recorded only when `as:`
// made it differ from Name: the key is what a client sees and the
// attribute is what a reader greps the source for.
Attribute string `json:"attribute,omitempty"`
// Using names the entity the value renders with, in the same keying as
// Document.Entities. It is the edge that makes the record a tree rather
// than a list.
Using string `json:"using,omitempty"`
// Merge is set for an exposure whose value is merged into the object
// around it instead of being placed under Name: `expose :user, merge:
// true, using: UserBasic` on a member sends the user's own keys on the
// member, and no `user` key at all.
//
// Recording it is what keeps the record from reading as the opposite of
// what GitLab sends. Without it a member said it carries a `user` object
// and said nothing about the id, username and name it really carries, so
// an audit reported the one key GitLab never sends as missing and the
// nine it does send as invented.
Merge bool `json:"merge,omitempty"`
// Conditions gate the field. Empty means GitLab sends it with every
// response of every endpoint that renders this entity.
Conditions []Condition `json:"conditions,omitempty"`
}
Field is one exposure.
type Gate ¶
type Gate struct {
// If and Unless are the conditions as [Condition.Describe] renders them,
// joined with " && " when a field carries several, since Grape requires
// all of them to pass.
If string
Unless string
// Tier is the highest plan the conditions demand, empty when they demand
// none.
Tier string
// Edition is "ee" when a condition was written under ee/, which is where
// Enterprise prepends live.
//
// It is read from the condition's own file, so a field with no condition
// has no location to answer from and reports nothing: an exposure records
// where its lambda was written and not where the exposure itself was.
//
// Only a block condition has a file. A hash or a symbol condition carries
// none, so a field gated by one of those alone never answers "ee", even
// when the exposure was written under ee/: an epic's reference, gated by
// `if: { with_reference: true }` in ee/lib/api/entities/epic.rb, reads
// with no edition. Empty therefore means "no located condition said so",
// never "written outside ee/".
Edition string
}
Gate is what a field's conditions amount to, in the terms a finding reports.
func (Gate) Gated ¶
Gated reports whether the field is sent under any condition at all.
It holds for every field that carries a condition, readable or not, because Document.GateOf renders each one through Condition.Describe, which never returns nothing. It used to fall false for a field whose only condition carried no text, and so reported the opposite of what the record holds.
type Param ¶
type Param struct {
Required bool `json:"required,omitempty"`
Type string `json:"type,omitempty"`
Default string `json:"default,omitempty"`
Desc string `json:"desc,omitempty"`
}
Param is one declared parameter.
type Route ¶
type Route struct {
Method string `json:"method"`
// Path is Grape's origin, without the (.:format) suffix the router adds,
// since no caller sends that.
Path string `json:"path"`
// Entity is what the endpoint's `desc … success/entity` annotation names,
// which is the same annotation GitLab's OpenAPI generator reads. It can be
// wrong about what the endpoint really presents, and being wrong in the
// record is better than being absent: GET /keys is annotated
// APIEntitiesUserWithAdmin and serves an SSH key with a user under it.
Entity string `json:"entity,omitempty"`
Summary string `json:"summary,omitempty"`
// Params is what the endpoint declares it accepts, keyed by name.
Params map[string]Param `json:"params,omitempty"`
}
Route is one mounted endpoint.
type Source ¶
type Source struct {
Image string `json:"image"`
Digest string `json:"digest,omitempty"`
Version string `json:"version"`
Revision string `json:"revision,omitempty"`
RetrievedAt string `json:"retrieved_at"`
// SHA256 is of the introspection output as it left the container, before
// this record wrapped it, so two runs of one image can be compared without
// the wrapper's own fields entering the digest.
SHA256 string `json:"sha256"`
Entities int `json:"entities"`
Fields int `json:"fields"`
Routes int `json:"routes"`
Features int `json:"features"`
}
Source is the instance the record was taken from.
The image reference and the version are both recorded because they answer different questions: the reference is what to run to reproduce this, and the version is what to compare against a GitLab release. A record whose version is behind the current release is stale in the sense that matters, and no amount of re-running the same image fixes it.