gocsi

package module
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Dec 4, 2017 License: Apache-2.0 Imports: 23 Imported by: 38

README

GoCSI

The Container Storage Interface (CSI) is an industry standard specification for creating storage plug-ins for container orchestrators. GoCSI aids in the development and testing of CSI plug-ins:

Component Description
gocsi CSI Go library
csc CSI command line interface (CLI) client
csp CSI storage plug-in (CSP) bootstrapper
mock CSI mock storage plug-in (SP)

Quick Start

The following example illustrates using Docker in combination with the GoCSI SP bootstrapper csp to create a new CSI SP from scratch, serve it on a UNIX socket, and then use the GoCSI command line client csc to invoke the GetSupportedVersions and GetPluginInfo RPCs:

$ docker run -it golang:latest sh -c \
  "go get github.com/thecodeteam/gocsi && \
  make -C src/github.com/thecodeteam/gocsi csi-sp"

Library

The root of the GoCSI project is a general purpose library for CSI. This package provides the following features:

Interceptors

GoCSI includes the following gRPC client-side and server-side interceptors:

Type Client Server Description
Request & response logging Logs request & response data (except UserCredentials)
Request ID injector Injects outgoing (or incoming) requests with a unique ID
Spec validator Validates requests & responses against the CSI spec
Idempotency Assists in making an SP idempotent

Please refer to the CSI client csc for examples of how to implement the client-side interceptors. The csp package illustrates the use of GoCSI's server-side interceptors.

PageVolumes

The PageVolumes function invokes the ListVolumes RPC until all available volumes are retrieved, returning them over a Go channel.

func PageVolumes(
	ctx context.Context,
	client csi.ControllerClient,
	req csi.ListVolumesRequest,
	opts ...grpc.CallOption) (<-chan csi.VolumeInfo, <-chan error)

The csc command controller listvolumes --paging uses PageVolumes to stream volumes from an SP in order to minimize the amount of memory required for a client to process all available volumes.

Documentation

Overview

Package gocsi provides a Container Storage Interface (CSI) library, client, and other helpful utilities.

Index

Constants

View Source
const (
	// Namespace is the namesapce used by the protobuf.
	Namespace = "csi"

	// CSIEndpoint is the name of the environment variable that
	// contains the CSI endpoint.
	CSIEndpoint = "CSI_ENDPOINT"

	// CreateVolume is the full method name for the
	// eponymous RPC message.
	CreateVolume = ctrlSvc + "CreateVolume"

	// DeleteVolume is the full method name for the
	// eponymous RPC message.
	DeleteVolume = ctrlSvc + "DeleteVolume"

	// ControllerPublishVolume is the full method name for the
	// eponymous RPC message.
	ControllerPublishVolume = ctrlSvc + "ControllerPublishVolume"

	// ControllerUnpublishVolume is the full method name for the
	// eponymous RPC message.
	ControllerUnpublishVolume = ctrlSvc + "ControllerUnpublishVolume"

	// ValidateVolumeCapabilities is the full method name for the
	// eponymous RPC message.
	ValidateVolumeCapabilities = ctrlSvc + "ValidateVolumeCapabilities"

	// ListVolumes is the full method name for the
	// eponymous RPC message.
	ListVolumes = ctrlSvc + "ListVolumes"

	// GetCapacity is the full method name for the
	// eponymous RPC message.
	GetCapacity = ctrlSvc + "GetCapacity"

	// ControllerGetCapabilities is the full method name for the
	// eponymous RPC message.
	ControllerGetCapabilities = ctrlSvc + "ControllerGetCapabilities"

	// ControllerProbe is the full method name for the
	// eponymous RPC message.
	ControllerProbe = ctrlSvc + "ControllerProbe"

	// GetSupportedVersions is the full method name for the
	// eponymous RPC message.
	GetSupportedVersions = identSvc + "GetSupportedVersions"

	// GetPluginInfo is the full method name for the
	// eponymous RPC message.
	GetPluginInfo = identSvc + "GetPluginInfo"

	// GetNodeID is the full method name for the
	// eponymous RPC message.
	GetNodeID = nodeSvc + "GetNodeID"

	// NodePublishVolume is the full method name for the
	// eponymous RPC message.
	NodePublishVolume = nodeSvc + "NodePublishVolume"

	// NodeUnpublishVolume is the full method name for the
	// eponymous RPC message.
	NodeUnpublishVolume = nodeSvc + "NodeUnpublishVolume"

	// NodeProbe is the full method name for the
	// eponymous RPC message.
	NodeProbe = nodeSvc + "NodeProbe"

	// NodeGetCapabilities is the full method name for the
	// eponymous RPC message.
	NodeGetCapabilities = nodeSvc + "NodeGetCapabilities"
)

