plugins

package
v0.0.0-...-c4e71b8 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNomadDeploymentNotImplemented = errors.New("nomad-deploy: real Nomad execution is not implemented yet — no deployment occurred")

ErrNomadDeploymentNotImplemented is returned by Deploy after validation and staging succeed. stageNomadJob builds a Nomad CLI container but never executes it, and runNomadCommand only checks for a local Nomad binary and logs — neither performs a real `nomad job run` or calls the Nomad API. A shipwright.Deployer's returned string is recorded by engine.Execute as the step's successful output, so fabricating a "nomad://..." reference here would let a workflow report a deployment that never happened. Deploy fails closed with this sentinel instead, until real execution is implemented.

Functions

This section is empty.

Types

type Capabilities

type Capabilities struct {
	Builder    shipwright.Builder
	Testers    []shipwright.Tester
	Artifactor shipwright.Artifactor
	Deployer   shipwright.Deployer
	Runner     shipwright.Runner
}

Capabilities bundles the Layer 1 capability interfaces (pkg/shipwright: Builder, Tester, Artifactor, Deployer, Runner) that are wired for the current run, exposed to plugins via PluginContext.GetCapabilities().

This replaces the retired PluginContext.GetPipeline() (WU10, tasks.md 10.1/10.2, design.md's composition-model migration) — the composition contract's extension point is now the five capability interfaces directly, never a bundled "Pipeline" type. Capabilities itself carries no bundle identity or name (design.md D-F): it is a plain data carrier exposing whichever capabilities are wired for this run, not a named capability-set preset. Any field MAY be nil — a run need not wire every capability, and Deployer/Runner have no concrete implementation yet (pkg/shipwright.DeployConfig/RunConfig are empty at this change). Testers is a slice because multiple independent Tester implementations MAY compose over the same input (design.md D-F's orthogonality win — unit test, lint, vulnerability scan are three separate Testers).

type MockConfiguration

type MockConfiguration struct {
	AllFunc              func() map[string]any
	EnvironmentFunc      func() string
	GetBoolFunc          func(key string) bool
	GetConfigSummaryFunc func() string
	GetDurationFunc      func(key string) time.Duration
	GetFloatFunc         func(key string) float64
	GetFunc              func(key string) any
	GetIntFunc           func(key string) int
	GetStringFunc        func(key string) string
	HasFunc              func(key string) bool
	LoadFunc             func() error
	LoadWithDefaultsFunc func(defaults map[string]any) error
	LoggingFunc          func() interfaces.LoggingConfig
	PipelineFunc         func() interfaces.PipelineConfig
	RegistryFunc         func() interfaces.RegistryConfig
	SecurityFunc         func() interfaces.SecurityConfig
	SetFunc              func(key string, value any)
	ValidateFunc         func() error
}

MockConfiguration implements interfaces.Configuration interface for testing.

func NewMockConfiguration

func NewMockConfiguration() *MockConfiguration

NewMockConfiguration creates a new mock configuration.

func (*MockConfiguration) All

func (m *MockConfiguration) All() map[string]any

All implements interfaces.Configuration interface.

func (*MockConfiguration) Environment

func (m *MockConfiguration) Environment() string

Environment implements interfaces.Configuration interface.

func (*MockConfiguration) Get

func (m *MockConfiguration) Get(key string) any

Get implements interfaces.Configuration interface.

func (*MockConfiguration) GetBool

func (m *MockConfiguration) GetBool(key string) bool

GetBool implements interfaces.Configuration interface.

func (*MockConfiguration) GetConfigSummary

func (m *MockConfiguration) GetConfigSummary() string

GetConfigSummary implements interfaces.Configuration interface.

func (*MockConfiguration) GetDuration

func (m *MockConfiguration) GetDuration(key string) time.Duration

GetDuration implements interfaces.Configuration interface.

func (*MockConfiguration) GetFloat

func (m *MockConfiguration) GetFloat(key string) float64

GetFloat implements interfaces.Configuration interface.

func (*MockConfiguration) GetInt

func (m *MockConfiguration) GetInt(key string) int

GetInt implements interfaces.Configuration interface.

func (*MockConfiguration) GetString

func (m *MockConfiguration) GetString(key string) string

GetString implements interfaces.Configuration interface.

func (*MockConfiguration) Has

func (m *MockConfiguration) Has(key string) bool

Has implements interfaces.Configuration interface.

func (*MockConfiguration) Load

func (m *MockConfiguration) Load() error

Load implements interfaces.Configuration interface.

func (*MockConfiguration) LoadWithDefaults

func (m *MockConfiguration) LoadWithDefaults(defaults map[string]any) error

LoadWithDefaults implements interfaces.Configuration interface.

func (*MockConfiguration) Logging

Logging implements interfaces.Configuration interface.

func (*MockConfiguration) Pipeline

Pipeline implements interfaces.Configuration interface.

func (*MockConfiguration) Registry

Registry implements interfaces.Configuration interface.

func (*MockConfiguration) Security

Security implements interfaces.Configuration interface.

func (*MockConfiguration) Set

func (m *MockConfiguration) Set(key string, value any)

Set implements interfaces.Configuration interface.

func (*MockConfiguration) Validate

func (m *MockConfiguration) Validate() error

Validate implements interfaces.Configuration interface.

type MockHookManager

type MockHookManager struct {
	ExecuteHooksFunc func(ctx context.Context, stepName string, hookType interfaces.HookType) error
	GetHooksFunc     func(stepName string, hookType interfaces.HookType) []interfaces.HookFunc
	RegisterHookFunc func(stepName string, hookType interfaces.HookType, hook interfaces.HookFunc) error
	RemoveHookFunc   func(stepName string, hookType interfaces.HookType, hook interfaces.HookFunc) error
}

MockHookManager implements interfaces.HookManager interface for testing.

func NewMockHookManager

func NewMockHookManager() *MockHookManager

NewMockHookManager creates a new mock hook manager.

func (*MockHookManager) ExecuteHooks

func (m *MockHookManager) ExecuteHooks(ctx context.Context, stepName string, hookType interfaces.HookType) error

ExecuteHooks implements interfaces.HookManager interface.

func (*MockHookManager) GetHooks

func (m *MockHookManager) GetHooks(stepName string, hookType interfaces.HookType) []interfaces.HookFunc

GetHooks implements interfaces.HookManager interface.

func (*MockHookManager) RegisterHook

func (m *MockHookManager) RegisterHook(stepName string, hookType interfaces.HookType, hook interfaces.HookFunc) error

RegisterHook implements interfaces.HookManager interface.

func (*MockHookManager) RemoveHook

func (m *MockHookManager) RemoveHook(stepName string, hookType interfaces.HookType, hook interfaces.HookFunc) error

RemoveHook implements interfaces.HookManager interface.

type MockLogger

type MockLogger struct {
	DebugFunc      func(msg string, fields ...any)
	ErrorFunc      func(msg string, fields ...any)
	FatalFunc      func(msg string, fields ...any)
	InfoFunc       func(msg string, fields ...any)
	WarnFunc       func(msg string, fields ...any)
	WithFieldFunc  func(key string, value any) interfaces.Logger
	WithFieldsFunc func(fields map[string]any) interfaces.Logger
}

MockLogger implements interfaces.Logger interface for testing.

func NewMockLogger

func NewMockLogger() *MockLogger

NewMockLogger creates a new mock logger.

func (*MockLogger) Debug

func (m *MockLogger) Debug(msg string, fields ...any)

Debug implements interfaces.Logger interface.

func (*MockLogger) Error

func (m *MockLogger) Error(msg string, fields ...any)

Error implements interfaces.Logger interface.

func (*MockLogger) Fatal

func (m *MockLogger) Fatal(msg string, fields ...any)

Fatal implements interfaces.Logger interface.

func (*MockLogger) Info

func (m *MockLogger) Info(msg string, fields ...any)

Info implements interfaces.Logger interface.

func (*MockLogger) Warn

func (m *MockLogger) Warn(msg string, fields ...any)

Warn implements interfaces.Logger interface.

func (*MockLogger) WithField

func (m *MockLogger) WithField(key string, value any) interfaces.Logger

WithField implements interfaces.Logger interface.

func (*MockLogger) WithFields

func (m *MockLogger) WithFields(fields map[string]any) interfaces.Logger

WithFields implements interfaces.Logger interface.

type MockPlugin

type MockPlugin struct {
	CleanupFunc    func(ctx context.Context) error
	InitializeFunc func(ctx context.Context, pluginCtx PluginContext) error
	NameFunc       func() string
	VersionFunc    func() string
}

MockPlugin implements Plugin interface for testing.

func NewMockPlugin

func NewMockPlugin() *MockPlugin

NewMockPlugin creates a new mock plugin.

func (*MockPlugin) Cleanup

func (m *MockPlugin) Cleanup(ctx context.Context) error

Cleanup implements Plugin interface.

func (*MockPlugin) Initialize

func (m *MockPlugin) Initialize(ctx context.Context, pluginCtx PluginContext) error

Initialize implements Plugin interface.

func (*MockPlugin) Name

func (m *MockPlugin) Name() string

Name implements Plugin interface.

func (*MockPlugin) Version

func (m *MockPlugin) Version() string

Version implements Plugin interface.

type MockPluginContext

type MockPluginContext struct {
	GetConfigurationFunc    func() interfaces.Configuration
	GetDaggerClientFunc     func() (*dagger.Client, error)
	GetHookManagerFunc      func() interfaces.HookManager
	GetLoggerFunc           func() interfaces.Logger
	GetConfigFunc           func() pipelines.Config
	GetCapabilitiesFunc     func() Capabilities
	GetProviderRegistryFunc func() *providers.Registry
	GetStepRegistryFunc     func() interfaces.StepRegistry
}

MockPluginContext implements PluginContext interface for testing.

func NewMockPluginContext

func NewMockPluginContext() *MockPluginContext

NewMockPluginContext creates a new mock plugin context.

func (*MockPluginContext) GetCapabilities

func (m *MockPluginContext) GetCapabilities() Capabilities

GetCapabilities implements PluginContext interface.

func (*MockPluginContext) GetConfig

func (m *MockPluginContext) GetConfig() pipelines.Config

GetConfig implements PluginContext interface.

func (*MockPluginContext) GetConfiguration

func (m *MockPluginContext) GetConfiguration() interfaces.Configuration

GetConfiguration implements PluginContext interface.

func (*MockPluginContext) GetDaggerClient

func (m *MockPluginContext) GetDaggerClient() (*dagger.Client, error)

GetDaggerClient implements PluginContext interface.

func (*MockPluginContext) GetHookManager

func (m *MockPluginContext) GetHookManager() interfaces.HookManager

GetHookManager implements PluginContext interface.

func (*MockPluginContext) GetLogger

func (m *MockPluginContext) GetLogger() interfaces.Logger

GetLogger implements PluginContext interface.

func (*MockPluginContext) GetProviderRegistry

func (m *MockPluginContext) GetProviderRegistry() *providers.Registry

GetProviderRegistry implements PluginContext interface.

func (*MockPluginContext) GetStepRegistry

func (m *MockPluginContext) GetStepRegistry() interfaces.StepRegistry

GetStepRegistry implements PluginContext interface.

type MockPluginLoader

type MockPluginLoader struct {
	ListBuiltinsFunc    func() []string
	LoadBuiltinFunc     func(ctx context.Context, name string) (Plugin, error)
	LoadFromConfigFunc  func(ctx context.Context, config map[string]interface{}) (Plugin, error)
	LoadFromFileFunc    func(ctx context.Context, filePath string) (Plugin, error)
	RegisterBuiltinFunc func(name string, factory func() Plugin)
}

MockPluginLoader implements PluginLoader interface for testing.

func NewMockPluginLoader

func NewMockPluginLoader() *MockPluginLoader

NewMockPluginLoader creates a new mock plugin loader.

func (*MockPluginLoader) ListBuiltins

func (m *MockPluginLoader) ListBuiltins() []string

ListBuiltins implements PluginLoader interface.

func (*MockPluginLoader) LoadBuiltin

func (m *MockPluginLoader) LoadBuiltin(ctx context.Context, name string) (Plugin, error)

LoadBuiltin implements PluginLoader interface.

func (*MockPluginLoader) LoadFromConfig

func (m *MockPluginLoader) LoadFromConfig(ctx context.Context, config map[string]interface{}) (Plugin, error)

LoadFromConfig implements PluginLoader interface.

func (*MockPluginLoader) LoadFromFile

func (m *MockPluginLoader) LoadFromFile(ctx context.Context, filePath string) (Plugin, error)

LoadFromFile implements PluginLoader interface.

func (*MockPluginLoader) RegisterBuiltin

func (m *MockPluginLoader) RegisterBuiltin(name string, factory func() Plugin)

RegisterBuiltin implements PluginLoader interface.

type MockStepRegistry

type MockStepRegistry struct {
	ExecuteStepFunc       func(ctx context.Context, stepName string) error
	GetExecutionOrderFunc func() ([]string, error)
	GetStepConfigFunc     func(stepName string) (interfaces.StepConfig, error)
	GetStepHandlerFunc    func(stepName string) (interfaces.StepHandler, error)
	ListStepsFunc         func() []string
	RegisterStepFunc      func(stepName string, handler interfaces.StepHandler) error
	ValidateStepFunc      func(stepName string) error
}

MockStepRegistry implements interfaces.StepRegistry interface for testing.

func NewMockStepRegistry

func NewMockStepRegistry() *MockStepRegistry

NewMockStepRegistry creates a new mock step registry.

func (*MockStepRegistry) ExecuteStep

func (m *MockStepRegistry) ExecuteStep(ctx context.Context, stepName string) error

ExecuteStep implements interfaces.StepRegistry interface.

func (*MockStepRegistry) GetExecutionOrder

func (m *MockStepRegistry) GetExecutionOrder() ([]string, error)

GetExecutionOrder implements interfaces.StepRegistry interface.

func (*MockStepRegistry) GetStepConfig

func (m *MockStepRegistry) GetStepConfig(stepName string) (interfaces.StepConfig, error)

GetStepConfig implements interfaces.StepRegistry interface.

func (*MockStepRegistry) GetStepHandler

func (m *MockStepRegistry) GetStepHandler(stepName string) (interfaces.StepHandler, error)

GetStepHandler implements interfaces.StepRegistry interface.

func (*MockStepRegistry) ListSteps

func (m *MockStepRegistry) ListSteps() []string

ListSteps implements interfaces.StepRegistry interface.

func (*MockStepRegistry) RegisterStep

func (m *MockStepRegistry) RegisterStep(stepName string, handler interfaces.StepHandler) error

RegisterStep implements interfaces.StepRegistry interface.

func (*MockStepRegistry) ValidateStep

func (m *MockStepRegistry) ValidateStep(stepName string) error

ValidateStep implements interfaces.StepRegistry interface.

type NomadDeployPlugin

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

NomadDeployPlugin adds Nomad deployment capability to Shipwright workflows.

It is BOTH the Plugin (lifecycle: Name/Version/Initialize/Cleanup) and the shipwright.Deployer (Layer 1 capability) it contributes. Initialize registers a Deployer factory into the run's *providers.Registry through PluginContext.GetProviderRegistry(); the factory returns a per-step copy of this plugin bound to that step's typed `with` values, which is what internal/workflow/engine.dispatchDeploy actually calls.

Why this replaced the previous hook/step registration: the earlier implementation registered a "push"-after hook on interfaces.HookManager and a "deploy-nomad" handler on interfaces.StepRegistry. Both were only ever executed by the preset-driven step flow that this change's preset-deletion work unit removed — engine.Execute consults neither. Those registrations therefore made the plugin LOOK wired while guaranteeing its deploy logic could never run. They are removed rather than kept as compatibility dead weight.

func (*NomadDeployPlugin) Cleanup

func (p *NomadDeployPlugin) Cleanup(ctx context.Context) error

Cleanup cleans up the plugin.

func (*NomadDeployPlugin) Deploy

func (p *NomadDeployPlugin) Deploy(
	ctx context.Context,
	artifactRef string,
	environment string,
	creds *dagger.Secret,
) (string, error)

Deploy implements shipwright.Deployer: it deploys an already-published artifact reference into a named environment and returns the deployment reference the engine records as this step's output.

func (*NomadDeployPlugin) Initialize

func (p *NomadDeployPlugin) Initialize(ctx context.Context, pluginCtx PluginContext) error

Initialize reads plugin configuration and registers this plugin's shipwright.Deployer into the run's provider registry.

A nil registry (no workflow wired for this run) is not an error: the plugin simply contributes nothing, matching PluginContext.GetProviderRegistry's documented contract.

func (*NomadDeployPlugin) Name

func (p *NomadDeployPlugin) Name() string

Name returns the plugin name.

func (*NomadDeployPlugin) Version

func (p *NomadDeployPlugin) Version() string

Version returns the plugin version.

type Plugin

type Plugin interface {
	// Name returns the unique name of the plugin.
	Name() string

	// Version returns the version of the plugin.
	Version() string

	// Initialize is called when the plugin is loaded.
	// The plugin can use this to register hooks, add steps, etc.
	Initialize(ctx context.Context, pluginCtx PluginContext) error

	// Cleanup is called when the plugin is unloaded or the pipeline completes.
	Cleanup(ctx context.Context) error
}

Plugin defines the interface that all plugins must implement.

Extension point (single, current): during Initialize a plugin registers a Layer 1 capability implementation (pkg/shipwright: Builder/Tester/Artifactor/Deployer/Runner) into the *providers.Registry reachable through PluginContext.GetProviderRegistry(), using WU7's own Register*/Resolve* primitives. That registry is the exact one internal/workflow/engine.Execute resolves each manifest step against, so a plugin-contributed provider is indistinguishable from an in-repo one at resolution time (internal/workflow/providers/register.go's RegisterDefaults is the same registration shape).

The older extension points — HookManager.RegisterHook and StepRegistry.RegisterStep — are NOT the workflow extension mechanism. Their only executor was the legacy preset-driven step flow deleted in this change's preset-deletion work unit; engine.Execute never consults either. A plugin registering into them today registers behavior nothing will ever run. The accessors remain on PluginContext for the pre-existing non-workflow consumers under examples/, and are deliberately unused by the in-repo plugin.

func NewNomadDeployPlugin

func NewNomadDeployPlugin() Plugin

NewNomadDeployPlugin creates a new Nomad deploy plugin.

type PluginContext

type PluginContext interface {
	// GetDaggerClient returns the Dagger client for container operations.
	GetDaggerClient() (*dagger.Client, error)

	// GetConfiguration returns the pipeline configuration.
	GetConfiguration() interfaces.Configuration

	// GetHookManager returns the hook manager for registering hooks.
	GetHookManager() interfaces.HookManager

	// GetStepRegistry returns the step registry for adding custom steps.
	//
	// Deprecated as an extension point: nothing in the workflow engine
	// executes StepRegistry steps — see the Plugin doc comment.
	GetStepRegistry() interfaces.StepRegistry

	// GetCapabilities returns the Layer 1 capability bundle wired for the
	// current run (replaces the retired GetPipeline()).
	GetCapabilities() Capabilities

	// GetProviderRegistry returns the typed provider registry
	// (internal/workflow/providers, WU7) the current workflow run resolves
	// every manifest step against, so a plugin can CONTRIBUTE a capability
	// implementation into it during Initialize:
	//
	//	reg := pluginCtx.GetProviderRegistry()
	//	reg.RegisterDeployer(providers.Ref{Name: "nomad-deploy", Version: "1"},
	//	    providers.WithSchema{...}, func(v providers.Values) shipwright.Deployer { ... })
	//
	// This is deliberately the registry ITSELF rather than one new Plugin
	// method per capability: WU7 already ships, and already tests, five typed
	// Register*/Resolve* pairs, so the bridge adds one accessor instead of a
	// parallel five-method surface that would have to be kept in sync.
	//
	// Returns nil when the current run wired no registry (for example a unit
	// test constructing a bare PluginContext). A plugin MUST treat nil as
	// "no provider extension point available" and skip registration rather
	// than panic.
	//
	// SECURITY (design.md D-I): the returned registry only ever receives
	// factories from Go code compiled into this binary. Plugins reaching this
	// accessor are loaded through PluginRegistry.LoadBuiltinPlugins, which
	// resolves ONLY compile-time-registered builtin factories and never
	// touches PluginLoader.LoadFromFile/LoadFromConfig — the two paths that
	// can reach plugin.Open. A plugin-contributed provider is therefore still
	// "already compiled, self-registered" exactly as D-I requires.
	GetProviderRegistry() *providers.Registry

	// GetConfig returns the pipeline-specific configuration (replaces the
	// retired GetPipelineConfig()).
	GetConfig() pipelines.Config

	// GetLogger returns the logger instance.
	GetLogger() interfaces.Logger
}

PluginContext provides access to pipeline resources for plugins. This allows plugins to interact with the pipeline without modifying the core library.

func NewPluginContext

func NewPluginContext(
	daggerClient *dagger.Client,
	configuration interfaces.Configuration,
	hookManager interfaces.HookManager,
	stepRegistry interfaces.StepRegistry,
	capabilities Capabilities,
	providerRegistry *providers.Registry,
	pipelineConfig pipelines.Config,
	logger interfaces.Logger,
) PluginContext

NewPluginContext creates a new plugin context.

providerRegistry is the workflow run's own *providers.Registry — the extension point plugins register capability implementations into (see PluginContext.GetProviderRegistry). Pass nil when no workflow registry is wired; plugins treat that as "no provider extension point available".

type PluginLoader

type PluginLoader interface {
	// LoadFromFile loads a plugin from a Go file.
	LoadFromFile(ctx context.Context, filePath string) (Plugin, error)

	// LoadFromConfig loads a plugin from configuration.
	LoadFromConfig(ctx context.Context, config map[string]interface{}) (Plugin, error)

	// LoadBuiltin loads a built-in plugin by name.
	LoadBuiltin(ctx context.Context, name string) (Plugin, error)

	// RegisterBuiltin registers a built-in plugin factory.
	RegisterBuiltin(name string, factory func() Plugin)

	// ListBuiltins returns the names of every compile-time-registered
	// builtin plugin factory, in unspecified order. This is what lets
	// PluginRegistry.LoadBuiltinPlugins load the full builtin set WITHOUT
	// going through configuration — the config path is the only one that can
	// reach plugin.Open.
	ListBuiltins() []string
}

PluginLoader loads plugins from various sources (files, config, etc.).

func NewLoader

func NewLoader() PluginLoader

NewLoader creates a new plugin loader.

type PluginRegistry

type PluginRegistry interface {
	// RegisterPlugin registers a plugin with the registry.
	RegisterPlugin(plugin Plugin) error

	// LoadPlugin loads a plugin by name from configuration or file.
	LoadPlugin(ctx context.Context, name string, pluginCtx PluginContext) error

	// LoadPluginsFromConfig loads all plugins specified in configuration.
	//
	// SECURITY: this path can reach PluginLoader.LoadFromConfig, whose
	// `type: file` branch calls plugin.Open on a config-supplied path
	// (arbitrary in-process native code). It is NOT used by the --workflow
	// entrypoint — see LoadBuiltinPlugins — and must stay reserved for
	// operator-initiated, out-of-band plugin installation (the security
	// skill's "prefer LoadBuiltin over LoadFromFile" rule).
	LoadPluginsFromConfig(ctx context.Context, pluginCtx PluginContext) error

	// LoadBuiltinPlugins registers and initializes every plugin the
	// PluginLoader has a compile-time builtin factory for
	// (PluginLoader.RegisterBuiltin, called from the DI container's
	// registerPluginComponents). This is the ONLY plugin load path the
	// --workflow CLI entrypoint uses.
	//
	// It never consults configuration and never calls LoadFromConfig or
	// LoadFromFile, so plugin.Open is unreachable from it — the fail-closed
	// counterpart to design.md D-I's "providers resolve only to already
	// compiled, self-registered implementations".
	//
	// Already-registered plugins are skipped, so calling it more than once in
	// a process is safe.
	LoadBuiltinPlugins(ctx context.Context, pluginCtx PluginContext) error

	// GetPlugin retrieves a registered plugin by name.
	GetPlugin(name string) (Plugin, error)

	// ListPlugins returns all registered plugin names.
	ListPlugins() []string

	// InitializeAll initializes all registered plugins.
	InitializeAll(ctx context.Context, pluginCtx PluginContext) error

	// CleanupAll cleans up all registered plugins.
	CleanupAll(ctx context.Context) error
}

PluginRegistry manages plugin registration and lifecycle.

func NewRegistry

func NewRegistry(loader PluginLoader) PluginRegistry

NewRegistry creates a new plugin registry.

Jump to

Keyboard shortcuts

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