target

package
v0.0.0-...-f54f16c Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2024 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package target encapsulates all the ways we will interact with deployment targets, starting with a client/target to control the hab supervisor locally

Index

Constants

View Source
const (
	// HabTimeoutInstallPackage is the timeout for InstallPackage
	// commands. Since package installs also install dependencies,
	// a given package installation can often take considerable
	// time.
	HabTimeoutInstallPackage = 1 * time.Hour
	// HabTimeoutIsInstalled is the timeout for
	// IsInstalled. IsInstalled runs hab pkg path which we expect
	// to be very fast typically.
	HabTimeoutIsInstalled = 1 * time.Minute
	// HabTimeoutDefault is the timeout for hab commands that
	// don't have other timeouts.
	HabTimeoutDefault = 5 * time.Minute
)
View Source
const HabitatInstallScriptURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"

Variables

This section is empty.

Functions

func SupportsCleanShutdown

func SupportsCleanShutdown(habSupP habpkg.HabPkg) bool

SupportsCleanShutdown returns true if the provided hab-sup version supports customized service shutdown behavior.

func SupportsSupHup

func SupportsSupHup(habSupP habpkg.HabPkg) bool

SupportsSupHup returns true if the provided hab-sup version supports restarting hab-sup via SIGHUP

Types

type Bootstrapper

type Bootstrapper interface {
	// InstallHabitat should install the `hab` binary, using the
	// version specified in the manifest.
	InstallHabitat(context.Context, manifest.ReleaseManifest, cli.BodyWriter) error
	// InstallDeploymentService installs the deployment-service
	// package specified in the manifest.
	InstallDeploymentService(context.Context, *dc.ConfigRequest, manifest.ReleaseManifest) error
	// SetupSupervisor should install our habitat supervisor
	// configuration into the systems init system. If this
	// function completes without error, the Habitat supervisor
	// should be available to load further Chef Automate services.
	SetupSupervisor(context.Context, *dc.ConfigRequest, manifest.ReleaseManifest, cli.FormatWriter) error
	// DeployDeploymentService configures and starts the
	// deployment-service. If this function exits without an
	// error, the deployment-service should be loaded via Habitat.
	DeployDeploymentService(ctx context.Context, cfg *dc.ConfigRequest, releaseManifest manifest.ReleaseManifest,
		bootstrapBundlePath string, writer cli.BodyWriter) error
	// SetHabitatEnvironment is a work-around for backwards
	// compatibility. It should set the PATH and HAB_SUP_BINARY
	// environment variables in the current process to ensure that
	// further Habitat interactions use the correct version of hab
	// and hab-sup. This should be called after SetupSupervisor.
	SetHabitatEnvironment(manifest.ReleaseManifest) error

	InstallAutomateBackendDeployment(ctx context.Context, cfg *dc.ConfigRequest, releaseManifest manifest.ReleaseManifest, saas bool) error
}

A bootstrapper is capable of getting the deployment-service configured and started

type DeployedService

type DeployedService struct {
	Pkg                 habpkg.HabPkg
	Binds               []string
	UpdateStrategy      string
	DesiredProcessState DesiredProcessState
}

DeployedService represents a Habitat supervised service

type DesiredProcessState

type DesiredProcessState string
const (
	ProcessStateUp      DesiredProcessState = "up"
	ProcessStateDown    DesiredProcessState = "down"
	ProcessStateUnknown DesiredProcessState = "unknown"
)

type HabCmd

type HabCmd interface {
	// InstallPackage installs an Installable habitat package
	// (a hartifact or a package from the Depot)
	InstallPackage(context.Context, habpkg.Installable, string) (string, error)
	// IsInstalled returns true if the specified package is
	// installed and false otherwise.  An error is returned when
	// the underlying habitat commands have failed.
	IsInstalled(context.Context, habpkg.VersionedPackage) (bool, error)
	// BinlinkPackage binlinks a binary in the given Habitat
	// package. An error is returned if the underlying hab command
	// failed.
	BinlinkPackage(context.Context, habpkg.VersionedPackage, string) (string, error)

	// LoadService loads the given habpkg.VersionedPackage as a service
	// with the provided options.
	LoadService(context.Context, habpkg.VersionedPackage, ...LoadOption) (string, error)
	// UnloadService unloads a given habpkg.VersionedPackage
	UnloadService(context.Context, habpkg.VersionedPackage) (string, error)
	// StartService starts an already-loaded service identified by
	// the given habpkg.VersionedPackage.
	StartService(context.Context, habpkg.VersionedPackage) (string, error)
	// StopService stops an already-loaded service identified by
	// the given habpkg.VersionedPackage.
	StopService(context.Context, habpkg.VersionedPackage) (string, error)

	// Terminate supervisor, does not block
	SupTerm(context.Context) error
}