Variables

View Source
var ErrAccessModeRequired = status.Error(
	codes.InvalidArgument, "acess mode required")

ErrAccessModeRequired occurs when an RPC is made with a missing acess mode argument.

View Source
var ErrAccessTypeRequired = status.Error(
	codes.InvalidArgument, "acess type required")

ErrAccessTypeRequired occurs when an RPC is made with a missing acess type argument.

View Source
var ErrBlockTypeRequired = status.Error(
	codes.InvalidArgument, "block type required")

ErrBlockTypeRequired occurs when an RPC is made with a missing access type block value.

View Source
var ErrEmptyNodeID = status.Error(codes.Internal, "empty node ID")

ErrEmptyNodeID occurs when an RPC returns an empty NodeID.

View Source
var ErrEmptyPluginName = status.Error(
	codes.Internal, "empty plug-in name")

ErrEmptyPluginName occurs when GetPluginInfo returns an empty plug-in name.

View Source
var ErrEmptyPublishVolumeInfo = status.Error(
	codes.Internal, "empty publish volume info")

ErrEmptyPublishVolumeInfo occurs when an RPC returns empty publish volume info.

View Source
var ErrEmptySupportedVersions = status.Error(
	codes.Internal, "empty supported versions")

ErrEmptySupportedVersions occurs when an RPC returns a zero-length supported versions list.

View Source
var ErrEmptyVendorVersion = status.Error(
	codes.Internal, "empty vendor version")

ErrEmptyVendorVersion occurs when GetPluginInfo returns an empty vendor version.

View Source
var ErrEmptyVolumeID = status.Error(
	codes.Internal, "empty volumeInfo.Id")

ErrEmptyVolumeID occurs when an RPC returns a VolumeInfo with an zero-length Id field.

View Source
var ErrInvalidProvider = errors.New("invalid service provider")

ErrInvalidProvider is returned from NewService if the specified provider name is unknown.

View Source
var ErrInvalidTargetPath = errors.New("invalid targetPath")

ErrInvalidTargetPath occurs when an RPC is made with an invalid targetPath argument.

View Source
var ErrMissingCSIEndpoint = errors.New("missing CSI_ENDPOINT")

ErrMissingCSIEndpoint occurs when the value for the environment variable CSI_ENDPOINT is not set.

View Source
var ErrMountTypeRequired = status.Error(
	codes.InvalidArgument, "mount type required")

ErrMountTypeRequired occurs when an RPC is made with a missing access type mount value.

View Source
var ErrNilVolumeInfo = status.Error(
	codes.Internal, "nil volumeInfo")

ErrNilVolumeInfo occurs when an RPC returns a nil VolumeInfo.

View Source
var ErrNodeIDRequired = status.Error(
	codes.InvalidArgument, "node ID required")

ErrNodeIDRequired occurs when an RPC is made with an empty node ID argument.

View Source
var ErrNonNilControllerCapabilities = status.Error(
	codes.Internal, "non-nil, empty controller capabilities")

ErrNonNilControllerCapabilities occurs when NodeGetCapabilities returns a non-nil, empty list.

View Source
var ErrNonNilEmptyAttribs = status.Error(
	codes.Internal, "non-nil, empty volumeInfo.Attributes")

ErrNonNilEmptyAttribs occurs when an RPC returns a VolumeInfo with a non-nil Attributes field that has no elements.

View Source
var ErrNonNilEmptyPluginManifest = status.Error(
	codes.Internal, "non-nil, empty plug-in manifest")

ErrNonNilEmptyPluginManifest occurs when GetPluginInfo returns a non-nil, empty manifest.

