client

package
v2.10.2 Latest Latest
Warning

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

Go to latest
Published: Jan 16, 2023 License: Apache-2.0 Imports: 47 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ConnectorSocketName is the path used when communicating to the connector process.
	ConnectorSocketName = "/tmp/telepresence-connector.socket"

	// DaemonSocketName is the path used when communicating to the daemon process.
	DaemonSocketName = "/var/run/telepresence-daemon.socket"
)
View Source
const (
	// APIVersion is the API version of the daemon and connector API.
	APIVersion = 3
)
View Source
const ConfigFile = "config.yml"

Variables

View Source
var DisplayName = "Telepresence" //nolint:gochecknoglobals // extension point

Functions

func CheckTimeout added in v2.1.2

func CheckTimeout(ctx context.Context, err error) error

func DialSocket

func DialSocket(ctx context.Context, socketName string, opts ...grpc.DialOption) (*grpc.ClientConn, error)

DialSocket dials the given socket and returns the resulting connection.

func DisplayVersion

func DisplayVersion() string

DisplayVersion returns a printable version for `telepresence`.

func Executable added in v2.5.0

func Executable() (string, error)

func GetConfigFile added in v2.3.5

func GetConfigFile(c context.Context) string

GetConfigFile gets the path to the configFile as stored in filelocation.AppUserConfigDir.

func GetExe

func GetExe() string

GetExe returns the name of the running executable.

func GetInstallMechanism added in v2.4.8

func GetInstallMechanism() (string, error)

GetInstallMechanism returns how the executable was installed on the machine.

func GetMechanismFromPath added in v2.4.8

func GetMechanismFromPath(execPath string) (string, error)

GetMechanismFromPath is a helper function that contains most of the logic required for GetInstallMechanism, but enables us to test it since we can control the path passed in.

func IsDaemon added in v2.9.0

func IsDaemon() bool

func IsRunning added in v2.8.0

func IsRunning(ctx context.Context, path string) (bool, error)

IsRunning makes an attempt to dial the given socket and returns true if that succeeds. If the attempt doesn't succeed the method returns false. No error is returned when the failed attempt is caused by a non-existing socket.

func ListenSocket added in v2.4.0

func ListenSocket(ctx context.Context, processName, socketName string) (net.Listener, error)

ListenSocket returns a listener for the given socket and returns the resulting connection.

func MergeAndReplace added in v2.9.0

func MergeAndReplace(c context.Context, defaults *Config, priority *Config, root bool) error

func ProcessName added in v2.9.0

func ProcessName() string

func ReloadDaemonConfig added in v2.9.0

func ReloadDaemonConfig(c context.Context, root bool) error

ReloadDaemonConfig calls SetLevel with the log level defined for the rootDaemon or userDaemon depending on the root flag. Assumes that the config has already been reloaded.

func RemoveSocket added in v2.4.0

func RemoveSocket(listener net.Listener) error

RemoveSocket removes any representation of the socket from the filesystem.

func ReplaceConfig added in v2.5.0

func ReplaceConfig(ctx context.Context, config *Config)

ReplaceConfig replaces the config last stored using WithConfig with the given Config.

func RestoreDefaults added in v2.9.0

func RestoreDefaults(c context.Context, root bool) error

func Retry

func Retry(c context.Context, text string, f func(context.Context) error, durations ...time.Duration) error

Retry will run the given function repeatedly with an increasing delay until it returns without error.

The function takes 0 to 2 durations with the following meaning

Delay - initial delay, i.e. the delay between the first and the second call.
MaxDelay - maximum delay between calling the functions (delay will never grow beyond this value)

func RunError

func RunError(err error) error

RunError checks if the given err is a *exit.ExitError, and if so, extracts Stderr and the ExitCode from it.

func Semver

func Semver() semver.Version

func SetConfig added in v2.4.0

func SetConfig(ctx context.Context, configDir, configYml string) (context.Context, error)

SetConfig creates a config from the configYml provided and assigns it to a new context which is returned. Use this if you are testing components of the config.yml, otherwise you can use setDefaultConfig.

func SetDefaultConfig added in v2.4.0

func SetDefaultConfig(ctx context.Context, configDir string) (context.Context, error)

