db

package
v0.0.0-...-d2f00f9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 6, 2024 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

Package db handles all the core database interaction It includes processors for each core type (blob, bottle, manifest, and event).

Index

Constants

View Source
const BlobProcessorVersion = 3

BlobProcessorVersion is the current version of the processor code. This is incremented after each measurable change to the BlobProcessor().

View Source
const BottleProcessorVersion = 11

BottleProcessorVersion is the current version of the processor code. This is incremented after each measurable change to the BottleProcessor().

View Source
const CanonicalDigestAlgorithm = digest.Canonical

CanonicalDigestAlgorithm is the digest used internally for de-duplication and discovering aliases.

View Source
const EventProcessorVersion = 3

EventProcessorVersion is the current version of the processor code. This is incremented after each measurable change to the EventProcessor().

View Source
const ManifestProcessorVersion = 4

ManifestProcessorVersion is the current version of the processor code. This is incremented after each measurable change to the ManifestProcessor().

View Source
const SignatureProcessorVersion = 1

SignatureProcessorVersion is the current version of the processor code. This is incremented after each measurable change to the SignatureProcessor().

Variables

This section is empty.

Functions

func ArtifactsCount

func ArtifactsCount(con *gorm.DB) int64

ArtifactsCount will get total count of Artifacts in DB.

func BlobDataBytes

func BlobDataBytes(con *gorm.DB) int64

BlobDataBytes will get the total number of bytes for data blobs in DB.

func BottlePulls

func BottlePulls(con *gorm.DB, dgst digest.Digest) int64

BottlePulls will query the events and return the pull Request count of the provided digest.

func BottlesCount

func BottlesCount(con *gorm.DB) int64

BottlesCount will get total count of Bottles in DB.

func ChildrenOf

func ChildrenOf(digests []digest.Digest) func(db *gorm.DB) *gorm.DB

ChildrenOf is a scope that will the query to children of the provided digests.

func DeprecatedBy

func DeprecatedBy(dgst digest.Digest) func(db *gorm.DB) *gorm.DB

DeprecatedBy is a scope that will query to digests that are deprecated by the provided digest.

func DeprecatesThis

func DeprecatesThis(dgst digest.Digest) func(db *gorm.DB) *gorm.DB

DeprecatesThis is a scope that will query to digests that deprecates the provided digest.

func EventsCount

func EventsCount(con *gorm.DB) int64

EventsCount will get total count of events in DB.

func ExcludeDeprecated

func ExcludeDeprecated() func(db *gorm.DB) *gorm.DB

ExcludeDeprecated is a scope that will prevent bottles from being included in the query if they are deprecated by another bottle.

func FilterByDigest

func FilterByDigest(dgst digest.Digest, objType string) func(db *gorm.DB) *gorm.DB

FilterByDigest will use the digest to filter the query for an object type objType must "belong to" a Data record. Bottle, Manifest, Event, and PublicArtifact all have this property.

func FilterByDigests

func FilterByDigests(dgsts []digest.Digest, objType string) func(db *gorm.DB) *gorm.DB

FilterByDigests will use the digests to filter the query for an object type objType must "belong to" a Data record. Bottle, Manifest, Event, and PublicArtifact all have this property.

func FilterByParts

func FilterByParts(partDigests []digest.Digest) func(db *gorm.DB) *gorm.DB

FilterByParts is a scope that will show bottles that have all parts with provided digests.

func FilterByPublicKeyFP

func FilterByPublicKeyFP(fp digest.Digest) func(db *gorm.DB) *gorm.DB

FilterByPublicKeyFP scopes the request to signatures with a given fingerprint.

func FilterBySelectors

func FilterBySelectors(selectors []string) func(db *gorm.DB) *gorm.DB

FilterBySelectors scopes the request to a bottle selector.

func FilterByTrustLevel

func FilterByTrustLevel(filter string) func(db *gorm.DB) *gorm.DB

FilterByTrustLevel selects records that include a trust level matching the trust level string eg "verified", "trusted".

func FindDeprecatedBy

func FindDeprecatedBy(con *gorm.DB, dgst digest.Digest) ([]digest.Digest, error)

FindDeprecatedBy finds all bottles that are deprecated by the bottle with the given digest.

func FindDeprecates

