Adds a second, optional ingress shape and fixes the download target of qBittorrent. The existing per-app-host shape stays the default and renders unchanged. Shared domain (#167) Setting ingress.host moves every app without a host of its own into one Ingress object with one path each (<app>.ingress.path, default /<app>), one TLS secret and therefore one DNS name and one certificate for the whole stack. A per-app host always wins and keeps its own object, so mixed operation needs no extra values. The apps differ in what they can do under a sub-path, and the chart follows that rather than pretending they are alike: - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but only when their own env block does not already carry the variable. - bazarr has no environment variable for its base path and stays one documented manual step (Settings -> General -> Base URL). - qbittorrent has no base path at all and renders a second object on the same host with a prefix-stripping rewrite. It repeats the same TLS secret name and deliberately omits the cert-manager annotation, so both objects state their own TLS while only one certificate is requested. - seerr has no base path either and cannot be rewritten, so it is pinned to "/" and a fail refuses any other path. A second fail refuses two apps claiming the same path. qBittorrent save path (#166) Without Session\DefaultSavePath qBittorrent writes to its built-in default /config/Downloads - the 1Gi config volume. It fills within minutes, every torrent goes to "errored" and qBittorrent can no longer write its own metadata to BT_backup, so a restart in that state costs the torrent list. The init container (renamed qbittorrent-config-interface -> qbittorrent-config-seed, since it no longer only sets the interface) now seeds qbittorrent.savePath, default /downloads. Unlike the interface binding it is inserted ONLY when the key is absent and never replaced: a path changed in the WebUI is a legitimate operator decision and must survive a restart. This is the reason for the major bump: an installation that never set the path writes somewhere else after the upgrade. Documentation NOTES.txt and the media block in values.yaml now spell out why a shared claim with three subPaths does not produce hardlinks: the kernel compares the vfsmount in do_linkat, not the superblock, so two bind mounts of one filesystem are EXDEV - while stat reports the same device number on both sides and suggests the opposite. The layout that would work (one claim, one mount, categories as sub-directories) is named as a possibility; the chart cannot express it today. README.md gains the shared domain as the second admissible ingress shape of the style guideline.
752 lines
21 KiB
JSON
752 lines
21 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
"title": "freesailarr values",
|
|
"description": "Values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. The objects that go verbatim into the pod spec - probes, security contexts, free-form annotations, global - stay open, because the chart does not interpret their contents. The hardening, probe and resources blocks additionally accept null: an override may erase them, and the chart then falls back to its own defaults (issue #99) rather than rendering a container without hardening, without a probe or without resource accounting.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"global": {
|
|
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
|
"type": "object"
|
|
},
|
|
"scaleDown": {
|
|
"type": "boolean"
|
|
},
|
|
"replicas": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"secrets": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"gluetunSecretRef": {
|
|
"description": "op:// item path for the gluetun VPN credentials. The item must carry the fields wireguard-private-key and wireguard-addresses; the template reads exactly those names.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"puid": {
|
|
"description": "User the linuxserver.io containers drop to after their s6-overlay init, and runAsUser for seerr and recyclarr.",
|
|
"type": "integer"
|
|
},
|
|
"pgid": {
|
|
"type": "integer"
|
|
},
|
|
"timezone": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/podSecurityContextBlock"
|
|
},
|
|
"storage": {
|
|
"description": "StorageClass for the config volumeClaimTemplates. Deliberately NOT null-safe: an erased block yields an invalid claim template, which fails loudly, and issue #99 covers only the silent class.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"storageClass": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"media": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"downloads": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"movies": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"tv": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ingress": {
|
|
"description": "Ingress defaults. Two shapes: one host per app (each app carries its own host), or one shared domain with sub-paths (ingress.host set, per-app hosts empty). A per-app host always wins, so both mix.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"description": "Master switch; false suppresses every app ingress regardless of the per-app toggles.",
|
|
"type": "boolean"
|
|
},
|
|
"host": {
|
|
"description": "Shared domain for the sub-path mode. Empty or null means the default: every app on a host of its own.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"clusterIssuer": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"className": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"annotations": {
|
|
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
|
"type": "object"
|
|
}
|
|
}
|
|
},
|
|
"gluetun": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"vpn": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"serviceProvider": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"type": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"serverCities": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"serverCountries": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"dnsUpstreamResolvers": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"firewallOutboundSubnets": {
|
|
"description": "Comma-separated CIDRs the gluetun firewall allows outbound outside the tunnel. Set to the cluster pod and service CIDRs.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"qbittorrent": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"networkInterface": {
|
|
"description": "Interface the BitTorrent session binds to (gluetun's tun device). Enforced on every start, because a deviating value would be a leak. null or \"\" leaves the binding unset - which lets libtorrent announce past the tunnel.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"savePath": {
|
|
"description": "Default save path for new torrents, seeded into the persisted config ONLY when the key is absent - never replaced, so a path changed in the WebUI survives a restart. null or \"\" opts out and leaves qBittorrent on its built-in default /config/Downloads, i.e. the small config volume.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"configInit": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"prowlarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"flaresolverr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"radarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"recyclarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"sonarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"seerr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"bazarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"definitions": {
|
|
"image": {
|
|
"description": "Every image in this chart is pinned here, gluetun included; no tag comes from appVersion. This is also what puts them all under Renovate's helm-values manager, which reads exactly these two keys.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"repository": {
|
|
"type": "string"
|
|
},
|
|
"tag": {
|
|
"type": [
|
|
"string",
|
|
"number"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"appIngress": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"host": {
|
|
"description": "Host of this app's own Ingress object. Required while this app's ingress is enabled AND the shared ingress.host is not set; never demanded when this app's ingress is off, and never demanded for an app served under the shared domain. Setting it always wins over the shared domain.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"path": {
|
|
"description": "Sub-path under the shared ingress.host, e.g. /sonarr. Read only in the shared-domain mode and ignored while this app has a host of its own. Null or empty falls back to /<app>, and to / for seerr, which cannot be served anywhere else.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"clusterIssuer": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"className": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"annotations": {
|
|
"description": "Overrides the global ingress annotations for this app; free-form, so kept open.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"appStorage": {
|
|
"description": "Size of this app's config volumeClaimTemplate. Not null-safe on purpose: an erased size yields an invalid claim template and fails loudly.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"size": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"resources": {
|
|
"description": "May be null / empty: the chart then falls back to this container's own default requests and computed limits (issue #99).",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"limitFactor": {
|
|
"type": "number",
|
|
"exclusiveMinimum": 0
|
|
},
|
|
"requests": {
|
|
"$ref": "#/definitions/resourceList"
|
|
},
|
|
"limits": {
|
|
"$ref": "#/definitions/resourceList"
|
|
}
|
|
}
|
|
},
|
|
"resourceList": {
|
|
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"cpu": {
|
|
"type": [
|
|
"string",
|
|
"number",
|
|
"null"
|
|
]
|
|
},
|
|
"memory": {
|
|
"type": [
|
|
"string",
|
|
"number",
|
|
"null"
|
|
]
|
|
},
|
|
"ephemeral-storage": {
|
|
"type": [
|
|
"string",
|
|
"number",
|
|
"null"
|
|
]
|
|
},
|
|
"ephemeralStorage": {
|
|
"type": [
|
|
"string",
|
|
"number",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"type": [
|
|
"array",
|
|
"null"
|
|
],
|
|
"items": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": [
|
|
"name"
|
|
],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"value": {
|
|
"type": "string"
|
|
},
|
|
"valueFrom": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"secretKeyRef": {
|
|
"$ref": "#/definitions/keyRef"
|
|
},
|
|
"configMapKeyRef": {
|
|
"$ref": "#/definitions/keyRef"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"keyRef": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": [
|
|
"name",
|
|
"key"
|
|
],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"key": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"podSecurityContextBlock": {
|
|
"description": "May be null / empty at any level: the chart then falls back to its pod hardening defaults (issue #99), seccompProfile included.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"pod": {
|
|
"description": "Merged onto the chart's pod hardening defaults; kept open because the chart does not interpret the individual fields.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"containerSecurityContext": {
|
|
"description": "Merged onto THIS container's hardening defaults; kept open because the chart does not interpret the individual fields. May be null / empty, in which case those defaults apply (issue #99). The fallbacks reproduce this chart's deliberate deviations - the linuxserver.io containers start as root and keep a writable root filesystem so their s6 init can drop to PUID/PGID, and restoring the 'clean' standard there would leave them on the image's uid 911.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
},
|
|
"probe": {
|
|
"description": "Merged onto this container's probe defaults; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The defaults carry their context - path and port, and recyclarr's exec command, since it serves no HTTP port.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
}
|