SetDefaultConfig creates a config that has the registry set correctly. This ensures that the config on the machine of whatever is running the test, isn't used, which could cause conflict with the tests.

func SocketExists

func SocketExists(name string) (bool, error)

SocketExists returns true if a socket is found with the given name.

func Version

func Version() string

Version returns the version of this executable.

func WaitUntilRunning added in v2.8.0

func WaitUntilRunning(ctx context.Context, name, path string, ttw time.Duration) error

WaitUntilRunning waits until the socket at the given path comes into existence and a dial is successful and returns when that happens. The wait will be max ttw (time to wait) long.

func WaitUntilSocketAppears

func WaitUntilSocketAppears(name, path string, ttw time.Duration) error

WaitUntilSocketAppears waits until the socket at the given path comes into existence and returns when that happens. The wait will be max ttw (time to wait) long.

func WaitUntilSocketVanishes

func WaitUntilSocketVanishes(name, path string, ttw time.Duration) error

WaitUntilSocketVanishes waits until the socket at the given path is removed and returns when that happens. The wait will be max ttw (time to wait) long. An error is returned if that time is exceeded before the socket is removed.

func Watch added in v2.5.0

func Watch(c context.Context, onReload func(context.Context) error) error

Watch uses a file system watcher that receives events when the configuration changes and calls the given function when that happens.

func WithConfig added in v2.4.2

func WithConfig(ctx context.Context, config *Config) context.Context

WithConfig returns a context with the given Config.

func WithEnsuredState

func WithEnsuredState[T any](ctx context.Context, r T, prolog Prolog[T], action, epilog Action[T]) error

WithEnsuredState calls prolog, and if that was successful, calls act. If epilog is not nil, it is guaranteed to be called when prolog returns true.

func WithEnv added in v2.4.2

func WithEnv(ctx context.Context, env *Env) context.Context

WithEnv returns a context with the given Env.

func WrapRecvErr added in v2.3.3

func WrapRecvErr(err error, msg string) error

WrapRecvErr wraps an error from a Recv call. If the error is nil, nil is returned. If the error indicates that the remote end has , a RecvEOFError wrapping the error will be returned. Otherwise, the original error will be wrapped as fmt.Errorf("%s: %w", msg, err).

Types

type Action added in v2.9.0

type Action[T any] func(T, context.Context) error

type Cloud added in v2.3.5

type Cloud struct {
	SkipLogin       bool          `json:"skipLogin,omitempty" yaml:"skipLogin,omitempty"`
	RefreshMessages time.Duration `json:"refreshMessages,omitempty" yaml:"refreshMessages,omitempty"`
	SystemaHost     string        `json:"systemaHost,omitempty" yaml:"systemaHost,omitempty"`
	SystemaPort     string        `json:"systemaPort,omitempty" yaml:"systemaPort,omitempty"`
}

func (Cloud) IsZero added in v2.5.0

func (c Cloud) IsZero() bool

IsZero controls whether this element will be included in marshalled output.

func (Cloud) MarshalYAML added in v2.4.4

func (c Cloud) MarshalYAML() (any, error)

MarshalYAML is not using pointer receiver here, because Cloud is not pointer in the Config struct.

func (*Cloud) UnmarshalYAML added in v2.3.5

func (c *Cloud) UnmarshalYAML(node *yaml.Node) (err error)

UnmarshalYAML parses the images YAML.

type Config added in v2.1.2

type Config struct {
	Timeouts        Timeouts        `json:"timeouts,omitempty" yaml:"timeouts,omitempty"`
	LogLevels       LogLevels       `json:"logLevels,omitempty" yaml:"logLevels,omitempty"`
	Images          Images          `json:"images,omitempty" yaml:"images,omitempty"`
	Cloud           Cloud           `json:"cloud,omitempty" yaml:"cloud,omitempty"`
	Grpc            Grpc            `json:"grpc,omitempty" yaml:"grpc,omitempty"`
	TelepresenceAPI TelepresenceAPI `json:"telepresenceAPI,omitempty" yaml:"telepresenceAPI,omitempty"`
	Intercept       Intercept       `json:"intercept,omitempty" yaml:"intercept,omitempty"`
}

Config contains all configuration values for the telepresence CLI.

func GetConfig added in v2.1.2

func GetConfig(ctx context.Context) *Config