func FindDeprecates(con *gorm.DB, dgst digest.Digest) ([]digest.Digest, error)

FindDeprecates finds all bottles that deprecates the bottle with the given digest.

func FindDigests

func FindDigests(con *gorm.DB, dataID uint) ([]digest.Digest, error)

FindDigests returns all the digests known for a given data item.

func GetSignaturesWithAnnotations

func GetSignaturesWithAnnotations(ctx context.Context, con *gorm.DB, signatures *[]Signature) (*[]Signature, error)

GetSignaturesWithAnnotations takes a list of signatures without annotations and returns a slice of those same signatures with annotations included.

func IncludeDigests

func IncludeDigests(tableName string) func(db *gorm.DB) *gorm.DB

IncludeDigests is a scope that must be used with Digested above. It populates the DigestsCSV field.

func IncludeIsDeprecated

func IncludeIsDeprecated() func(db *gorm.DB) *gorm.DB

IncludeIsDeprecated includes an extra column "is_deprecated" if the bottle is deprecated.

func IncludeNumPulls

func IncludeNumPulls() func(db *gorm.DB) *gorm.DB

IncludeNumPulls includes an extra column "num_pulls" which is the number of pull events for the bottle.

func ManifestsCount

func ManifestsCount(con *gorm.DB) int64

ManifestsCount will get total count of Manifests in DB.

func MigrateDB

func MigrateDB(ctx context.Context, conn *gorm.DB, scheme *runtime.Scheme) error

MigrateDB migrates the DB to the new schema and reprocesses bottles.

func Open

func Open(ctx context.Context, conf v1alpha1.Database, scheme *runtime.Scheme) (*gorm.DB, error)

Open opens a DB connection using URL formatting. Also migrates the database For example file:test.db, "file::memory:" or postgres://jack:secret@foo.example.com:5432,bar.example.com:5432/mydb

func OpenPostgresDB

func OpenPostgresDB(dsn string) (*gorm.DB, error)

OpenPostgresDB connect to a Postgres database.

func OpenSqliteDB

func OpenSqliteDB(dsn string) (*gorm.DB, error)

OpenSqliteDB connect to a sqlite database (file).

func ParentsOf

func ParentsOf(digests []digest.Digest) func(db *gorm.DB) *gorm.DB

ParentsOf is a scope that will the query to parents of the provided digests.

func RankByDescription

func RankByDescription(description string) func(db *gorm.DB) *gorm.DB

RankByDescription will use FTS if available to rank order the search by best match on description.

func RankByNumPulls

func RankByNumPulls() func(db *gorm.DB) *gorm.DB

RankByNumPulls orders the bottles by number of pull events each has.

func Reprocess

func Reprocess(ctx context.Context, con *gorm.DB, processor Processor) error

Reprocess reprocesses the given type that are out of date w.r.t. the provided processor.

func SearchByAuthor

func SearchByAuthor(author string) func(db *gorm.DB) *gorm.DB

SearchByAuthor will search by Author name or email.

func SearchByRepository

func SearchByRepository(bottleRepo string) func(db *gorm.DB) *gorm.DB

SearchByRepository will search by image repository that the bottle is stored in.

func SignaturesCount

func SignaturesCount(con *gorm.DB) int64

SignaturesCount will get total count of Artifacts in DB.

func UserPulls

func UserPulls(con *gorm.DB, dgst digest.Digest, limit int) (map[string]int, error)

UserPulls will query the events and return a mapping of username to number of pulls for a given bottle.

func WithSignature

func WithSignature(digests []digest.Digest) func(db *gorm.DB) *gorm.DB

WithSignature scopes the request to bottles with a signature that has the given fingerprint.

func WithSignatureAnnotations

func WithSignatureAnnotations(annotations []string) func(db *gorm.DB) *gorm.DB

WithSignatureAnnotations scopes the request to bottles with a signature that has the given annotation.

Types

type Annotation

type Annotation struct {
	Model
	BottleID uint

	Key   string // unique per bottle
	Value string
}

Annotation is bottle annotations.

func (Annotation) GetLocation

func (a Annotation) GetLocation() string

GetLocation gets the index (key of the annotation).

type Author

type Author struct {
	BottleMemberLocated

	Name  string `gorm:"index"`
	URL   string // TODO should this be URI as well?
	Email string `gorm:"index"`
}

