util

package
v0.0.0-...-44c146a Latest Latest
Warning

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

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

Documentation

Overview

NOTE: Added to skip creating shadow manifests for localSecret struct +kubebuilder:skip

Index

Constants

View Source
const (

	// EventReasonValidationFailed is used when VolumeReplicationGroup validation fails
	EventReasonValidationFailed = "FailedValidation"

	// EventReasonPVCListFailed is used when VRG fails to get the list of PVCs
	EventReasonPVCListFailed = "PVCListFailed"

	// EventReasonVRCreateFailed is used when VRG fails to create VolRep resource
	EventReasonVRCreateFailed = "VRCreateFailed"

	// EventReasonVRCreateFailed is used when VRG fails to update VolRep resource
	EventReasonVRUpdateFailed = "VRUpdateFailed"

	// EventReasonProtectPVCFailed is used when VRG fails to protect PVC
	EventReasonProtectPVCFailed = "ProtectPVCFailed"

	// EventReasonUploadFailed is used when VRG fails to upload PV cluster data
	EventReasonUploadFailed = "UploadFailed"

	// EventReasonVrgUploadFailed is used when VRG fails to upload VRG object
	EventReasonVrgUploadFailed = "VrgUploadFailed"

	// EventReasonPrimarySuccess is an event generated when VRG is successfully
	// processed as Primary.
	EventReasonPrimarySuccess = "PrimaryVRGProcessSuccess"

	// EventReasonSecondarySuccess is an event generated when VRG is successfully
	// processed as Primary.
	EventReasonSecondarySuccess = "SecondaryVRGProcessSuccess"

	// EventReasonSecondarySuccess is an event generated when VRG is successfully
	// processed as Primary.
	EventReasonDeleteSuccess = "VRGDeleteSuccess"

	// EventReasonDeploying is generated when DRPC begins to deploy ramen manged cluster
	// component(s) and the application
	EventReasonDeploying = "DRPCDeploying"

	// EventReasonDeployFail is an event generated when DRPC fails to do
	// a successful initial deployment of the application and ramen managed
	// cluster component(s)
	EventReasonDeployFail = "DRPCDeployFailed"

	// EventReasonDeploySuccess is an event generated when DRPC successfully
	// deploys ramen and the application in the managed cluster initially
	EventReasonDeploySuccess = "DRPCDeploySuccess"

	// EventReasonFailingOver is an event generated when DRPC starts the failover
	// process
	EventReasonFailingOver = "DRPCFailingOver"

	// EventReasonFailoverSuccess is an evenet generated when DRPC does a successful
	// failover
	EventReasonFailoverSuccess = "DRPCFailoverSuccess"

	// EventReasonRelocating is an event generated when DRPC starts relocating
	EventReasonRelocating = "DRPCRelocating"

	// EventReasonRelocationSuccess is an event generated when DRPC successfully
	// relocates an application along with ramen managed cluster component(s)
	EventReasonRelocationSuccess = "DRPCRelocationSuccess"

	// EventReasonSwitchFailed is generated when DRPC fails to switch the cluster
	// where the app is placed
	EventReasonSwitchFailed = "DRPCClusterSwitchFailed"
)
View Source
const (
	OCMBackupLabelKey   string = "cluster.open-cluster-management.io/backup"
	OCMBackupLabelValue string = "ramen"
)
View Source
const (
	DrClusterManifestWorkName = "ramen-dr-cluster"

	// ManifestWorkNameFormat is a formated a string used to generate the manifest name
	// The format is name-namespace-type-mw where:
	// - name is the DRPC name
	// - namespace is the DRPC namespace
	// - type is "vrg"
	ManifestWorkNameFormat             string = "%s-%s-%s-mw"
	ManifestWorkNameFormatClusterScope string = "%s-%s-mw"

	// ManifestWork Types
	MWTypeVRG   string = "vrg"
	MWTypeNS    string = "ns"
	MWTypeNF    string = "nf"
	MWTypeMMode string = "mmode"
)
View Source
const (
	CreatedByLabelKey          = "app.kubernetes.io/created-by"
	CreatedByLabelValueVolSync = "volsync"

	PodVolumePVCClaimIndexName    string = "spec.volumes.persistentVolumeClaim.claimName"
	VolumeAttachmentToPVIndexName string = "spec.source.persistentVolumeName"
)
View Source
const (

	//nolint:lll
	// See: https://github.com/stolostron/rhacm-docs/blob/2.4_stage/governance/custom_template.adoc#special-annotation-for-reprocessing
	PolicyTriggerAnnotation = "policy.open-cluster-management.io/trigger-update"

	// Finalizer on the secret
	SecretPolicyFinalizer string = "drpolicies.ramendr.openshift.io/policy-protection"
)
View Source
const (
	LastEventValidDuration = 10
)