A HabCmd runs the `hab` command-line tool with a standard set of options.

func NewHabCmd

func NewHabCmd(c command.Executor, offlineMode bool) HabCmd

NewHabCmd returns an habCmd that uses the given command.Executor. If offlineMode is true then any InstallPackage() calls will use Habitat's OFFLINE_INSTALL feature.

type HabSup

type HabSup interface {
	SupPkg() (habpkg.HabPkg, error)
	SupPid() (int, error)
	LauncherPid() (int, error)
	Hup(ctx context.Context) error
}

HabSup interface describes various operations on hab-sup and launcher

func LocalHabSup

func LocalHabSup(client habapi.HabServiceInfoAPIClient) HabSup

LocalHabSup is a HabSup implementation on a local target

type LoadOption

type LoadOption func([]string) []string

func BindMode

func BindMode(mode string) LoadOption

BindMode is a LoadOption that applies the passed binding mode to the service's load command line arguments.

func Binds

func Binds(binds []string) LoadOption

Binds is a LoadOption that applies the passed bind to the service's load command line arguments.

type LocalTarget

type LocalTarget struct {
	HabCmd
	Executor  command.Executor
	HabClient *habapi.Client

	HabBaseDir string
	HabBackoff time.Duration
	// contains filtered or unexported fields
}

LocalTarget struct

func NewLocalTarget

func NewLocalTarget(offlineMode bool) *LocalTarget

NewLocalTarget creates a new local target

func (*LocalTarget) CommandExecutor

func (t *LocalTarget) CommandExecutor() command.Executor

func (*LocalTarget) DeployDeploymentService

func (t *LocalTarget) DeployDeploymentService(ctx context.Context, config *dc.ConfigRequest, m manifest.ReleaseManifest, bootstrapBundlePath string, writer cli.BodyWriter) error

func (*LocalTarget) DeployedServices

func (t *LocalTarget) DeployedServices(ctx context.Context) (map[string]DeployedService, error)

DeployedServices returns a list of all the services the supervisor is supervising

func (*LocalTarget) DestroyData

func (t *LocalTarget) DestroyData() error

func (*LocalTarget) DestroyPkgCache

func (t *LocalTarget) DestroyPkgCache() error

func (*LocalTarget) DestroySupervisor

func (t *LocalTarget) DestroySupervisor() error

func (*LocalTarget) Disable

func (t *LocalTarget) Disable() error

func (*LocalTarget) EnsureDisabled

func (t *LocalTarget) EnsureDisabled() error

func (*LocalTarget) EnsureHabUser

func (t *LocalTarget) EnsureHabUser(writer cli.FormatWriter) error

EnsureHabUser ensures that the hab user and group exists.