Author is author information.

type Base

type Base struct {
	Model
	ProcessorVersion uint `gorm:"index"`
	Data             Data // Base belongs to Data
	DataID           uint
}

Base is the common base for all four core types.

type Blob

type Blob struct {
	Base
}

Blob is raw data (typically a public artifact of a bottle).

type BlobProcessor

type BlobProcessor struct{}

BlobProcessor handles blob processing.

func (*BlobProcessor) PrimaryTable

func (p *BlobProcessor) PrimaryTable() string

PrimaryTable returns primary table that this processor updates.

func (*BlobProcessor) Process

func (p *BlobProcessor) Process(con *gorm.DB, base Base) error

Process converts Bottle data to the DB model for a Bottle.

func (*BlobProcessor) Version

func (p *BlobProcessor) Version() uint

Version returns the processor version.

type Bottle

type Bottle struct {
	Base
	APIVersion      string
	Description     string
	Authors         []Author         // Bottle has many authors
	Sources         []Source         // Bottle has many sources
	Metrics         []Metric         // Bottle has many metrics
	PublicArtifacts []PublicArtifact // Bottle has many public_artifacts
	Labels          []Label          // Bottle has many labels
	Annotations     []Annotation     // Bottle has many annotations
	Parts           []Part           // Bottle has many parts
	Deprecates      []Deprecates     // Bottle has many deprecates
	Signatures      []Signature      // Bottle has many signatures
}

Bottle is the metadata for a Bottle.

type BottleMemberLocated

type BottleMemberLocated struct {
	Model
	BottleID uint // foreign key

	// Location in the array from JSON
	Location uint
}

BottleMemberLocated is the base for all "has many" members of a Bottle.

func (BottleMemberLocated) GetLocation

func (b BottleMemberLocated) GetLocation() uint

GetLocation gets the location in the original array.

type BottleProcessor

type BottleProcessor struct {
	// contains filtered or unexported fields
}

BottleProcessor handles bottle processing.

func NewBottleProcessor

func NewBottleProcessor(scheme *runtime.Scheme) *BottleProcessor

NewBottleProcessor creates a new BottleProcessor populating the Scheme and Codecs.

func (*BottleProcessor) PrimaryTable

func (p *BottleProcessor) PrimaryTable() string

PrimaryTable returns primary table that this processor updates.

func (*BottleProcessor) Process

func (p *BottleProcessor) Process(con *gorm.DB, base Base) error

Process converts Bottle data to the DB model for a Bottle.

func (*BottleProcessor) Version

func (p *BottleProcessor) Version() uint

Version returns the processor version.

type BottleRelative

type BottleRelative struct {
	Bottle
	Digested
	PartSelectors selectors.LabelSelectorSet `gorm:"-"` // Optional if the relationship is partial
}

BottleRelative is a bottle relative to a bottle (parent, child, grand parent, etc.).

func (BottleRelative) GetSourceByRelative

func (r BottleRelative) GetSourceByRelative(relative *BottleRelative) *Source

GetSourceByRelative returns a pointer to a source if one of the given relative's digests matches one of the sources on the BottleRelative. If no source is found, nil is returned.

func (BottleRelative) GetSourceByURI

func (r BottleRelative) GetSourceByURI(uri string) *Source

GetSourceByURI returns a pointer to a source if the given URI matches one of the sources on the BottleRelative. If no source is found, nil is returned.

func (BottleRelative) MatchesSource

func (r BottleRelative) MatchesSource(source Source) bool

MatchesSource returns true if the source matches this relative.

type Data

type Data struct {
	Model
	RawData         []byte
	CanonicalDigest digest.Digest `gorm:"uniqueIndex"` // canonical digest is an internal only digest (like blake2 or sha3 used for de-duplication)
}

Data stores the actual data for all types (blob, bottles, manifests, events).

type DefaultTrustAnchor

type DefaultTrustAnchor struct{}

DefaultTrustAnchor can be used to get a trusted value back when no other trust anchor is available.

func (*DefaultTrustAnchor) VerifyTrust

func (t *DefaultTrustAnchor) VerifyTrust() bool

VerifyTrust on the DefaultTrustAnchor always returns false.

type Deprecates

type Deprecates struct {
	BottleMemberLocated

	DeprecatedBottleDigest digest.Digest
}