func GetDefaultConfig added in v2.4.2

func GetDefaultConfig() Config

GetDefaultConfig returns the default configuration settings.

func LoadConfig added in v2.4.2

func LoadConfig(c context.Context) (cfg *Config, err error)

LoadConfig loads and returns the Telepresence configuration as stored in filelocation.AppUserConfigDir or filelocation.AppSystemConfigDirs.

func (*Config) Merge added in v2.4.4

func (c *Config) Merge(o *Config)

Merge merges this instance with the non-zero values of the given argument. The argument values take priority.

type DNS added in v2.9.0

type DNS struct {
	LocalIP         net.IP        `json:"localIP,omitempty" yaml:"localIP,omitempty"`
	RemoteIP        net.IP        `json:"remoteIP,omitempty" yaml:"remoteIP,omitempty"`
	IncludeSuffixes []string      `json:"includeSuffixes,omitempty" yaml:"includeSuffixes,omitempty"`
	ExcludeSuffixes []string      `json:"excludeSuffixes,omitempty" yaml:"excludeSuffixes,omitempty"`
	LookupTimeout   time.Duration `json:"lookupTimeout,omitempty" yaml:"lookupTimeout,omitempty"`
}

type DNSSnake added in v2.9.0

type DNSSnake struct {
	LocalIP         net.IP        `json:"local_ip,omitempty" yaml:"local_ip,omitempty"`
	RemoteIP        net.IP        `json:"remote_ip,omitempty" yaml:"remote_ip,omitempty"`
	IncludeSuffixes []string      `json:"include_suffixes,omitempty" yaml:"include_suffixes,omitempty"`
	ExcludeSuffixes []string      `json:"exclude_suffixes,omitempty" yaml:"exclude_suffixes,omitempty"`
	LookupTimeout   time.Duration `json:"lookup_timeout,omitempty" yaml:"lookup_timeout,omitempty"`
}

DNSSnake is the same as DNS but with snake_case json/yaml names.

type DnsConfig added in v2.9.0

type DnsConfig struct {
	// LocalIP is the address of the local DNS server. This entry is only
	// used on Linux system that are not configured to use systemd-resolved and
	// can be overridden by using the option --dns on the command line and defaults
	// to the first line of /etc/resolv.conf
	LocalIP iputil.IPKey `json:"local-ip,omitempty"`

	// RemoteIP is the address of the cluster's DNS service. It will default
	// to the IP of the kube-dns.kube-system or the dns-default.openshift-dns service.
	RemoteIP iputil.IPKey `json:"remote-ip,omitempty"`

	// ExcludeSuffixes are suffixes for which the DNS resolver will always return
	// NXDOMAIN (or fallback in case of the overriding resolver).
	ExcludeSuffixes []string `json:"exclude-suffixes,omitempty"`

	// IncludeSuffixes are suffixes for which the DNS resolver will always attempt to do
	// a lookup. Includes have higher priority than excludes.
	IncludeSuffixes []string `json:"include-suffixes,omitempty"`

	// The maximum time to wait for a cluster side host lookup.
	LookupTimeout v1.Duration `json:"lookup-timeout,omitempty"`
}

The DnsConfig is part of the KubeconfigExtension struct.

type Env

type Env struct {
	// I'd like to set TELEPRESENCE_LOGIN_DOMAIN,default=auth.datawire.io, but
	// sethvargo/go-envconfig doesn't support filling in the default for our later references to
	// it in following settings, so we have to do the hack with maybeSetDefault below.  *sigh* I
	// guess I'm just spoiled by apro/cmd/amb-sidecar/types/internal/envconfig.
	LoginDomain        string   `env:"TELEPRESENCE_LOGIN_DOMAIN,        parser=nonempty-string"`
	LoginAuthURL       *url.URL `env:"TELEPRESENCE_LOGIN_AUTH_URL,      parser=absolute-URL,    default=https://${TELEPRESENCE_LOGIN_DOMAIN}/auth"`
	LoginTokenURL      *url.URL `env:"TELEPRESENCE_LOGIN_TOKEN_URL,     parser=absolute-URL,    default=https://${TELEPRESENCE_LOGIN_DOMAIN}/token"`
	LoginCompletionURL *url.URL `env:"TELEPRESENCE_LOGIN_COMPLETION_URL,parser=absolute-URL,    default=https://${TELEPRESENCE_LOGIN_DOMAIN}/completion"`
	UserInfoURL        *url.URL `env:"TELEPRESENCE_USER_INFO_URL,       parser=absolute-URL,    default=https://${TELEPRESENCE_LOGIN_DOMAIN}/api/userinfo"`
	ManagerNamespace   string   `env:"TELEPRESENCE_MANAGER_NAMESPACE,   parser=nonempty-string,default=ambassador"`

	// This environment variable becomes the default for the images.registry and images.webhookRegistry
	Registry string `env:"TELEPRESENCE_REGISTRY,                        parser=nonempty-string,default=docker.io/datawire"`

	// This environment variable becomes the default for the images.agentImage and images.webhookAgentImage
	AgentImage string `env:"TELEPRESENCE_AGENT_IMAGE,                   parser=possibly-empty-string,default="`
	// contains filtered or unexported fields
}