If the hab user and hab group both exist, we return without modification (even if the hab group isn't the primary group of the hab user.

If the hab group exists but the hab user doesn't, we create the hab user and set the hab group as its primary group.

If neither the hab user or hab group exist, we create both the user and group.

An error is returned if user or group lookup fails, if a hab user exists without a corresponding hab group, or if the useradd command fails.

func (*LocalTarget) EnsureStopped

func (t *LocalTarget) EnsureStopped() error

func (*LocalTarget) GetAutomateUnitFile

func (t *LocalTarget) GetAutomateUnitFile() ([]byte, error)

func (*LocalTarget) GetSymlinkedHabSup

func (t *LocalTarget) GetSymlinkedHabSup() (habpkg.HabPkg, error)

func (*LocalTarget) GetUserToml

func (t *LocalTarget) GetUserToml(pkg habpkg.VersionedPackage) (string, error)

GetUserToml reads the user toml for the given package. If it does not exist, an empty string is returned

func (*LocalTarget) HabAPIClient

func (t *LocalTarget) HabAPIClient() *habapi.Client

func (*LocalTarget) HabCache

func (t *LocalTarget) HabCache() depot.HabCache

func (*LocalTarget) HabSup

func (t *LocalTarget) HabSup() HabSup

func (*LocalTarget) HabSupRestart

func (t *LocalTarget) HabSupRestart(ctx context.Context, sortedServiceList []string) (bool, error)

HabSupRestart restarts the Habitat Supervisor

Where possible, we restart hab-sup without restarting the entire process tree.

The list of services is used in the case of a full systemd restart. See the comment on SystemdRestart for details.

func (*LocalTarget) HabSupRestartRequired

func (t *LocalTarget) HabSupRestartRequired(desiredPkg habpkg.HabPkg) (bool, error)

func (*LocalTarget) IPs

func (t *LocalTarget) IPs() []net.IP

func (*LocalTarget) InstallAutomateBackendDeployment

func (t *LocalTarget) InstallAutomateBackendDeployment(ctx context.Context, c *dc.ConfigRequest, m manifest.ReleaseManifest, saas bool) error

func (*LocalTarget) InstallAutomateUnitFile

func (t *LocalTarget) InstallAutomateUnitFile(config *dc.ConfigRequest, habP habpkg.HabPkg, habSupP habpkg.HabPkg, habLauncherP habpkg.HabPkg) error

func (*LocalTarget) InstallDeploymentService

func (t *LocalTarget) InstallDeploymentService(ctx context.Context, c *dc.ConfigRequest, m manifest.ReleaseManifest) error

func (*LocalTarget) InstallHabitat

func (t *LocalTarget) InstallHabitat(ctx context.Context, m manifest.ReleaseManifest, writer cli.BodyWriter) error

func (*LocalTarget) InstallService

func (t *LocalTarget) InstallService(ctx context.Context, svc habpkg.Installable, channel string) error

InstallService installs an automate service. Returns an error if the install failed for any reason.

func (*LocalTarget) InstallSupPackages

func (t *LocalTarget) InstallSupPackages(ctx context.Context, releaseManifest manifest.ReleaseManifest, writer cli.BodyWriter) error

InstallSupPackages installs non-service Habitat packages included in product.meta core

func (*LocalTarget) IsBinlinked

func (t *LocalTarget) IsBinlinked(pkg habpkg.VersionedPackage, cmd string) (bool, error)

func (*LocalTarget) LoadDeploymentService

func (t *LocalTarget) LoadDeploymentService(ctx context.Context, svc habpkg.VersionedPackage) error

func (*LocalTarget) LoadService

func (t *LocalTarget) LoadService(ctx context.Context, svc habpkg.VersionedPackage, opts ...LoadOption) error

LoadService starts the package in the supervisor, either via load or start as appropriate for the startStyle

func (*LocalTarget) RemoveService

func (t *LocalTarget) RemoveService(ctx context.Context, svc habpkg.VersionedPackage) error

RemoveService removes the given service from this target. It returns an error if any portion of the removal fails.

func (*LocalTarget) RenderAutomateUnitFile

func (t *LocalTarget) RenderAutomateUnitFile(proxyConfig string, habP habpkg.HabPkg, habLauncherP habpkg.HabPkg) (string, error)

func (*LocalTarget) SetHabitatEnvironment

func (t *LocalTarget) SetHabitatEnvironment(m manifest.ReleaseManifest) error

Modify environment to ensure we are using a version of hab and hab-sup from the given manifest.

We do this for the backwards compatibility path.

func (*LocalTarget) SetUserToml

func (t *LocalTarget) SetUserToml(name, config string) error

InitServiceConfig lays down config for an automate service

func (*LocalTarget) SetupSupervisor

func (t *LocalTarget) SetupSupervisor(ctx context.Context, config *dc.ConfigRequest, m manifest.ReleaseManifest, writer cli.FormatWriter) error

func (*LocalTarget) StartService

func (t *LocalTarget) StartService(ctx context.Context, svc habpkg.VersionedPackage) error

StartService starts an already loaded service. Service startup is asynchronous.

func (*LocalTarget) Status

func (t *LocalTarget) Status(ctx context.Context, serviceNames []string) *api.ServiceStatus

Status verifies status of a service by hitting its health check endpoint

func (*LocalTarget) Stop

func (t *LocalTarget) Stop(ctx context.Context) error

Stop stops the A2 services

func (*LocalTarget) StopService

func (t *LocalTarget) StopService(ctx context.Context, svc habpkg.VersionedPackage) error

func (*LocalTarget) SymlinkHabSup

func (t *LocalTarget) SymlinkHabSup(habSupP habpkg.HabPkg) error

func (*LocalTarget) SystemdReload

func (t *LocalTarget) SystemdReload() error

SystemdReload calls systemctl daemon-reload on the target

func (*LocalTarget) SystemdReloadRequired

func (t *LocalTarget) SystemdReloadRequired() (bool, error)

SystemdReloadRequired returns true if a reload is required.

It looks like we should be able to call `systemctl show chef-chef-automate.service --property NeedDaemonReload` instead, but that seems to report 'no' even when status reports a reload is required.

func (*LocalTarget) SystemdRestart

func (t *LocalTarget) SystemdRestart(ctx context.Context, sortedServiceList []string) (bool, error)

SystemdRestart restarts the chef-automate systemd unit. It takes the running hab-sup package and a list of services so that it can manually shut down services before restarting systemd. hab-sup shuts down services with a TERM followed (after 8 seconds) by a KILL. We would like to avoid the KILL being sent to our data services. Shutting down services in their reverse startup order makes it more likely that data services will shut down quickly since all of their clients have been stopped.

func (*LocalTarget) SystemdRestartWithSupVersion

func (t *LocalTarget) SystemdRestartWithSupVersion(ctx context.Context, habSupP habpkg.HabPkg, sortedServiceList []string) (bool, error)

func (*LocalTarget) SystemdRunning

func (t *LocalTarget) SystemdRunning() (bool, error)

SystemdRunning returns true if PID 1 is systemd

func (*LocalTarget) UnloadService

func (t *LocalTarget) UnloadService(ctx context.Context, svc habpkg.VersionedPackage) error

UnloadService unloads the given service from this target. It returns an error if any portion of the removal fails. Packages are not deleted for unload

func (*LocalTarget) WriteAutomateUnitFile

func (t *LocalTarget) WriteAutomateUnitFile(content []byte) error

type MockTarget

type MockTarget struct {
	mock.Mock
}

MockTarget is an autogenerated mock type for the Target type

func (*MockTarget) BinlinkPackage

func (_m *MockTarget) BinlinkPackage(_a0 context.Context, _a1 habpkg.VersionedPackage, _a2 string) (string, error)

BinlinkPackage provides a mock function with given fields: _a0, _a1, _a2

func (*MockTarget) CommandExecutor

func (_m *MockTarget) CommandExecutor() command.Executor

CommandExecutor provides a mock function with given fields:

func (*MockTarget) DeployDeploymentService

func (_m *MockTarget) DeployDeploymentService(ctx context.Context, cfg *deployment.ConfigRequest, releaseManifest manifest.ReleaseManifest, bootstrapBundlePath string, writer cli.BodyWriter) error

DeployDeploymentService provides a mock function with given fields: ctx, cfg, releaseManifest, bootstrapBundlePath, writer

func (*MockTarget) DeployedServices

func (_m *MockTarget) DeployedServices(ctx context.Context) (map[string]DeployedService, error)

DeployedServices provides a mock function with given fields: ctx

func (*MockTarget) DestroyData

func (_m *MockTarget) DestroyData() error

DestroyData provides a mock function with given fields:

func (*MockTarget) DestroyPkgCache

func (_m *MockTarget) DestroyPkgCache() error

DestroyPkgCache provides a mock function with given fields:

func (*MockTarget) DestroySupervisor

func (_m *MockTarget) DestroySupervisor() error

DestroySupervisor provides a mock function with given fields:

func (*MockTarget) Disable

func (_m *MockTarget) Disable() error

Disable provides a mock function with given fields:

func (*MockTarget) EnsureDisabled

func (_m *MockTarget) EnsureDisabled() error

EnsureDisabled provides a mock function with given fields:

func (*MockTarget) EnsureStopped

func (_m *MockTarget) EnsureStopped() error

EnsureStopped provides a mock function with given fields:

func (*MockTarget) GetAutomateUnitFile

func (_m *MockTarget) GetAutomateUnitFile() ([]byte, error)

GetAutomateUnitFile provides a mock function with given fields:

func (*MockTarget) GetSymlinkedHabSup

func (_m *MockTarget) GetSymlinkedHabSup() (habpkg.HabPkg, error)

GetSymlinkedHabSup provides a mock function with given fields:

func (*MockTarget) GetUserToml

func (_m *MockTarget) GetUserToml(pkg habpkg.VersionedPackage) (string, error)

GetUserToml provides a mock function with given fields: pkg

func (*MockTarget) HabAPIClient

func (_m *MockTarget) HabAPIClient() *habapi.Client

HabAPIClient provides a mock function with given fields:

func (*MockTarget) HabCache

func (_m *MockTarget) HabCache() depot.HabCache

HabCache provides a mock function with given fields:

func (*MockTarget) HabSup

func (_m *MockTarget) HabSup() HabSup

HabSup provides a mock function with given fields:

func (*MockTarget) HabSupRestart

func (_m *MockTarget) HabSupRestart(_a0 context.Context, _a1 []string) (bool, error)

HabSupRestart provides a mock function with given fields: _a0, _a1

func (*MockTarget) HabSupRestartRequired

func (_m *MockTarget) HabSupRestartRequired(_a0 habpkg.HabPkg) (bool, error)

HabSupRestartRequired provides a mock function with given fields: _a0

func (*MockTarget) IPs

func (_m *MockTarget) IPs() []net.IP

IPs provides a mock function with given fields:

func (*MockTarget) InstallAutomateBackendDeployment

func (_m *MockTarget) InstallAutomateBackendDeployment(ctx context.Context, cfg *deployment.ConfigRequest, releaseManifest manifest.ReleaseManifest, saas bool) error

InstallAutomateBackendDeployment provides a mock function with given fields: ctx, cfg, releaseManifest,saas

func (*MockTarget) InstallDeploymentService

func (_m *MockTarget) InstallDeploymentService(_a0 context.Context, _a1 *deployment.ConfigRequest, _a2 manifest.ReleaseManifest) error

InstallDeploymentService provides a mock function with given fields: _a0, _a1, _a2

func (*MockTarget) InstallHabitat

func (_m *MockTarget) InstallHabitat(_a0 context.Context, _a1 manifest.ReleaseManifest, _a2 cli.BodyWriter) error

InstallHabitat provides a mock function with given fields: _a0, _a1, _a2

func (*MockTarget) InstallService

func (_m *MockTarget) InstallService(_a0 context.Context, _a1 habpkg.Installable, _a2 string) error

InstallService provides a mock function with given fields: _a0, _a1, _a2

func (*MockTarget) IsBinlinked

func (_m *MockTarget) IsBinlinked(_a0 habpkg.VersionedPackage, _a1 string) (bool, error)

IsBinlinked provides a mock function with given fields: _a0, _a1

func (*MockTarget) IsInstalled

func (_m *MockTarget) IsInstalled(_a0 context.Context, _a1 habpkg.VersionedPackage) (bool, error)

IsInstalled provides a mock function with given fields: _a0, _a1

func (*MockTarget) LoadDeploymentService

func (_m *MockTarget) LoadDeploymentService(_a0 context.Context, _a1 habpkg.VersionedPackage) error

LoadDeploymentService provides a mock function with given fields: _a0, _a1

func (*MockTarget) LoadService

func (_m *MockTarget) LoadService(_a0 context.Context, _a1 habpkg.VersionedPackage, _a2 ...LoadOption) error

LoadService provides a mock function with given fields: _a0, _a1, _a2

func (*MockTarget) RemoveService

func (_m *MockTarget) RemoveService(_a0 context.Context, _a1 habpkg.VersionedPackage) error

RemoveService provides a mock function with given fields: _a0, _a1

func (*MockTarget) RenderAutomateUnitFile

func (_m *MockTarget) RenderAutomateUnitFile(proxyConfig string, habP habpkg.HabPkg, habLauncherP habpkg.HabPkg) (string, error)

RenderAutomateUnitFile provides a mock function with given fields: proxyConfig, habP, habLauncherP

func (*MockTarget) SetHabitatEnvironment

func (_m *MockTarget) SetHabitatEnvironment(_a0 manifest.ReleaseManifest) error

SetHabitatEnvironment provides a mock function with given fields: _a0

func (*MockTarget) SetUserToml

func (_m *MockTarget) SetUserToml(name string, config string) error

SetUserToml provides a mock function with given fields: name, config

func (*MockTarget) SetupSupervisor

SetupSupervisor provides a mock function with given fields: _a0, _a1, _a2, _a3

func (*MockTarget) StartService

func (_m *MockTarget) StartService(_a0 context.Context, _a1 habpkg.VersionedPackage) error

StartService provides a mock function with given fields: _a0, _a1

func (*MockTarget) Status

func (_m *MockTarget) Status(ctx context.Context, serviceNames []string) *interservicedeployment.ServiceStatus

Status provides a mock function with given fields: ctx, serviceNames

func (*MockTarget) Stop

func (_m *MockTarget) Stop(ctx context.Context) error

Stop provides a mock function with given fields: ctx

func (*MockTarget) StopService

func (_m *MockTarget) StopService(_a0 context.Context, _a1 habpkg.VersionedPackage) error

StopService provides a mock function with given fields: _a0, _a1

func (*MockTarget) SymlinkHabSup

func (_m *MockTarget) SymlinkHabSup(habSupP habpkg.HabPkg) error

SymlinkHabSup provides a mock function with given fields: habSupP

func (*MockTarget) SystemdReload

func (_m *MockTarget) SystemdReload() error

SystemdReload provides a mock function with given fields:

func (*MockTarget) SystemdReloadRequired

func (_m *MockTarget) SystemdReloadRequired() (bool, error)

SystemdReloadRequired provides a mock function with given fields:

func (*MockTarget) SystemdRestart

func (_m *MockTarget) SystemdRestart(_a0 context.Context, _a1 []string) (bool, error)

SystemdRestart provides a mock function with given fields: _a0, _a1

func (*MockTarget) SystemdRunning

func (_m *MockTarget) SystemdRunning() (bool, error)

SystemdRunning provides a mock function with given fields:

func (*MockTarget) UnloadService

func (_m *MockTarget) UnloadService(_a0 context.Context, _a1 habpkg.VersionedPackage) error

UnloadService provides a mock function with given fields: _a0, _a1

func (*MockTarget) WriteAutomateUnitFile

func (_m *MockTarget) WriteAutomateUnitFile(_a0 []byte) error

WriteAutomateUnitFile provides a mock function with given fields: _a0

type ServiceManager

NOTE(ssd) 2018-10-08: This is mostly the HabCmd interface but without the output return since Target is currently responsible for logging.

type Target

type Target interface {
	Bootstrapper
	ServiceManager

	LoadDeploymentService(context.Context, habpkg.VersionedPackage) error

	DeployedServices(ctx context.Context) (map[string]DeployedService, error)
	Status(ctx context.Context, serviceNames []string) *api.ServiceStatus

	Stop(ctx context.Context) error
	EnsureStopped() error
	Disable() error
	EnsureDisabled() error

	DestroySupervisor() error
	DestroyData() error
	DestroyPkgCache() error

	SetUserToml(name, config string) error
	GetUserToml(pkg habpkg.VersionedPackage) (string, error)

	SymlinkHabSup(habSupP habpkg.HabPkg) error
	GetSymlinkedHabSup() (habpkg.HabPkg, error)

	RenderAutomateUnitFile(proxyConfig string, habP habpkg.HabPkg, habLauncherP habpkg.HabPkg) (string, error)
	GetAutomateUnitFile() ([]byte, error)
	WriteAutomateUnitFile([]byte) error

	SystemdReloadRequired() (bool, error)
	SystemdReload() error
	SystemdRestart(context.Context, []string) (bool, error)
	SystemdRunning() (bool, error)

	HabSupRestartRequired(habpkg.HabPkg) (bool, error)
	HabSupRestart(context.Context, []string) (bool, error)

	CommandExecutor() command.Executor
	HabAPIClient() *habapi.Client
	HabSup() HabSup

	// IPs() returns a list of IPs assigned to the target
	IPs() []net.IP

	HabCache() depot.HabCache
}

Target encapsulates all commands interacting with a2 stack

Jump to

Keyboard shortcuts

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