Deprecates is a deprecated Bottle.

type Digest

type Digest struct {
	Model
	Data   Data // Digest belongs to Data
	DataID uint
	Digest digest.Digest `gorm:"uniqueIndex"` // A user provided name/digest for this data
}

Digest is a table of aliases.

type Digested

type Digested struct {
	DigestsCSV string `gorm:"digests_csv" json:"-"`
	// DataID  uint            `json:"-"`
	Digests []digest.Digest `gorm:"-"`
}

Digested can be added to a query type to enable.

func (*Digested) AfterFind

func (e *Digested) AfterFind(tx *gorm.DB) error

AfterFind will set the Digests field.

type Event

type Event struct {
	Base

	ManifestID uint
	Manifest   Manifest // Event belongs to Manifest
	// While a manifest may have different digests (different algorithms) this event is specific to this manifest.
	// The repository in this event may only have one (name) digest for this Manifest.
	ManifestDigest digest.Digest `gorm:"index"`

	BottleID     uint // Event belongs to Bottle (prejoining since the association is not allowed to change)
	Bottle       Bottle
	BottleDigest digest.Digest `gorm:"index"` // prejoin since it does not change

	Action       string // pull or push
	Repository   string
	Tag          string
	AuthRequired bool
	Bandwidth    uint64
	Timestamp    time.Time `gorm:"index"`
	Username     string    `gorm:"index"`
}

Event is used to record an actual download/upload event.

type EventProcessor

type EventProcessor struct{}

EventProcessor handles bottle processing.

func (*EventProcessor) PrimaryTable

func (p *EventProcessor) PrimaryTable() string

PrimaryTable returns primary table that this processor updates.

func (*EventProcessor) Process

func (p *EventProcessor) Process(con *gorm.DB, base Base) error

Process converts Event data to the DB model.

func (*EventProcessor) Version

func (p *EventProcessor) Version() uint

Version returns the processor version.

type Generation

type Generation []BottleRelative

Generation represents a generation of ancestors (e.g., parents, grandparents, children).

func FindChildren

func FindChildren(con *gorm.DB, digests []digest.Digest) (Generation, error)

FindChildren finds all children of the provides bottle digest (any number of bottles).

func FindGenerations

func FindGenerations(con *gorm.DB, bottleDigest digest.Digest, depth uint, finder NextGenerationFinder) ([]Generation, error)

FindGenerations is a generic function to find multiple generations.

func FindParents

func FindParents(con *gorm.DB, digests []digest.Digest) (Generation, error)

FindParents finds all parents of the provides bottle digest (any number of bottles).

func GetAncestors

func GetAncestors(con *gorm.DB, bottleDigest digest.Digest, depth uint) ([]Generation, error)

GetAncestors returns all ancestors Input: a bottle digest and depth (1 = parents, 2 = grandparents, etc.) Output: a double-slice where each outer slice index indicates the generation, i.e. 0 = parents, etc.

func GetDescendants

func GetDescendants(con *gorm.DB, bottleDigest digest.Digest, depth uint) ([]Generation, error)

GetDescendants returns all descendants Input: a bottle digest and depth (1 = children, 2 = grandchildren, etc.) Output: a double-slice where each outer slice index indicates the generation, i.e. 0 = children, etc.

func (Generation) FindRelativeIdx

func (g Generation) FindRelativeIdx(source Source) int

FindRelativeIdx finds the relative that matches the source in this generation.

func (Generation) GetDigests

func (g Generation) GetDigests() []digest.Digest

GetDigests returns all the bottle digests for this generation (the result may contain duplicate digests).

func (Generation) GetNonBottleParents

func (g Generation) GetNonBottleParents() []NonBottleRelative

GetNonBottleParents extracts the non-bottle URIs.

func (Generation) GetUniqueDigests

func (g Generation) GetUniqueDigests() []digest.Digest

GetUniqueDigests returns all the bottle digests for this generation (the result will not contain duplicate digests).

func (Generation) GetUnknownBottleParents

func (g Generation) GetUnknownBottleParents(knownParents Generation) []UnknownBottleRelative

GetUnknownBottleParents extracts the unknown bottle digests from a generation.

type GormLogger