func GetEnv added in v2.4.2

func GetEnv(ctx context.Context) *Env

func LoadEnv

func LoadEnv() (*Env, error)

func LoadEnvWith added in v2.4.7

func LoadEnvWith(lookupFunc func(key string) (string, bool)) (*Env, error)

func (Env) Get

func (env Env) Get(key string) string

type Grpc added in v2.3.5

type Grpc struct {
	// MaxReceiveSize is the maximum message size in bytes the client can receive in a gRPC call or stream message.
	// Overrides the gRPC default of 4MB.
	MaxReceiveSize resource.Quantity `json:"maxReceiveSize,omitempty" yaml:"maxReceiveSize,omitempty"`
}

func (Grpc) MarshalYAML added in v2.4.4

func (g Grpc) MarshalYAML() (any, error)

MarshalYAML is not using pointer receiver here, because Cloud is not pointer in the Config struct.

func (*Grpc) UnmarshalYAML added in v2.3.5

func (g *Grpc) UnmarshalYAML(node *yaml.Node) (err error)

UnmarshalYAML parses the images YAML.

type Images added in v2.3.5

type Images struct {
	PrivateRegistry        string `json:"registry,omitempty" yaml:"registry,omitempty"`
	PrivateAgentImage      string `json:"agentImage,omitempty" yaml:"agentImage,omitempty"`
	PrivateWebhookRegistry string `json:"webhookRegistry,omitempty" yaml:"webhookRegistry,omitempty"`
}

func (*Images) AgentImage added in v2.3.5

func (img *Images) AgentImage(c context.Context) string

func (*Images) Registry added in v2.3.5

func (img *Images) Registry(c context.Context) string

func (*Images) UnmarshalYAML added in v2.3.5

func (img *Images) UnmarshalYAML(node *yaml.Node) (err error)

UnmarshalYAML parses the images YAML.

func (*Images) WebhookRegistry added in v2.3.5

func (img *Images) WebhookRegistry(c context.Context) string

type Intercept added in v2.4.10

type Intercept struct {
	AppProtocolStrategy k8sapi.AppProtocolStrategy `json:"appProtocolStrategy,omitempty" yaml:"appProtocolStrategy,omitempty"`
	DefaultPort         int                        `json:"defaultPort,omitempty" yaml:"defaultPort,omitempty"`
	UseFtp              bool                       `json:"useFtp,omitempty" yaml:"useFtp,omitempty"`
}

func (Intercept) IsZero added in v2.5.0

func (ic Intercept) IsZero() bool

IsZero controls whether this element will be included in marshalled output.

func (Intercept) MarshalYAML added in v2.4.10

func (ic Intercept) MarshalYAML() (any, error)

MarshalYAML is not using pointer receiver here, because Intercept is not pointer in the Config struct.

type Kubeconfig added in v2.9.0

type Kubeconfig struct {
	KubeconfigExtension
	Namespace   string // default cluster namespace.
	Context     string
	Server      string
	FlagMap     map[string]string
	ConfigFlags *genericclioptions.ConfigFlags
	RestConfig  *rest.Config
}

func NewInClusterConfig added in v2.9.0

func NewInClusterConfig(c context.Context, flagMap map[string]string) (*Kubeconfig, error)

This represents an inClusterConfig.

func NewKubeconfig added in v2.9.0

func NewKubeconfig(c context.Context, flagMap map[string]string) (*Kubeconfig, error)

func (*Kubeconfig) AddRemoteKubeConfigExtension added in v2.9.0

func (kf *Kubeconfig) AddRemoteKubeConfigExtension(ctx context.Context, cfgYaml []byte) error

func (*Kubeconfig) ContextServiceAndFlagsEqual added in v2.9.0

func (kf *Kubeconfig) ContextServiceAndFlagsEqual(okf *Kubeconfig) bool

ContextServiceAndFlagsEqual determines if this instance is equal to the given instance with respect to context, server, and flag arguments.

func (*Kubeconfig) GetManagerNamespace added in v2.9.0

func (kf *Kubeconfig) GetManagerNamespace() string

func (*Kubeconfig) GetRestConfig added in v2.9.0

func (kf *Kubeconfig) GetRestConfig() *rest.Config

type KubeconfigExtension added in v2.9.0

type KubeconfigExtension struct {
	DNS        *DnsConfig       `json:"dns,omitempty"`
	AlsoProxy  []*iputil.Subnet `json:"also-proxy,omitempty"`
	NeverProxy []*iputil.Subnet `json:"never-proxy,omitempty"`
	Manager    *ManagerConfig   `json:"manager,omitempty"`
}

KubeconfigExtension is an extension read from the selected kubeconfig Cluster.

type LogLevels added in v2.3.0

type LogLevels struct {
	UserDaemon logrus.Level `json:"userDaemon,omitempty" yaml:"userDaemon,omitempty"`
	RootDaemon logrus.Level `json:"rootDaemon,omitempty" yaml:"rootDaemon,omitempty"`
}

func (LogLevels) IsZero added in v2.5.0

func (ll LogLevels) IsZero() bool

IsZero controls whether this element will be included in marshalled output.

func (*LogLevels) UnmarshalYAML added in v2.3.0

func (ll *LogLevels) UnmarshalYAML(node *yaml.Node) (err error)

UnmarshalYAML parses the logrus log-levels.

type ManagerConfig added in v2.9.0

type ManagerConfig struct {
	// Namespace is the name of the namespace where the traffic manager is to be found
	Namespace string `json:"namespace,omitempty"`
}

The ManagerConfig is part of the KubeconfigExtension struct. It configures discovery of the traffic manager.

type Prolog added in v2.9.0

type Prolog[T any] func(T, context.Context) (acquired bool, err error)

type RecvEOFError added in v2.8.0

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

RecvEOFError should be returned when a component has returned EOF from a stream. Do not use this if, for example, the initial dial to a stream fails.

func (*RecvEOFError) Error added in v2.8.0

func (e *RecvEOFError) Error() string

func (*RecvEOFError) Unwrap added in v2.8.0

func (e *RecvEOFError) Unwrap() error

type Routing added in v2.9.0

type Routing struct {
	Subnets    []*iputil.Subnet `json:"subnets,omitempty" yaml:"subnets,omitempty"`
	AlsoProxy  []*iputil.Subnet `json:"alsoProxy,omitempty" yaml:"alsoProxy,omitempty"`
	NeverProxy []*iputil.Subnet `json:"neverProxy,omitempty" yaml:"neverProxy,omitempty"`
}

type RoutingSnake added in v2.9.0

type RoutingSnake struct {
	Subnets    []*iputil.Subnet `json:"subnets,omitempty" yaml:"subnets,omitempty"`
	AlsoProxy  []*iputil.Subnet `json:"also_proxy_subnets,omitempty" yaml:"also_proxy_subnets,omitempty"`
	NeverProxy []*iputil.Subnet `json:"never_proxy_subnets,omitempty" yaml:"never_proxy_subnets,omitempty"`
}

RoutingSnake is the same as Routing but with snake_case json/yaml names.

type SessionConfig added in v2.9.0

type SessionConfig struct {
	ClientFile       string `json:"clientFile,omitempty" yaml:"clientFile,omitempty"`
	*Config          `json:"clientConfig" yaml:",inline"`
	DNS              DNS     `json:"dns,omitempty" yaml:"dns,omitempty"`
	Routing          Routing `json:"routing,omitempty" yaml:"routing,omitempty"`
	ManagerNamespace string  `json:"managerNamespace,omitempty" yaml:"managerNamespace,omitempty"`
}

type TelepresenceAPI added in v2.4.8

type TelepresenceAPI struct {
	Port int `json:"port,omitempty" yaml:"port,omitempty"`
}

type TimeoutID added in v2.3.0

type TimeoutID int
const (
	TimeoutAgentInstall TimeoutID = iota
	TimeoutApply
	TimeoutClusterConnect
	TimeoutConnectivityCheck
	TimeoutEndpointDial
	TimeoutHelm
	TimeoutIntercept
	TimeoutProxyDial
	TimeoutRoundtripLatency
	TimeoutTrafficManagerAPI
	TimeoutTrafficManagerConnect
)

type Timeouts added in v2.1.2

type Timeouts struct {

	// PrivateAgentInstall is how long to wait for an agent to be installed (i.e. apply of service and deploy manifests)
	PrivateAgentInstall time.Duration `json:"agentInstall,omitempty" yaml:"agentInstall,omitempty"`
	// PrivateApply is how long to wait for a k8s manifest to be applied
	PrivateApply time.Duration `json:"apply,omitempty" yaml:"apply,omitempty"`
	// PrivateClusterConnect is the maximum time to wait for a connection to the cluster to be established
	PrivateClusterConnect time.Duration `json:"clusterConnect,omitempty" yaml:"clusterConnect,omitempty"`
	// PrivateConnectivityCheck timeout used when checking if cluster is already proxied on the workstation
	PrivateConnectivityCheck time.Duration `json:"connectivityCheck,omitempty" yaml:"connectivityCheck,omitempty"`
	// PrivateEndpointDial is how long to wait for a Dial to a service for which the IP is known.
	PrivateEndpointDial time.Duration `json:"endpointDial,omitempty" yaml:"endpointDial,omitempty"`
	// PrivateHelm is how long to wait for any helm operation.
	PrivateHelm time.Duration `json:"helm,omitempty" yaml:"helm,omitempty"`
	// PrivateIntercept is the time to wait for an intercept after the agents has been installed
	PrivateIntercept time.Duration `json:"intercept,omitempty" yaml:"intercept,omitempty"`
	// PrivateRoundtripLatency is how much to add  to the EndpointDial timeout when establishing a remote connection.
	PrivateRoundtripLatency time.Duration `json:"roundtripLatency,omitempty" yaml:"roundtripLatency,omitempty"`
	// PrivateProxyDial is how long to wait for the proxy to establish an outbound connection
	PrivateProxyDial time.Duration `json:"proxyDial,omitempty" yaml:"proxyDial,omitempty"`
	// PrivateTrafficManagerConnect is how long to wait for the traffic-manager API to connect
	PrivateTrafficManagerAPI time.Duration `json:"trafficManagerAPI,omitempty" yaml:"trafficManagerAPI,omitempty"`
	// PrivateTrafficManagerConnect is how long to wait for the initial port-forwards to the traffic-manager
	PrivateTrafficManagerConnect time.Duration `json:"trafficManagerConnect,omitempty" yaml:"trafficManagerConnect,omitempty"`
}

func (*Timeouts) Get added in v2.4.2

func (t *Timeouts) Get(timeoutID TimeoutID) time.Duration

func (Timeouts) IsZero added in v2.5.0

func (t Timeouts) IsZero() bool

IsZero controls whether this element will be included in marshalled output.

func (Timeouts) MarshalYAML added in v2.4.4

func (t Timeouts) MarshalYAML() (any, error)

MarshalYAML is not using pointer receiver here, because Timeouts is not pointer in the Config struct.

func (*Timeouts) TimeoutContext added in v2.3.0

func (t *Timeouts) TimeoutContext(ctx context.Context, timeoutID TimeoutID) (context.Context, context.CancelFunc)

func (*Timeouts) UnmarshalYAML added in v2.1.2

func (t *Timeouts) UnmarshalYAML(node *yaml.Node) (err error)

UnmarshalYAML caters for the unfortunate fact that time.Duration doesn't do YAML or JSON at all.

Directories

Path Synopsis
cli
ann
output
Package output provides structured output for *cobra.Command.
Package output provides structured output for *cobra.Command.
dns
k8s

Jump to

Keyboard shortcuts

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