k8s

package
v0.0.0-...-50d8424 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// MaxBodySize limits the size of request bodies to 1MB to prevent DoS
	MaxBodySize = 1048576
)

Variables

View Source
var (
	ErrNamespaceMismatch = errors.New("namespace mismatch")
	ErrNamespaceRequired = errors.New("namespace required")
	ErrForbidden         = errors.New("forbidden")
	ErrAdminRequired     = errors.New("admin required")
)

Functions

This section is empty.

Types

type ClusterService

type ClusterService interface {
	GetClient(r *http.Request) (kubernetes.Interface, error)
	GetDynamicClient(r *http.Request) (dynamic.Interface, error)
	GetMetricsClient(r *http.Request) *metricsv.Clientset
	GetRESTConfig(r *http.Request) (*rest.Config, error)
}

ClusterService defines the interface for cluster operations

type ClusterStatsRepository

type ClusterStatsRepository interface {
	GetNodeCount(ctx context.Context) (int, error)
	GetNamespaceCount(ctx context.Context) (int, error)
	GetPodCount(ctx context.Context) (int, error)
	GetDeploymentCount(ctx context.Context) (int, error)
	GetServiceCount(ctx context.Context) (int, error)
	GetIngressCount(ctx context.Context) (int, error)
	GetPVCCount(ctx context.Context) (int, error)
	GetPVCount(ctx context.Context) (int, error)
}

ClusterStatsRepository defines the interface for fetching cluster statistics

type ClusterStatsService

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

ClusterStatsService provides business logic for cluster statistics

func NewClusterStatsService

func NewClusterStatsService(repo ClusterStatsRepository) *ClusterStatsService

NewClusterStatsService creates a new ClusterStatsService

func (*ClusterStatsService) GetClusterStats

func (s *ClusterStatsService) GetClusterStats(ctx context.Context) (models.ClusterStats, error)

GetClusterStats fetches cluster statistics

type CronJobRepository

type CronJobRepository interface {
	GetCronJob(ctx context.Context, namespace, name string) (*batchv1.CronJob, error)
	CreateJob(ctx context.Context, namespace string, job *batchv1.Job) (*batchv1.Job, error)
}

CronJobRepository defines the interface for CronJob operations

type CronJobService

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

CronJobService provides business logic for CronJob operations

func NewCronJobService

func NewCronJobService(repo CronJobRepository) *CronJobService

NewCronJobService creates a new CronJobService

func (*CronJobService) TriggerCronJob

func (s *CronJobService) TriggerCronJob(ctx context.Context, namespace, name string) (string, error)

TriggerCronJob manually triggers a CronJob by creating an immediate Job

type DeleteResourceRequest

type DeleteResourceRequest struct {
	Kind      string // Resource kind (e.g., "Pod", "Deployment")
	Name      string // Resource name
	Namespace string // Namespace (required for namespaced resources)
	Force     bool   // If true, force deletion with 0 grace period
}

DeleteResourceRequest represents parameters for deleting a Kubernetes resource.

type DeploymentRepository

type DeploymentRepository interface {
	GetScale(ctx context.Context, namespace, name string) (*autoscalingv1.Scale, error)
	UpdateScale(ctx context.Context, namespace, name string, scale *autoscalingv1.Scale) (*autoscalingv1.Scale, error)
	GetDeployment(ctx context.Context, namespace, name string) (*appsv1.Deployment, error)
	UpdateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) (*appsv1.Deployment, error)
}

DeploymentRepository defines the interface for Deployment operations

type DeploymentService

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

DeploymentService provides business logic for Deployment operations

func NewDeploymentService

func NewDeploymentService(repo DeploymentRepository) *DeploymentService

NewDeploymentService creates a new DeploymentService

func (*DeploymentService) RolloutDeployment

func (s *DeploymentService) RolloutDeployment(ctx context.Context, namespace, name string) error

RolloutDeployment triggers a rollout/restart of a deployment by updating an annotation

func (*DeploymentService) ScaleDeployment

func (s *DeploymentService) ScaleDeployment(ctx context.Context, namespace, name string, delta int) (int32, error)

ScaleDeployment scales a deployment by a given delta

type Factory

type Factory interface {
	CreateResourceService(dynamicClient dynamic.Interface, client kubernetes.Interface) *ResourceService
	CreateImportService(dynamicClient dynamic.Interface, client kubernetes.Interface) *ImportService
	CreateNamespaceService(client kubernetes.Interface) *NamespaceService
	CreateClusterStatsService(client kubernetes.Interface) *ClusterStatsService
	CreateDeploymentService(client kubernetes.Interface) *DeploymentService
	CreateCronJobService(client kubernetes.Interface) *CronJobService
	CreateWatchService() *WatchService
}

Factory defines the service factory contract to allow test doubles.

type GVRResolver

type GVRResolver interface {
	ResolveGVR(ctx context.Context, kind, apiVersion string, namespacedParam string) (schema.GroupVersionResource, models.ResourceMeta, error)
}

GVRResolver resolves GroupVersionResource from kind and apiVersion

type GetResourceRequest

type GetResourceRequest struct {
	Kind          string // Resource kind (e.g., "Pod", "Deployment")
	Name          string // Resource name
	Namespace     string // Namespace (defaults to "default" for namespaced resources)
	AllNamespaces bool   // If true, ignore namespace filter
	Group         string // API group (optional)
	Version       string // API version (optional)
	Resource      string // Resource name (optional)
	Namespaced    bool   // Whether the resource is namespaced
}

GetResourceRequest represents parameters for retrieving a Kubernetes resource.

type ImportResourceRequest

type ImportResourceRequest struct {
	YAMLContent []byte
}

ImportResourceRequest represents parameters for importing resources

type ImportResourceResponse

type ImportResourceResponse struct {
	Status  string   `json:"status"`
	Count   int      `json:"count"`
	Applied []string `json:"resources"`
}

ImportResourceResponse represents the result of an import operation

type ImportService

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

ImportService provides business logic for importing Kubernetes resources

func NewImportService

func NewImportService(resourceRepo ResourceRepository, gvrResolver GVRResolver, k8sClient kubernetes.Interface) *ImportService

NewImportService creates a new ImportService

func (*ImportService) ImportResources

ImportResources parses multi-document YAML and applies resources

type K8sClusterStatsRepository

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

K8sClusterStatsRepository implements ClusterStatsRepository

func NewK8sClusterStatsRepository

func NewK8sClusterStatsRepository(client kubernetes.Interface) *K8sClusterStatsRepository

NewK8sClusterStatsRepository creates a new K8sClusterStatsRepository

func (*K8sClusterStatsRepository) GetDeploymentCount

func (r *K8sClusterStatsRepository) GetDeploymentCount(ctx context.Context) (int, error)

GetDeploymentCount returns the number of deployments

func (*K8sClusterStatsRepository) GetIngressCount

func (r *K8sClusterStatsRepository) GetIngressCount(ctx context.Context) (int, error)

GetIngressCount returns the number of ingresses

func (*K8sClusterStatsRepository) GetNamespaceCount

func (r *K8sClusterStatsRepository) GetNamespaceCount(ctx context.Context) (int, error)

GetNamespaceCount returns the number of namespaces

func (*K8sClusterStatsRepository) GetNodeCount

func (r *K8sClusterStatsRepository) GetNodeCount(ctx context.Context) (int, error)

GetNodeCount returns the number of worker nodes (excluding control plane nodes)

func (*K8sClusterStatsRepository) GetPVCCount

func (r *K8sClusterStatsRepository) GetPVCCount(ctx context.Context) (int, error)

GetPVCCount returns the number of PVCs

func (*K8sClusterStatsRepository) GetPVCount

func (r *K8sClusterStatsRepository) GetPVCount(ctx context.Context) (int, error)

GetPVCount returns the number of PVs

func (*K8sClusterStatsRepository) GetPodCount

func (r *K8sClusterStatsRepository) GetPodCount(ctx context.Context) (int, error)

GetPodCount returns the number of pods

func (*K8sClusterStatsRepository) GetServiceCount

func (r *K8sClusterStatsRepository) GetServiceCount(ctx context.Context) (int, error)

GetServiceCount returns the number of services

type K8sCronJobRepository

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

K8sCronJobRepository implements CronJobRepository

func NewK8sCronJobRepository

func NewK8sCronJobRepository(client kubernetes.Interface) *K8sCronJobRepository

NewK8sCronJobRepository creates a new K8sCronJobRepository

func (*K8sCronJobRepository) CreateJob

func (r *K8sCronJobRepository) CreateJob(ctx context.Context, namespace string, job *batchv1.Job) (*batchv1.Job, error)

CreateJob creates a Job

func (*K8sCronJobRepository) GetCronJob

func (r *K8sCronJobRepository) GetCronJob(ctx context.Context, namespace, name string) (*batchv1.CronJob, error)

GetCronJob gets a CronJob

type K8sDeploymentRepository

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

K8sDeploymentRepository implements DeploymentRepository

func NewK8sDeploymentRepository

func NewK8sDeploymentRepository(client kubernetes.Interface) *K8sDeploymentRepository

NewK8sDeploymentRepository creates a new K8sDeploymentRepository

func (*K8sDeploymentRepository) GetDeployment

func (r *K8sDeploymentRepository) GetDeployment(ctx context.Context, namespace, name string) (*appsv1.Deployment, error)

GetDeployment gets a deployment by name

func (*K8sDeploymentRepository) GetScale

func (r *K8sDeploymentRepository) GetScale(ctx context.Context, namespace, name string) (*autoscalingv1.Scale, error)

GetScale gets the scale of a deployment

func (*K8sDeploymentRepository) UpdateDeployment

func (r *K8sDeploymentRepository) UpdateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment) (*appsv1.Deployment, error)

UpdateDeployment updates a deployment

func (*K8sDeploymentRepository) UpdateScale

func (r *K8sDeploymentRepository) UpdateScale(ctx context.Context, namespace, name string, scale *autoscalingv1.Scale) (*autoscalingv1.Scale, error)

UpdateScale updates the scale of a deployment

type K8sGVRResolver

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

K8sGVRResolver implements GVRResolver

func NewK8sGVRResolver

func NewK8sGVRResolver() *K8sGVRResolver

NewK8sGVRResolver creates a new K8sGVRResolver

func NewK8sGVRResolverWithDiscovery

func NewK8sGVRResolverWithDiscovery(discoveryClient interface{}) *K8sGVRResolver

NewK8sGVRResolverWithDiscovery creates a new K8sGVRResolver with discovery client

func (*K8sGVRResolver) ResolveGVR

func (r *K8sGVRResolver) ResolveGVR(ctx context.Context, kind, apiVersion string, namespacedParam string) (schema.GroupVersionResource, models.ResourceMeta, error)

ResolveGVR resolves GVR from kind and apiVersion

type K8sNamespaceRepository

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

K8sNamespaceRepository implements NamespaceRepository

func NewK8sNamespaceRepository

func NewK8sNamespaceRepository(client kubernetes.Interface) *K8sNamespaceRepository

NewK8sNamespaceRepository creates a new K8sNamespaceRepository

func (*K8sNamespaceRepository) List

List fetches all namespaces

type K8sResourceRepository

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

K8sResourceRepository implements ResourceRepository using dynamic client

func NewK8sResourceRepository

func NewK8sResourceRepository(client dynamic.Interface) *K8sResourceRepository

NewK8sResourceRepository creates a new K8sResourceRepository

func (*K8sResourceRepository) Create

Create creates a new resource

func (*K8sResourceRepository) Delete

func (r *K8sResourceRepository) Delete(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool, options metav1.DeleteOptions) error

Delete deletes a resource

func (*K8sResourceRepository) Get

func (r *K8sResourceRepository) Get(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool) (*unstructured.Unstructured, error)

Get retrieves a resource

func (*K8sResourceRepository) Patch

func (r *K8sResourceRepository) Patch(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool, patchData []byte, patchType types.PatchType, options metav1.PatchOptions) (*unstructured.Unstructured, error)

Patch applies a patch to a resource

type ListResourcesRequest

type ListResourcesRequest struct {
	Kind          string
	Namespace     string
	AllNamespaces bool
	LabelSelector string
	Client        kubernetes.Interface
	MetricsClient metricsv.Interface
}

ListResourcesRequest represents parameters for listing resources

type NamespaceRepository

type NamespaceRepository interface {
	List(ctx context.Context) ([]corev1.Namespace, error)
}

NamespaceRepository defines the interface for fetching Kubernetes namespaces

type NamespaceService

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

NamespaceService provides business logic for namespace operations

func NewNamespaceService

func NewNamespaceService(repo NamespaceRepository) *NamespaceService

NewNamespaceService creates a new NamespaceService

func (*NamespaceService) GetNamespaces

func (s *NamespaceService) GetNamespaces(ctx context.Context) ([]models.Namespace, error)

GetNamespaces fetches and transforms namespaces It filters namespaces based on user permissions

type ResourceListService

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

ResourceListService provides business logic for listing Kubernetes resources

func NewResourceListService

func NewResourceListService(clusterService ClusterService, prometheusURL string) *ResourceListService

NewResourceListService creates a new ResourceListService

func (*ResourceListService) ListResources

ListResources fetches and transforms Kubernetes resources of a specific kind This is the business logic layer that handles the transformation of different resource types It filters resources based on user permissions before returning them

type ResourceRepository

type ResourceRepository interface {
	Get(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool) (*unstructured.Unstructured, error)
	Create(ctx context.Context, gvr schema.GroupVersionResource, namespace string, namespaced bool, obj *unstructured.Unstructured, options metav1.CreateOptions) (*unstructured.Unstructured, error)
	Patch(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool, patchData []byte, patchType types.PatchType, options metav1.PatchOptions) (*unstructured.Unstructured, error)
	Delete(ctx context.Context, gvr schema.GroupVersionResource, name, namespace string, namespaced bool, options metav1.DeleteOptions) error
}

ResourceRepository defines the interface for accessing Kubernetes resources

type ResourceService

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

ResourceService provides business logic for Kubernetes resource operations. It handles resource updates, deletions, and YAML retrieval using the dynamic client.

func NewResourceService

func NewResourceService(resourceRepo ResourceRepository, gvrResolver GVRResolver) *ResourceService

NewResourceService creates a new ResourceService with the provided repository and GVR resolver.

func (*ResourceService) CreateResource

func (s *ResourceService) CreateResource(ctx context.Context, yamlContent string) (interface{}, error)

CreateResource creates a Kubernetes resource from YAML content. It validates namespace permissions before creating.

func (*ResourceService) DeleteResource

func (s *ResourceService) DeleteResource(ctx context.Context, req DeleteResourceRequest) error

DeleteResource deletes a Kubernetes resource. By default, it uses a 30-second grace period and foreground deletion propagation. If Force is true, it uses 0 grace period and background propagation. Returns an error if the resource cannot be found or deletion fails. It validates namespace permissions before deleting.

func (*ResourceService) GetResourceYAML

func (s *ResourceService) GetResourceYAML(ctx context.Context, req GetResourceRequest, discoveryClient interface{}) (string, error)

GetResourceYAML fetches a Kubernetes resource and returns its YAML representation. It resolves the GroupVersionResource, retrieves the resource, and converts it to YAML format. Returns the YAML string or an error if the resource cannot be found or retrieved.

func (*ResourceService) UpdateResource

func (s *ResourceService) UpdateResource(ctx context.Context, req UpdateResourceRequest) error

UpdateResource updates a Kubernetes resource from YAML content. It parses the YAML, resolves the GroupVersionResource, and applies the changes using a strategic merge patch. Returns an error if the YAML is invalid, the resource cannot be found, or the update fails. It validates namespace permissions before updating.

type Service

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

Service provides HTTP handlers for Kubernetes resource operations. It follows a layered architecture pattern with dependency injection via ServiceFactory.

func NewService

func NewService(h *models.Handlers, cs ClusterService) *Service

NewService creates a new Kubernetes service with the provided handlers and cluster service.

func (*Service) CreateResourceYAML

func (s *Service) CreateResourceYAML(w http.ResponseWriter, r *http.Request)

CreateResourceYAML handles HTTP POST requests to create a Kubernetes resource from YAML.

func (*Service) DeleteResource

func (s *Service) DeleteResource(w http.ResponseWriter, r *http.Request)

DeleteResource handles HTTP DELETE requests to delete a Kubernetes resource.

func (*Service) DryRunResourceYAML

func (s *Service) DryRunResourceYAML(w http.ResponseWriter, r *http.Request)

DryRunResourceYAML performs a dry-run creation

func (*Service) GetClusterStats

func (s *Service) GetClusterStats(w http.ResponseWriter, r *http.Request)

GetClusterStats handles HTTP GET requests to retrieve cluster-wide statistics. Returns counts of nodes, namespaces, pods, deployments, services, ingresses, PVCs, and PVs.

Example response:

{
  "nodes": 3,
  "namespaces": 10,
  "pods": 45,
  ...
}

func (*Service) GetNamespaces

func (s *Service) GetNamespaces(w http.ResponseWriter, r *http.Request)

GetNamespaces handles HTTP GET requests to retrieve all Kubernetes namespaces. Returns a JSON array of namespace objects.

@Summary Listar namespaces @Description Retorna todos los namespaces de Kubernetes @Tags k8s @Security Bearer @Produce json @Success 200 {array} object "Lista de namespaces" @Failure 400 {object} map[string]string "Error en la solicitud" @Failure 500 {object} map[string]string "Error del servidor" @Router /api/namespaces [get]

Example response:

[{"name": "default", "status": "Active"}, ...]

func (*Service) GetResourceYAML

func (s *Service) GetResourceYAML(w http.ResponseWriter, r *http.Request)

GetResourceYAML returns the YAML representation of a resource Refactored to use layered architecture: Handler (HTTP) -> Service (Business Logic) -> Repository (Data Access)

func (*Service) GetResources

func (s *Service) GetResources(w http.ResponseWriter, r *http.Request)

GetResources handles HTTP GET requests to list Kubernetes resources of a specific kind. Query parameters:

  • kind: The resource kind (e.g., "Pod", "Deployment", "Service")
  • namespace: The namespace to filter by, or "all" for all namespaces

Returns a JSON array of resource objects with metadata and status information.

func (*Service) ImportResourceYAML

func (s *Service) ImportResourceYAML(w http.ResponseWriter, r *http.Request)

ImportResourceYAML handles HTTP POST requests to import multiple Kubernetes resources from YAML.

func (*Service) RolloutDeployment

func (s *Service) RolloutDeployment(w http.ResponseWriter, r *http.Request)

RolloutDeployment triggers a rollout/restart of a deployment Refactored to use layered architecture: Handler -> Service -> Repository

func (*Service) ScaleResource

func (s *Service) ScaleResource(w http.ResponseWriter, r *http.Request)

ScaleResource handles HTTP POST requests to scale a Kubernetes Deployment. Query parameters:

  • kind: Must be "Deployment"
  • name: The deployment name
  • namespace: The namespace (defaults to "default" if empty)
  • delta: The number of replicas to add or subtract (positive or negative integer)

Returns the new replica count on success.

func (*Service) ServerSideApply

func (s *Service) ServerSideApply(w http.ResponseWriter, r *http.Request)

ServerSideApply performs a server-side apply

func (*Service) StreamResourceCreation

func (s *Service) StreamResourceCreation(w http.ResponseWriter, r *http.Request)

StreamResourceCreation handles SSE for resource creation feedback

func (*Service) TriggerCronJob

func (s *Service) TriggerCronJob(w http.ResponseWriter, r *http.Request)

TriggerCronJob triggers a CronJob manually Refactored to use layered architecture: Handler -> Service -> Repository

func (*Service) UpdateResourceYAML

func (s *Service) UpdateResourceYAML(w http.ResponseWriter, r *http.Request)

UpdateResourceYAML handles HTTP PUT requests to update a Kubernetes resource from YAML.

func (*Service) ValidateResourceYAML

func (s *Service) ValidateResourceYAML(w http.ResponseWriter, r *http.Request)

ValidateResourceYAML validates the YAML content

func (*Service) WatchResources

func (s *Service) WatchResources(w http.ResponseWriter, r *http.Request)

WatchResources handles WebSocket connections for watching Kubernetes resources

type ServiceFactory

type ServiceFactory struct{}

ServiceFactory provides factory methods for creating business logic services

func NewServiceFactory

func NewServiceFactory() *ServiceFactory

NewServiceFactory creates a new ServiceFactory

func (*ServiceFactory) CreateClusterStatsService

func (f *ServiceFactory) CreateClusterStatsService(client kubernetes.Interface) *ClusterStatsService

CreateClusterStatsService creates a ClusterStatsService with the given client

func (*ServiceFactory) CreateCronJobService

func (f *ServiceFactory) CreateCronJobService(client kubernetes.Interface) *CronJobService

CreateCronJobService creates a CronJobService with the given client

func (*ServiceFactory) CreateDeploymentService

func (f *ServiceFactory) CreateDeploymentService(client kubernetes.Interface) *DeploymentService

CreateDeploymentService creates a DeploymentService with the given client

func (*ServiceFactory) CreateImportService

func (f *ServiceFactory) CreateImportService(dynamicClient dynamic.Interface, client kubernetes.Interface) *ImportService

CreateImportService creates an ImportService with the given clients

func (*ServiceFactory) CreateNamespaceService

func (f *ServiceFactory) CreateNamespaceService(client kubernetes.Interface) *NamespaceService

CreateNamespaceService creates a NamespaceService with the given client

func (*ServiceFactory) CreateResourceService

func (f *ServiceFactory) CreateResourceService(dynamicClient dynamic.Interface, client kubernetes.Interface) *ResourceService

CreateResourceService creates a ResourceService with the given dynamic client and discovery client

func (*ServiceFactory) CreateWatchService

func (f *ServiceFactory) CreateWatchService() *WatchService

CreateWatchService creates a WatchService

type UpdateResourceRequest

type UpdateResourceRequest struct {
	YAMLContent string // YAML content of the resource to update
	Kind        string // Resource kind (e.g., "Pod", "Deployment")
	Name        string // Resource name
	Namespace   string // Namespace (empty for cluster-scoped resources)
	Namespaced  bool   // Whether the resource is namespaced
}

UpdateResourceRequest represents parameters for updating a Kubernetes resource.

type WatchRequest

type WatchRequest struct {
	Kind          string
	Namespace     string
	AllNamespaces bool
}

WatchRequest represents parameters for watching resources

type WatchResult

type WatchResult struct {
	Type      string `json:"type"`
	Name      string `json:"name"`
	Namespace string `json:"namespace"`
}

WatchResult represents a single watch event

type WatchService

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

WatchService provides business logic for watching Kubernetes resources Optional function hooks allow overriding behavior in tests without changing production logic.

func NewWatchService

func NewWatchService(gvrResolver GVRResolver) *WatchService

NewWatchService creates a new WatchService

func (*WatchService) StartWatch

func (s *WatchService) StartWatch(ctx context.Context, client dynamic.Interface, req WatchRequest) (watch.Interface, error)

StartWatch initializes a Kubernetes watch for the specified resource type Returns a watcher interface that can be used to receive events

func (*WatchService) TransformEvent

func (s *WatchService) TransformEvent(event watch.Event) (*WatchResult, error)

TransformEvent transforms a Kubernetes watch event to a WatchResult DTO

Jump to

Keyboard shortcuts

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