type GormLogger interface {
	logger.Interface
	IgnoreRecordNotFoundError(bool) GormLogger
	SlowThreshold(time.Duration) GormLogger
}

GormLogger is an interface that also implements the gorm logger interface (with a few more functions).

func NewGormLogger

func NewGormLogger() GormLogger

NewGormLogger creates a GORM compatible logger that actually logs to the given logr.Logger.

type Label

type Label struct {
	Model
	BottleID uint

	Key          string  `gorm:"index"` // unique per bottle
	Value        string  `gorm:"index"`
	NumericValue float64 `gorm:"index" json:"-"` // We drop this is JSON marshalling to avoid issues with logging
}

Label is bottle labels.

func (*Label) BeforeSave

func (l *Label) BeforeSave(tx *gorm.DB) error

BeforeSave is called before the struct is saved to the DB to convert the value to a float if possible.

func (Label) GetLocation

func (l Label) GetLocation() string

GetLocation gets the index (key of the label).

type Layer

type Layer struct {
	Model
	ManifestID uint
	Location   uint

	Digest digest.Digest `gorm:"index"` // This is not tracked by the telemetry server so it is just a string (not a reference to a Digest record)

}

Layer is a manifest layer.

func (Layer) GetLocation

func (l Layer) GetLocation() uint

GetLocation gets the index (key of the annotation).

type Manifest

type Manifest struct {
	Base
	BottleID     uint
	Bottle       Bottle        // Manifest belongs to Bottle
	BottleDigest digest.Digest `gorm:"index"`
	Layers       []Layer       // Bottle has many layers
}

Manifest is the OCI manifest v2 and points to a Bottle.

type ManifestProcessor

type ManifestProcessor struct{}

ManifestProcessor handles bottle processing.

func (*ManifestProcessor) PrimaryTable

func (p *ManifestProcessor) PrimaryTable() string

PrimaryTable returns primary table that this processor updates.

func (*ManifestProcessor) Process

func (p *ManifestProcessor) Process(con *gorm.DB, base Base) error

Process converts Manifest data to the DB model.

func (*ManifestProcessor) Version

func (p *ManifestProcessor) Version() uint

Version returns the processor version.

type Metric

type Metric struct {
	BottleMemberLocated

	Name        string // unique per bottle
	Description string
	Value       float64
}

Metric is metrics on Bottles.

type Model

type Model struct {
	gorm.Model
}

Model is the base of all records.

func (Model) GetPrimaryKey

func (m Model) GetPrimaryKey() uint

GetPrimaryKey gets the primary key.

func (*Model) SetPrimaryKey

func (m *Model) SetPrimaryKey(pk uint)

SetPrimaryKey sets the primary key.

type NextGenerationFinder

type NextGenerationFinder func(con *gorm.DB, digests []digest.Digest) (Generation, error)

NextGenerationFinder is a function that finds the next generation from the given digests (either up or down the family tree).

type NonBottleRelative

type NonBottleRelative struct {
	URI string
}

NonBottleRelative represents a relative that is not a bottle but instead a URI of some other kind.

func (NonBottleRelative) MatchesSource

func (u NonBottleRelative) MatchesSource(source Source) bool

MatchesSource returns true if the source matches this relative.

type Part

type Part struct {
	BottleMemberLocated

	Name   string // unique per bottle
	Size   uint64
	Digest digest.Digest `gorm:"index"` // we do not guarantee this exists in the telemetry server so this is just a string, not a Digest and DigestID

	// LabelStr is stored in the DB but code is expected to use Labels directly.
	LabelsStr string            `json:"-"`
	Labels    map[string]string `gorm:"-"`
}

Part is a data part of a Bottle.

func (*Part) AfterFind

func (p *Part) AfterFind(tx *gorm.DB) error

AfterFind is called after a find() to convert the LabelStr to the Labels map.

func (*Part) BeforeSave

func (p *Part) BeforeSave(tx *gorm.DB) error

BeforeSave is called before the struct is saved to the DB to convert the Label map to the LabelStr.

type Processor

type Processor interface {
	PrimaryTable() string
	Process(con *gorm.DB, base Base) error
	Version() uint
}

Processor knows how to process it's type to conver the raw data to a DB model.

type PublicArtifact