This implementation of events infrastructure is mainly based on the way events is handled in the ocs-operator. https://github.com/openshift/ocs-operator/blob/master/controllers/util/events.go

View Source
const (
	MModesLabel = "ramendr.openshift.io/maintenancemodes"
)

Variables

This section is empty.

Functions

func AddAnnotation

func AddAnnotation(obj client.Object, key, value string) bool

func AddFinalizer

func AddFinalizer(obj client.Object, finalizer string) bool

func AddLabel

func AddLabel(obj client.Object, key, value string) bool

func AddOwnerReference

func AddOwnerReference(obj, owner metav1.Object, scheme *runtime.Scheme) (bool, error)

func BuildManagedClusterViewName

func BuildManagedClusterViewName(resourceName, resourceNamespace, resource string) string

outputs a string for use in creating a ManagedClusterView name example: when looking for a vrg with name 'demo' in the namespace 'ramen', input: ("demo", "ramen", "vrg") this will give output "demo-ramen-vrg-mcv"

func ClusterScopedResourceNameFromMCVName

func ClusterScopedResourceNameFromMCVName(mcvName string) string

func ConditionAppend

func ConditionAppend(
	object metav1.Object,
	conditions *[]metav1.Condition,
	conditionType string,
	status metav1.ConditionStatus,
	reason,
	message string,
)

func ConditionUpdate

func ConditionUpdate(
	object metav1.Object,
	condition *metav1.Condition,
	status metav1.ConditionStatus,
	reason,
	message string,
)

func DRPolicyClusterNames

func DRPolicyClusterNames(drpolicy *rmn.DRPolicy) []string

func DRPolicyClusterNamesAsASet

func DRPolicyClusterNamesAsASet(drpolicy *rmn.DRPolicy) sets.String

func DRPolicyS3Profiles

func DRPolicyS3Profiles(drpolicy *rmn.DRPolicy, drclusters []rmn.DRCluster) sets.String

func DeletePVC

func DeletePVC(ctx context.Context,
	k8sClient client.Client,
	pvcName, namespace string,
	log logr.Logger,
) error

func DrpolicyRegionNames

func DrpolicyRegionNames(drpolicy *rmn.DRPolicy, drClusters []rmn.DRCluster) []string

func DrpolicyRegionNamesAsASet

func DrpolicyRegionNamesAsASet(drpolicy *rmn.DRPolicy, drClusters []rmn.DRCluster) sets.String

func DrpolicyValidated

func DrpolicyValidated(drpolicy *rmn.DRPolicy) error

func ExtractMModeFromManifestWork

func ExtractMModeFromManifestWork(mw *ocmworkv1.ManifestWork) (*rmn.MaintenanceMode, error)

func ExtractVRGFromManifestWork

func ExtractVRGFromManifestWork(mw *ocmworkv1.ManifestWork) (*rmn.VolumeReplicationGroup, error)

func GeneratePolicyName

func GeneratePolicyName(name string, maxLen int) string

GeneratePolicyName generates a policy name by combining the word "vs-secret-" with the name. However, if the length of the passed-in name is less than or equal to the 'maxLen', the passed-in name is returned as-is.

If the passed-in name and the namespace length exceeds 'maxLen', a unique hash of the passed-in name is computed using MD5 prepended to it "vs-secret-". If this combined name still exceeds 'maxLen', it is trimmed to fit within the limit by removing characters from the end of the hash up to maxLen.

Parameters:

potentialPolicyName: The preferred name of the policy.
namespace: The namespace associated with the policy.
maxLen: The maximum length of the generated name

Returns:

"vs-secret" + the generated name, which is either the passed-in name or a modified version that fits
  within the allowed length.

func GeneratePolicyResourceNames

func GeneratePolicyResourceNames(
	secret string,
) (policyName, plBindingName, plRuleName, configPolicyName string)

func GenericStatusConditionSet

func GenericStatusConditionSet(
	object client.Object,
	conditions *[]metav1.Condition,
	conditionType string,
	status metav1.ConditionStatus,
	reason, message string,
	log logr.Logger,
) bool

func GetAllDRPolicies

func GetAllDRPolicies(ctx context.Context, client client.Reader) (rmn.DRPolicyList, error)

func GetRawExtension

func GetRawExtension(
	manifests []ocmworkv1.Manifest,
	gvk schema.GroupVersionKind,
) (*runtime.RawExtension, error)

func GetSecondsFromSchedulingInterval

func GetSecondsFromSchedulingInterval(drpolicy *rmn.DRPolicy) (float64, error)

func HasLabel

func HasLabel(obj client.Object, key string) bool

func HasLabelWithValue

func HasLabelWithValue(obj client.Object, key string, value string) bool

func IndexFieldsForVSHandler

func IndexFieldsForVSHandler(ctx context.Context, fieldIndexer client.FieldIndexer) error

VSHandler will either look at VolumeAttachments or pods to determine if a PVC is mounted To do this, it requires an index on pods and volumeattachments to keep track of persistent volume claims mounted

func IsManifestInAppliedState

func IsManifestInAppliedState(mw *ocmworkv1.ManifestWork) bool

func IsPVAttachedToNode

func IsPVAttachedToNode(ctx context.Context,
	k8sClient client.Client,
	log logr.Logger,
	pvc *corev1.PersistentVolumeClaim,
) (bool, error)

For CSI drivers that support it, volume attachments will be created for the PV to indicate which node they are attached to. If a volume attachment exists, then we know the PV may not be ready to have a final replication sync performed (I/Os may still not be completely written out). This is a best-effort, as some CSI drivers may not support volume attachments (CSI driver Spec.AttachRequired: false) in this case, we won't find a volumeattachment and will just assume the PV is not in use anymore.

func IsPVCInUseByPod

func IsPVCInUseByPod(ctx context.Context,
	k8sClient client.Client,
	log logr.Logger,
	pvcNamespacedName types.NamespacedName,
	inUsePodMustBeReady bool,
) (bool, error)

IsPVCInUseByPod determines if there are any pod resources that reference the pvcName in the current pvcNamespace and returns true if found. Further if inUsePodMustBeReady is true, returns true only if the pod is in Ready state. TODO: Should we trust the cached list here, or fetch it from the API server?

func ListPVCsByPVCSelector

func ListPVCsByPVCSelector(
	ctx context.Context,
	k8sClient client.Client,
	logger logr.Logger,
	pvcLabelSelector metav1.LabelSelector,
	namespaces []string,
	volSyncDisabled bool,
) (*corev1.PersistentVolumeClaimList, error)

func ManifestWorkName

func ManifestWorkName(name, namespace, mwType string) string

func MapCopy

func MapCopy[M ~map[K]V, K, V comparable](src M, dst *M) bool

Copies src's key-value pairs into dst. Or, if dst is nil, assigns src to dst. Returns whether dst changes.

func MapCopyF

func MapCopyF[M ~map[K]V, K, V comparable](src M, dstGet func() M, dstSet func(M)) bool

func MapDelete

func MapDelete[M ~map[K]V, K, V comparable](src M, dst *M) bool

Deletes any key-value pairs from dst that are in src. Returns whether dst changes.

func MapDeleteF

func MapDeleteF[M ~map[K]V, K, V comparable](src M, dstGet func() M, dstSet func(M)) bool

func MapDoF

func MapDoF[M ~map[K]V, K, V comparable, R any](src M, dstGet func() M, dstSet func(M), mapDo func(M, *M) R) R

func MergeConditions

func MergeConditions(
	conditionSet func(*[]metav1.Condition, metav1.Condition),
	conditions *[]metav1.Condition,
	ignoreReasons []string,
	subConditions ...*metav1.Condition,
)

MergeConditions merges VRG conditions of the same type to generate a single condition for the Type

func Namespace

func Namespace(name string) *corev1.Namespace

func ObjectLabelsDelete

func ObjectLabelsDelete(object metav1.Object, labels map[string]string) bool

func ObjectLabelsDo

func ObjectLabelsDo[T any](object metav1.Object, labels map[string]string,
	do func(map[string]string, func() map[string]string, func(map[string]string)) T,
) T

func ObjectLabelsSet

func ObjectLabelsSet(object metav1.Object, labels map[string]string) bool

func ObjectMetaEmbedded

func ObjectMetaEmbedded(objectMeta *metav1.ObjectMeta) metav1.ObjectMeta

func ObjectOwnerSet

func ObjectOwnerSet(object, owner metav1.Object) bool

func ObjectOwnerUnsetIfSet

func ObjectOwnerUnsetIfSet(object, owner metav1.Object) bool

func ObjectsMap

func ObjectsMap[
	ObjectType any,
	ClientObject interface {
		*ObjectType
		client.Object
	},
](
	objects ...ObjectType,
) map[client.ObjectKey]ObjectType

func OptionalEqual

func OptionalEqual(a, b string) bool

OptionalEqual returns True if optional field values are equal, or one of them is unset.

func OwnerNamespaceNameAndName

func OwnerNamespaceNameAndName(labels Labels) (string, string, bool)

func OwnerNamespacedName

func OwnerNamespacedName(owner metav1.Object) types.NamespacedName

func OwnsAcrossNamespaces

func OwnsAcrossNamespaces(
	builder *builder.Builder,
	scheme *runtime.Scheme,
	object client.Object,
	opts ...builder.WatchesOption,
) *builder.Builder

func ReportIfNotPresent

func ReportIfNotPresent(recorder *EventReporter, instance runtime.Object,
	eventType, eventReason, msg string,
)

ReportIfNotPresent will report event if lastReportedEvent is not the same in last 10 minutes TODO: The duration 10 minutes can be changed to some other value if necessary

func ResourceIsDeleted

func ResourceIsDeleted(obj client.Object) bool

Return true if resource was marked for deletion.

func UpdateStringMap

func UpdateStringMap(dst *map[string]string, src map[string]string)

UpdateStringMap copies all key/value pairs in src adding them to map referenced by the dst pointer. When a key in src is already present in dst, the value in dst will be overwritten by the value associated with the key in src. The dst map is created if needed.

Types

type Comparison

type Comparison int
const (
	Different Comparison = iota
	Same
	Absent
)

func MapInsertOnlyAll

func MapInsertOnlyAll[M ~map[K]V, K, V comparable](src M, dst *M) Comparison

Copies src's key-value pairs into dst only if src's keys are all absent from dst. Or, if dst is nil, assigns src to dst. Returns state of src's key-value pairs in dst before any changes.

func MapInsertOnlyAllF

func MapInsertOnlyAllF[M ~map[K]V, K, V comparable](src M, dstGet func() M, dstSet func(M)) Comparison

func ObjectLabelInsertOnlyAll

func ObjectLabelInsertOnlyAll(object metav1.Object, labels map[string]string) Comparison

func ObjectOwnerSetIfNotAlready

func ObjectOwnerSetIfNotAlready(object, owner metav1.Object) Comparison

type CreateOrDeleteOrResourceVersionUpdatePredicate

type CreateOrDeleteOrResourceVersionUpdatePredicate struct{}

func (CreateOrDeleteOrResourceVersionUpdatePredicate) Create

func (CreateOrDeleteOrResourceVersionUpdatePredicate) Delete

func (CreateOrDeleteOrResourceVersionUpdatePredicate) Generic

func (CreateOrDeleteOrResourceVersionUpdatePredicate) Update

type CreateOrResourceVersionUpdatePredicate

type CreateOrResourceVersionUpdatePredicate struct{}

func (CreateOrResourceVersionUpdatePredicate) Create

func (CreateOrResourceVersionUpdatePredicate) Delete

func (CreateOrResourceVersionUpdatePredicate) Generic

func (CreateOrResourceVersionUpdatePredicate) Update

type EventReporter

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

EventReporter is custom events reporter type which allows user to limit the events

func NewEventReporter

func NewEventReporter(recorder record.EventRecorder) *EventReporter

NewEventReporter returns EventReporter object

type Labels

type Labels map[string]string

func OwnerLabels

func OwnerLabels(owner metav1.Object) Labels

type MWUtil

type MWUtil struct {
	client.Client
	APIReader       client.Reader
	Ctx             context.Context
	Log             logr.Logger
	InstName        string
	TargetNamespace string
}

func (*MWUtil) BuildManifestWorkName

func (mwu *MWUtil) BuildManifestWorkName(mwType string) string

func (*MWUtil) CreateOrUpdateDrClusterManifestWork

func (mwu *MWUtil) CreateOrUpdateDrClusterManifestWork(
	clusterName string,
	objectsToAppend []interface{}, annotations map[string]string,
) error

func (*MWUtil) CreateOrUpdateMModeManifestWork

func (mwu *MWUtil) CreateOrUpdateMModeManifestWork(
	name, cluster string,
	mMode rmn.MaintenanceMode, annotations map[string]string,
) error

MaintenanceMode ManifestWork creation

func (*MWUtil) CreateOrUpdateNFManifestWork

func (mwu *MWUtil) CreateOrUpdateNFManifestWork(
	name, namespace, homeCluster string,
	nf csiaddonsv1alpha1.NetworkFence, annotations map[string]string,
) error

NetworkFence MW creation

func (*MWUtil) CreateOrUpdateNamespaceManifest

func (mwu *MWUtil) CreateOrUpdateNamespaceManifest(
	name string, namespaceName string, managedClusterNamespace string,
	annotations map[string]string,
) error

func (*MWUtil) CreateOrUpdateVRGManifestWork

func (mwu *MWUtil) CreateOrUpdateVRGManifestWork(
	name, namespace, homeCluster string,
	vrg rmn.VolumeReplicationGroup, annotations map[string]string,
) error

func (*MWUtil) DeleteManifestWork

func (mwu *MWUtil) DeleteManifestWork(mwName, mwNamespace string) error

func (*MWUtil) DeleteManifestWorksForCluster

func (mwu *MWUtil) DeleteManifestWorksForCluster(clusterName string) error

func (*MWUtil) FindManifestWork

func (mwu *MWUtil) FindManifestWork(mwName, managedCluster string) (*ocmworkv1.ManifestWork, error)

func (*MWUtil) FindManifestWorkByType

func (mwu *MWUtil) FindManifestWorkByType(mwType, managedCluster string) (*ocmworkv1.ManifestWork, error)

func (*MWUtil) GenerateManifest

func (mwu *MWUtil) GenerateManifest(obj interface{}) (*ocmworkv1.Manifest, error)

func (*MWUtil) GetDrClusterManifestWork

func (mwu *MWUtil) GetDrClusterManifestWork(clusterName string) (*ocmworkv1.ManifestWork, error)

func (*MWUtil) GetVRGManifestWorkCount

func (mwu *MWUtil) GetVRGManifestWorkCount(drClusters []string) int

func (*MWUtil) ListMModeManifests

func (mwu *MWUtil) ListMModeManifests(cluster string) (*ocmworkv1.ManifestWorkList, error)

type ManagedClusterViewGetter

type ManagedClusterViewGetter interface {
	GetVRGFromManagedCluster(
		resourceName, resourceNamespace, managedCluster string,
		annotations map[string]string) (*rmn.VolumeReplicationGroup, error)

	GetNFFromManagedCluster(
		resourceName, resourceNamespace, managedCluster string,
		annotations map[string]string) (*csiaddonsv1alpha1.NetworkFence, error)

	GetMModeFromManagedCluster(
		resourceName, managedCluster string,
		annotations map[string]string) (*rmn.MaintenanceMode, error)

	ListMModesMCVs(managedCluster string) (*viewv1beta1.ManagedClusterViewList, error)

	GetResource(mcv *viewv1beta1.ManagedClusterView, resource interface{}) error

	DeleteManagedClusterView(clusterName, mcvName string, logger logr.Logger) error

	GetNamespaceFromManagedCluster(resourceName, resourceNamespace, managedCluster string,
		annotations map[string]string) (*corev1.Namespace, error)

	DeleteVRGManagedClusterView(resourceName, resourceNamespace, clusterName, resourceType string) error

	DeleteNamespaceManagedClusterView(resourceName, resourceNamespace, clusterName, resourceType string) error

	DeleteNFManagedClusterView(resourceName, resourceNamespace, clusterName, resourceType string) error
}

begin MCV code

type ManagedClusterViewGetterImpl

type ManagedClusterViewGetterImpl struct {
	client.Client
	APIReader client.Reader
}

func (ManagedClusterViewGetterImpl) DeleteManagedClusterView

func (m ManagedClusterViewGetterImpl) DeleteManagedClusterView(clusterName, mcvName string, logger logr.Logger) error

func (ManagedClusterViewGetterImpl) DeleteNFManagedClusterView

func (m ManagedClusterViewGetterImpl) DeleteNFManagedClusterView(
	resourceName, resourceNamespace, clusterName, resourceType string,
) error

func (ManagedClusterViewGetterImpl) DeleteNamespaceManagedClusterView

func (m ManagedClusterViewGetterImpl) DeleteNamespaceManagedClusterView(
	resourceName, resourceNamespace, clusterName, resourceType string,
) error

func (ManagedClusterViewGetterImpl) DeleteVRGManagedClusterView

func (m ManagedClusterViewGetterImpl) DeleteVRGManagedClusterView(
	resourceName, resourceNamespace, clusterName, resourceType string,
) error

func (ManagedClusterViewGetterImpl) GetMModeFromManagedCluster

func (m ManagedClusterViewGetterImpl) GetMModeFromManagedCluster(resourceName, managedCluster string,
	annotations map[string]string,
) (*rmn.MaintenanceMode, error)

func (ManagedClusterViewGetterImpl) GetNFFromManagedCluster

func (m ManagedClusterViewGetterImpl) GetNFFromManagedCluster(resourceName, resourceNamespace, managedCluster string,
	annotations map[string]string,
) (*csiaddonsv1alpha1.NetworkFence, error)

func (ManagedClusterViewGetterImpl) GetNamespaceFromManagedCluster

func (m ManagedClusterViewGetterImpl) GetNamespaceFromManagedCluster(
	resourceName, managedCluster, namespaceString string, annotations map[string]string,
) (*corev1.Namespace, error)

func (ManagedClusterViewGetterImpl) GetResource

func (m ManagedClusterViewGetterImpl) GetResource(mcv *viewv1beta1.ManagedClusterView, resource interface{}) error

func (ManagedClusterViewGetterImpl) GetVRGFromManagedCluster

func (m ManagedClusterViewGetterImpl) GetVRGFromManagedCluster(resourceName, resourceNamespace, managedCluster string,
	annotations map[string]string,
) (*rmn.VolumeReplicationGroup, error)

func (ManagedClusterViewGetterImpl) ListMModesMCVs

type ResourceUpdater

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

func NewResourceUpdater

func NewResourceUpdater(obj client.Object) *ResourceUpdater

func (*ResourceUpdater) AddFinalizer

func (u *ResourceUpdater) AddFinalizer(finalizerName string) *ResourceUpdater

func (*ResourceUpdater) AddLabel

func (u *ResourceUpdater) AddLabel(key, value string) *ResourceUpdater

func (*ResourceUpdater) AddOwner

func (u *ResourceUpdater) AddOwner(owner metav1.Object, scheme *runtime.Scheme) *ResourceUpdater

func (*ResourceUpdater) RemoveFinalizer

func (u *ResourceUpdater) RemoveFinalizer(finalizerName string) *ResourceUpdater

func (*ResourceUpdater) Update

func (u *ResourceUpdater) Update(ctx context.Context, client client.Client) error

type ResourceVersionUpdatePredicate

type ResourceVersionUpdatePredicate struct{}

func (ResourceVersionUpdatePredicate) Create

func (ResourceVersionUpdatePredicate) Delete

func (ResourceVersionUpdatePredicate) Generic

func (ResourceVersionUpdatePredicate) Update

type SecretsUtil

type SecretsUtil struct {
	client.Client
	APIReader client.Reader
	Ctx       context.Context
	Log       logr.Logger
}

func (*SecretsUtil) AddSecretToCluster

func (sutil *SecretsUtil) AddSecretToCluster(secretName, clusterName, namespace, targetns string) error

func (*SecretsUtil) RemoveSecretFromCluster

func (sutil *SecretsUtil) RemoveSecretFromCluster(secretName, clusterName, namespace string) error

Jump to

Keyboard shortcuts

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