View Source
var ErrNonNilNodeCapabilities = status.Error(
	codes.Internal, "non-nil, empty node capabilities")

ErrNonNilNodeCapabilities occurs when NodeGetCapabilities returns a non-nil, empty list.

View Source
var ErrOpPending = status.Error(
	codes.FailedPrecondition, "op pending")

ErrOpPending occurs when an RPC is made against a resource that is involved in a concurrent operation.

View Source
var ErrParseProtoAddrRequired = errors.New(
	"non-empty network address is required")

ErrParseProtoAddrRequired occurs when an empty string is provided to ParseProtoAddr.

View Source
var ErrPublishVolumeInfoRequired = status.Error(
	codes.InvalidArgument, "publish volume info required")

ErrPublishVolumeInfoRequired occurs when an RPC is made with an empty publish volume info argument.

View Source
var ErrTargetPathRequired = status.Error(
	codes.InvalidArgument, "target path required")

ErrTargetPathRequired occurs when an RPC is made with an empty target path argument.

View Source
var ErrUserCredentialsRequired = status.Error(
	codes.InvalidArgument, "user credentials required")

ErrUserCredentialsRequired occurs when an RPC is made with an empty user credentials argument.

View Source
var ErrVolumeAttributesRequired = status.Error(
	codes.InvalidArgument, "volume attributes required")

ErrVolumeAttributesRequired occurs when an RPC is made with an empty volume attributes argument.

View Source
var ErrVolumeCapabilitiesRequired = status.Error(
	codes.InvalidArgument, "volume capabilities required")

ErrVolumeCapabilitiesRequired occurs when an RPC is made with an empty volume capabilties argument.

View Source
var ErrVolumeCapabilityRequired = status.Error(
	codes.InvalidArgument, "volume capability required")

ErrVolumeCapabilityRequired occurs when an RPC is made with a missing volume capability argument.

View Source
var ErrVolumeIDRequired = status.Error(
	codes.InvalidArgument, "volume ID required")

ErrVolumeIDRequired occurs when an RPC is made with an empty volume ID argument.

View Source
var ErrVolumeNameRequired = status.Error(
	codes.InvalidArgument, "volume name required")

ErrVolumeNameRequired occurs when an RPC is made with an empty volume name argument.

Functions

func ChainUnaryClient

ChainUnaryClient chains one or more unary, client interceptors together into a left-to-right series that can be provided to a new gRPC client.

func ChainUnaryServer

ChainUnaryServer chains one or more unary, server interceptors together into a left-to-right series that can be provided to a new gRPC server.

func CompareVersions

func CompareVersions(a, b *csi.Version) int8

CompareVersions compares two versions and returns:

-1 if a > b
 0 if a = b
 1 if a < b

func ErrVolumeNotFound added in v0.2.4

func ErrVolumeNotFound(id string) error

ErrVolumeNotFound returns an error indicating a volume with the specified ID cannot be found.

func FprintfVersion added in v0.2.0

func FprintfVersion(w io.Writer, v csi.Version) (int, error)

FprintfVersion formats a Version as a string to the specified writer.

func GetCSIEndpoint

func GetCSIEndpoint() (network, addr string, err error)

GetCSIEndpoint returns the network address specified by the environment variable CSI_ENDPOINT.

func GetCSIEndpointListener

func GetCSIEndpointListener() (net.Listener, error)

GetCSIEndpointListener returns the net.Listener for the endpoint specified by the environment variable CSI_ENDPOINT.

func GetRequestID

func GetRequestID(ctx context.Context) (uint64, bool)

GetRequestID inspects the context for gRPC metadata and returns its request ID if available.

func IsSuccess added in v0.2.0

func IsSuccess(err error, successCodes ...codes.Code) error

IsSuccess returns nil if the provided error is an RPC error with an error code that is OK (0) or matches one of the additional, provided successful error codes. Otherwise the original error is returned.

func IsSuccessfulResponse added in v0.2.0

func IsSuccessfulResponse(method string, err error) error

IsSuccessfulResponse uses IsSuccess to determine if the response for a specific CSI method is successful. If successful a nil value is returned; otherwise the original error is returned.

func LookupEnv added in v0.2.0

func LookupEnv(ctx context.Context, key string) (string, bool)

LookupEnv returns the value of the provided environment variable by:

  1. Inspecting the context for a key "os.Environ" with a string slice value. If such a key and value exist then the string slice is searched for the specified key and if found its value is returned.

  2. Inspecting the context for a key "os.LookupEnv" with a value of func(string) (string, bool). If such a key and value exist then the function is used to attempt to discover the key's value. If the key and value are found they are returned.

  3. Returning the result of os.LookupEnv.

func NewBlockCapability

func NewBlockCapability(
	mode csi.VolumeCapability_AccessMode_Mode) *csi.VolumeCapability

NewBlockCapability returns a new *csi.VolumeCapability for a volume that is to be accessed as a raw device.

func NewClientLogger added in v0.2.0

func NewClientLogger(
	opts ...LoggingOption) grpc.UnaryClientInterceptor

NewClientLogger provides a UnaryClientInterceptor that can be configured to log both request and response data.

func NewClientRequestIDInjector added in v0.2.0

func NewClientRequestIDInjector() grpc.UnaryClientInterceptor

NewClientRequestIDInjector provides a UnaryClientInterceptor that injects the outgoing context with gRPC metadata that contains a unique ID.

func NewClientSpecValidator added in v0.2.0

func NewClientSpecValidator(
	opts ...SpecValidatorOption) grpc.UnaryClientInterceptor

NewClientSpecValidator provides a UnaryClientInterceptor that validates client request and response data against the CSI specification.

func NewIdempotentInterceptor

func NewIdempotentInterceptor(
	p IdempotencyProvider,
	opts ...IdempotentInterceptorOption) grpc.UnaryServerInterceptor

NewIdempotentInterceptor returns a new server-side, gRPC interceptor that can be used in conjunction with an IdempotencyProvider to provide serialized, idempotent access to the following CSI RPCs:

  • CreateVolume
  • DeleteVolume
  • ControllerPublishVolume
  • ControllerUnpublishVolume
  • NodePublishVolume
  • NodeUnpublishVolume

func NewMountCapability

func NewMountCapability(
	mode csi.VolumeCapability_AccessMode_Mode,
	fsType string,
	mountFlags ...string) *csi.VolumeCapability

NewMountCapability returns a new *csi.VolumeCapability for a volume that is to be mounted.

func NewServerLogger added in v0.2.0

func NewServerLogger(
	opts ...LoggingOption) grpc.UnaryServerInterceptor

NewServerLogger returns a new UnaryServerInterceptor that can be configured to log both request and response data.

func NewServerRequestIDInjector added in v0.2.0

func NewServerRequestIDInjector() grpc.UnaryServerInterceptor

NewServerRequestIDInjector returns a new UnaryServerInterceptor that reads a unique request ID from the incoming context's gRPC metadata. If the incoming context does not contain gRPC metadata or a request ID, then a new request ID is generated.

func NewServerSpecValidator added in v0.2.0

func NewServerSpecValidator(
	opts ...SpecValidatorOption) grpc.UnaryServerInterceptor

NewServerSpecValidator returns a new UnaryServerInterceptor that validates server request and response data against the CSI specification.

func PageVolumes added in v0.2.0

func PageVolumes(
	ctx context.Context,
	client csi.ControllerClient,
	req csi.ListVolumesRequest,
	opts ...grpc.CallOption) (<-chan csi.VolumeInfo, <-chan error)

PageVolumes issues one or more ListVolumes requests to retrieve all available volumes, returning them over a Go channel.

func ParseMap added in v0.2.0

func ParseMap(line string) map[string]string

ParseMap parses a string into a map. The string's expected pattern is:

KEY1=VAL1, "KEY2=VAL2 ", "KEY 3= VAL3"

The key/value pairs are separated by a comma and optional whitespace. Please see the encoding/csv package (https://goo.gl/1j1xb9) for information on how to quote keys and/or values to include leading and trailing whitespace.

func ParseMapWS added in v0.2.4

func ParseMapWS(line string) map[string]string

ParseMapWS parses a string into a map. The string's expected pattern is:

KEY1=VAL1 KEY2="VAL2 " "KEY 3"=' VAL3'

The key/value pairs are separated by one or more whitespace characters. Keys and/or values with whitespace should be quoted with either single or double quotes.

func ParseProtoAddr

func ParseProtoAddr(protoAddr string) (proto string, addr string, err error)

ParseProtoAddr parses a Golang network address.

func ParseSlice added in v0.2.4

func ParseSlice(line string) []string

ParseSlice parses a string into a slice. The string's expected pattern is:

VAL1, "VAL2 ", " VAL3 "

The values are separated by a comma and optional whitespace. Please see the encoding/csv package (https://goo.gl/1j1xb9) for information on how to quote values to include leading and trailing whitespace.

func ParseVersion

func ParseVersion(s string) (csi.Version, bool)

ParseVersion parses a string for a CSI version.

func ParseVersions added in v0.2.0

func ParseVersions(s string) []csi.Version

ParseVersions parses a string for one or more CSI versions.

func Setenv added in v0.2.0

func Setenv(ctx context.Context, key, val string) error

Setenv sets the value of the provided environment variable to the specified value by first inspecting the context for a key "os.Setenv" with a value of func(string, string) error. If the context does not contain such a function then os.Setenv is used instead.

func SprintfVersion

func SprintfVersion(v csi.Version) string

SprintfVersion formats a Version as a string.

func WithEnviron added in v0.2.3

func WithEnviron(ctx context.Context, v []string) context.Context

WithEnviron returns a new Context with the provided environment variable string slice.

func WithLookupEnv added in v0.2.0

func WithLookupEnv(ctx context.Context, f lookupEnvFunc) context.Context

WithLookupEnv returns a new Context with the provided function.

func WithSetenv added in v0.2.0

func WithSetenv(ctx context.Context, f setenvFunc) context.Context

WithSetenv returns a new Context with the provided function.

Types

type IdempotencyProvider

type IdempotencyProvider interface {
	// GetVolumeID should return the ID of the volume specified
	// by the provided volume name. If the volume does not exist then
	// an empty string should be returned.
	GetVolumeID(ctx context.Context, name string) (string, error)

	// GetVolumeInfo should return information about the volume
	// specified by the provided volume ID or name. If the volume does not
	// exist then a nil value should be returned.
	GetVolumeInfo(ctx context.Context, id, name string) (*csi.VolumeInfo, error)

	// IsControllerPublished should return publication for a volume's
	// publication status on a specified node.
	IsControllerPublished(
		ctx context.Context,
		volumeID, nodeID string) (map[string]string, error)

	// IsNodePublished should return a flag indicating whether or
	// not the volume exists and is published on the current host.
	IsNodePublished(
		ctx context.Context,
		id string,
		pubVolInfo map[string]string,
		targetPath string) (bool, error)
}

IdempotencyProvider is the interface that works with a server-side, gRPC interceptor to provide serial access and idempotency for CSI's volume resources.

type IdempotentInterceptorOption added in v0.2.0

type IdempotentInterceptorOption func(*idempIntercOpts)

IdempotentInterceptorOption configures the idempotent interceptor.

func WithIdempRequireVolumeExists added in v0.2.0

func WithIdempRequireVolumeExists() IdempotentInterceptorOption

WithIdempRequireVolumeExists is an IdempotentInterceptorOption that enforces the requirement that volumes must exist before proceeding with an operation.

func WithIdempTimeout added in v0.2.0

func WithIdempTimeout(t time.Duration) IdempotentInterceptorOption

WithIdempTimeout is an IdempotentInterceptorOption that sets the timeout used by the idempotent interceptor.

type LoggingOption added in v0.2.0

type LoggingOption func(*loggingOpts)

LoggingOption configures the logging interceptor.

func WithRequestLogging added in v0.2.0

func WithRequestLogging(w io.Writer) LoggingOption

WithRequestLogging is a LoggingOption that enables request logging for the logging interceptor.

func WithResponseLogging added in v0.2.0

func WithResponseLogging(w io.Writer) LoggingOption

WithResponseLogging is a LoggingOption that enables response logging for the logging interceptor.

type MutexWithTryLock

type MutexWithTryLock interface {
	// Lock locks the mutex.
	Lock()
	// Unlock unlocks the mutex.
	Unlock()
	// TryLock attempts to obtain a lock but times out if no lock
	// can be obtained in the specified duration. A flag is returned
	// indicating whether or not the lock was obtained.
	TryLock(timeout time.Duration) bool
}

MutexWithTryLock is a lock object that implements the semantics of a sync.Mutex in addition to a TryLock function.

func NewMutexWithTryLock

func NewMutexWithTryLock() MutexWithTryLock

NewMutexWithTryLock returns a new mutex that implements TryLock.

type PipeConn added in v0.2.0

type PipeConn interface {
	net.Listener

	// DialGrpc is used by a grpc client.
	DialGrpc(raddr string, timeout time.Duration) (net.Conn, error)

	// DialHTTP is used by net.http clients.
	DialHTTP(ctx context.Context, network, addr string) (net.Conn, error)
}

PipeConn is an in-memory network connection that can be provided to a Serve function as a net.Listener and to gRPC/net.http clients as their dialer.

func NewPipeConn added in v0.2.0

func NewPipeConn(name string) PipeConn

NewPipeConn returns a new pipe connection. The provided name is returned by PipeConn.Addr().String().

type SpecValidatorOption added in v0.2.0

type SpecValidatorOption func(*specValidatorOpts)

SpecValidatorOption configures the spec validator interceptor.

func WithRequiresControllerPublishVolumeCredentials added in v0.2.0

func WithRequiresControllerPublishVolumeCredentials() SpecValidatorOption

WithRequiresControllerPublishVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresControllerUnpublishVolumeCredentials added in v0.2.0

func WithRequiresControllerUnpublishVolumeCredentials() SpecValidatorOption

WithRequiresControllerUnpublishVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresCreateVolumeCredentials added in v0.2.0

func WithRequiresCreateVolumeCredentials() SpecValidatorOption

WithRequiresCreateVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresDeleteVolumeCredentials added in v0.2.0

func WithRequiresDeleteVolumeCredentials() SpecValidatorOption

WithRequiresDeleteVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresNodeID added in v0.2.0

func WithRequiresNodeID() SpecValidatorOption

WithRequiresNodeID is a SpecValidatorOption that indicates ControllerPublishVolume requests and GetNodeID responses must contain non-empty node ID data.

func WithRequiresNodePublishVolumeCredentials added in v0.2.0

func WithRequiresNodePublishVolumeCredentials() SpecValidatorOption

WithRequiresNodePublishVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresNodeUnpublishVolumeCredentials added in v0.2.0

func WithRequiresNodeUnpublishVolumeCredentials() SpecValidatorOption

WithRequiresNodeUnpublishVolumeCredentials is a SpecValidatorOption that indicates the eponymous requests must contain non-empty credentials data.

func WithRequiresPublishVolumeInfo added in v0.2.0

func WithRequiresPublishVolumeInfo() SpecValidatorOption

WithRequiresPublishVolumeInfo is a SpecValidatorOption that indicates ControllerPublishVolume responses and NodePublishVolume requests must contain non-empty publish volume info data.

func WithRequiresVolumeAttributes added in v0.2.0

func WithRequiresVolumeAttributes() SpecValidatorOption

WithRequiresVolumeAttributes is a SpecValidatorOption that indicates ControllerPublishVolume, ValidateVolumeCapabilities, and NodePublishVolume requests must contain non-empty volume attribute data.

func WithSuccessCreateVolumeAlreadyExists added in v0.2.0

func WithSuccessCreateVolumeAlreadyExists() SpecValidatorOption

WithSuccessCreateVolumeAlreadyExists is a SpecValidatorOption that the eponymous request should treat the eponymous error code as successful.

func WithSuccessDeleteVolumeNotFound added in v0.2.0

func WithSuccessDeleteVolumeNotFound() SpecValidatorOption

WithSuccessDeleteVolumeNotFound is a SpecValidatorOption that the eponymous request should treat the eponymous error code as successful.

func WithSupportedVersions added in v0.2.0

func WithSupportedVersions(versions ...csi.Version) SpecValidatorOption

WithSupportedVersions is a SpecValidatorOption that indicates the list of versions supported by any CSI RPC that participates in version validation.

Directories

Path Synopsis
csc
cmd

Jump to

Keyboard shortcuts

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