type PublicArtifact struct {
	BottleMemberLocated

	Name      string
	MediaType string
	Path      string `gorm:"index"` // unique per bottle
	Data      Data   // PublicArtifact belongs to Data
	DataID    uint

	// Digest is the digest of the artifact.
	// We need to preserve the digest used by the bottle to reference this artifact.
	Digest digest.Digest // `gorm:"index"` we might want an index here
}

PublicArtifact is metadata about a PublicArtifact as specified in the Bottle.

func FindArtifact

func FindArtifact(con *gorm.DB, bottleDigest digest.Digest, artifactPath string) (*PublicArtifact, error)

FindArtifact returns the artifact by bottle and path.

type Relative

type Relative interface {
	BottleRelative | NonBottleRelative | UnknownBottleRelative
}

Relative is a type constraint for any relative of a bottle.

type Signature

type Signature struct {
	Base

	ManifestID uint
	Manifest   Manifest // Event belongs to Manifest
	// While a manifest may have different digests (different algorithms) this signature is specific to this manifest.
	// The repository in this signature may only have one (name) digest for this Manifest.
	ManifestDigest digest.Digest `gorm:"index"`

	BottleID     uint // Signature belongs to Bottle (prejoining since the association is not allowed to change)
	Bottle       Bottle
	BottleDigest digest.Digest `gorm:"index"` // prejoin since it does not change

	SignatureType        string                // currently dev.cosignproject.cosign/signature
	Signature            []byte                // raw signature payload data
	PublicKey            string                // PEM encoded public key associated with signature (Verify)
	PublicKeyFingerPrint digest.Digest         `gorm:"index"` // the digest of the public key
	Annotations          []SignatureAnnotation // extra data, such as verify api, userid, etc.

	// Trusted is used in the code but not saved in the database.
	Trusted func(TrustAnchor) bool `gorm:"-"` // true if the signature identity can be validated against a given trust anchor (fingerprint+id known)
}

Signature is used to record "signed-off" attributes for a bottle.

func (*Signature) AfterFind

func (s *Signature) AfterFind(tx *gorm.DB) error

AfterFind is called after a find() to add the Trusted func to the signature.

type SignatureAnnotation

type SignatureAnnotation struct {
	Model
	SignatureID uint

	Key   string // unique per bottle
	Value string
}

SignatureAnnotation is used to record extra data on a signature, such as verify api, userid, etc.

type SignatureProcessor

type SignatureProcessor struct{}

SignatureProcessor handles bottle processing.

func (*SignatureProcessor) PrimaryTable

func (p *SignatureProcessor) PrimaryTable() string

PrimaryTable returns primary table that this processor updates.

func (*SignatureProcessor) Process

func (p *SignatureProcessor) Process(con *gorm.DB, base Base) error

Process converts Signature data to the DB model.

func (*SignatureProcessor) Version

func (p *SignatureProcessor) Version() uint

Version returns the processor version.

type Source

type Source struct {
	BottleMemberLocated

	Name string `gorm:"index"`
	URI  string `gorm:"index"`

	// optional if the URL is a bottle
	BottleDigest digest.Digest `gorm:"index"` // we do not guarantee this exists in the telemetry server so this is just a string, not a DataID, DigestID, or BottleID

	// PartSelectors are used in the code but not saved in the database. They are derived from the URI query parameters
	PartSelectors selectors.LabelSelectorSet `gorm:"-"`
}

Source is a data source for the bottle.

func (*Source) AfterFind

func (s *Source) AfterFind(tx *gorm.DB) error

AfterFind is called after a find() to extract the part selectors to the LabelSelectorSet.

func (*Source) BeforeSave

func (s *Source) BeforeSave(tx *gorm.DB) error

BeforeSave is called before the struct is saved to the DB to convert the part selectors back to URI.

type TrustAnchor

type TrustAnchor interface {
	VerifyTrust() bool
}

TrustAnchor is a source which we can validate an signature owner's identity against.

type UnknownBottleRelative

type UnknownBottleRelative struct {
	Digest        digest.Digest
	PartSelectors selectors.LabelSelectorSet
}

UnknownBottleRelative represents a bottle that is not known to the telemetry server (so we know nothing more than its digest).

func (UnknownBottleRelative) MatchesSource

func (b UnknownBottleRelative) MatchesSource(source Source) bool

MatchesSource returns true if the source matches this relative.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL