2019-01-01 08:23:01 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
2019-01-22 21:14:58 +00:00
|
|
|
"bufio"
|
2019-01-01 08:23:01 +00:00
|
|
|
"math/rand"
|
2019-01-22 21:14:58 +00:00
|
|
|
"os"
|
2019-01-09 16:54:15 +00:00
|
|
|
"path/filepath"
|
2019-01-22 21:14:58 +00:00
|
|
|
"strings"
|
2019-01-01 08:23:01 +00:00
|
|
|
"time"
|
|
|
|
|
2021-01-22 13:08:08 +00:00
|
|
|
"github.com/containerd/cgroups"
|
|
|
|
cgroupsv2 "github.com/containerd/cgroups/v2"
|
2019-03-08 22:47:44 +00:00
|
|
|
"github.com/opencontainers/runc/libcontainer/system"
|
2019-01-09 16:54:15 +00:00
|
|
|
"github.com/rancher/k3s/pkg/daemons/config"
|
2020-04-27 17:09:58 +00:00
|
|
|
"github.com/rancher/k3s/pkg/daemons/executor"
|
2021-04-21 22:56:20 +00:00
|
|
|
"github.com/rancher/k3s/pkg/util"
|
2020-12-09 20:32:27 +00:00
|
|
|
"github.com/rancher/k3s/pkg/version"
|
2019-01-01 08:23:01 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2021-03-02 08:57:40 +00:00
|
|
|
"golang.org/x/sys/unix"
|
2019-01-22 21:14:58 +00:00
|
|
|
"k8s.io/apimachinery/pkg/util/net"
|
2019-04-08 17:53:52 +00:00
|
|
|
"k8s.io/component-base/logs"
|
2019-04-26 22:02:30 +00:00
|
|
|
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
2019-03-08 22:47:44 +00:00
|
|
|
|
2019-12-12 01:23:55 +00:00
|
|
|
_ "k8s.io/component-base/metrics/prometheus/restclient" // for client metric registration
|
|
|
|
_ "k8s.io/component-base/metrics/prometheus/version" // for version metric registration
|
2019-01-01 08:23:01 +00:00
|
|
|
)
|
|
|
|
|
2021-01-21 01:03:22 +00:00
|
|
|
const unixPrefix = "unix://"
|
|
|
|
|
2019-01-01 08:23:01 +00:00
|
|
|
func Agent(config *config.Agent) error {
|
|
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
|
|
|
2019-11-05 09:45:07 +00:00
|
|
|
logs.InitLogs()
|
|
|
|
defer logs.FlushLogs()
|
2020-04-27 17:09:58 +00:00
|
|
|
if err := startKubelet(config); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-27 16:31:25 +00:00
|
|
|
|
|
|
|
if !config.DisableKubeProxy {
|
|
|
|
return startKubeProxy(config)
|
|
|
|
}
|
2019-01-01 08:23:01 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-27 16:31:25 +00:00
|
|
|
func startKubeProxy(cfg *config.Agent) error {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap := map[string]string{
|
2021-05-17 20:30:55 +00:00
|
|
|
"proxy-mode": "iptables",
|
|
|
|
"healthz-bind-address": "127.0.0.1",
|
|
|
|
"kubeconfig": cfg.KubeConfigKubeProxy,
|
|
|
|
"cluster-cidr": util.JoinIPNets(cfg.ClusterCIDRs),
|
|
|
|
"conntrack-max-per-core": "0",
|
|
|
|
"conntrack-tcp-timeout-established": "0s",
|
|
|
|
"conntrack-tcp-timeout-close-wait": "0s",
|
2019-04-05 00:43:00 +00:00
|
|
|
}
|
2019-11-05 09:45:07 +00:00
|
|
|
if cfg.NodeName != "" {
|
|
|
|
argsMap["hostname-override"] = cfg.NodeName
|
|
|
|
}
|
2019-01-01 08:23:01 +00:00
|
|
|
|
2019-11-05 09:45:07 +00:00
|
|
|
args := config.GetArgsList(argsMap, cfg.ExtraKubeProxyArgs)
|
2020-04-27 17:09:58 +00:00
|
|
|
logrus.Infof("Running kube-proxy %s", config.ArgString(args))
|
|
|
|
return executor.KubeProxy(args)
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-27 17:09:58 +00:00
|
|
|
func startKubelet(cfg *config.Agent) error {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap := map[string]string{
|
|
|
|
"healthz-bind-address": "127.0.0.1",
|
|
|
|
"read-only-port": "0",
|
2019-04-12 06:06:35 +00:00
|
|
|
"cluster-domain": cfg.ClusterDomain,
|
2019-05-29 18:53:51 +00:00
|
|
|
"kubeconfig": cfg.KubeConfigKubelet,
|
2019-04-05 00:43:00 +00:00
|
|
|
"eviction-hard": "imagefs.available<5%,nodefs.available<5%",
|
|
|
|
"eviction-minimum-reclaim": "imagefs.available=10%,nodefs.available=10%",
|
|
|
|
"fail-swap-on": "false",
|
|
|
|
//"cgroup-root": "/k3s",
|
2019-04-10 18:09:38 +00:00
|
|
|
"cgroup-driver": "cgroupfs",
|
|
|
|
"authentication-token-webhook": "true",
|
2019-08-27 04:36:56 +00:00
|
|
|
"anonymous-auth": "false",
|
2019-04-26 22:02:30 +00:00
|
|
|
"authorization-mode": modes.ModeWebhook,
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2020-04-27 16:41:57 +00:00
|
|
|
if cfg.PodManifests != "" && argsMap["pod-manifest-path"] == "" {
|
|
|
|
argsMap["pod-manifest-path"] = cfg.PodManifests
|
|
|
|
}
|
|
|
|
if err := os.MkdirAll(argsMap["pod-manifest-path"], 0755); err != nil {
|
|
|
|
logrus.Errorf("Failed to mkdir %s: %v", argsMap["pod-manifest-path"], err)
|
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if cfg.RootDir != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["root-dir"] = cfg.RootDir
|
|
|
|
argsMap["cert-dir"] = filepath.Join(cfg.RootDir, "pki")
|
|
|
|
argsMap["seccomp-profile-root"] = filepath.Join(cfg.RootDir, "seccomp")
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if cfg.CNIConfDir != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["cni-conf-dir"] = cfg.CNIConfDir
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if cfg.CNIBinDir != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["cni-bin-dir"] = cfg.CNIBinDir
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-04-30 20:12:02 +00:00
|
|
|
if cfg.CNIPlugin {
|
|
|
|
argsMap["network-plugin"] = "cni"
|
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if len(cfg.ClusterDNS) > 0 {
|
2021-04-21 22:56:20 +00:00
|
|
|
argsMap["cluster-dns"] = util.JoinIPs(cfg.ClusterDNSs)
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-03-26 22:15:16 +00:00
|
|
|
if cfg.ResolvConf != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["resolv-conf"] = cfg.ResolvConf
|
2019-03-26 22:15:16 +00:00
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if cfg.RuntimeSocket != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["container-runtime"] = "remote"
|
2020-01-16 17:21:19 +00:00
|
|
|
argsMap["containerd"] = cfg.RuntimeSocket
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["serialize-image-pulls"] = "false"
|
2021-01-21 01:03:22 +00:00
|
|
|
if strings.HasPrefix(argsMap["container-runtime-endpoint"], unixPrefix) {
|
|
|
|
argsMap["container-runtime-endpoint"] = cfg.RuntimeSocket
|
|
|
|
} else {
|
|
|
|
argsMap["container-runtime-endpoint"] = unixPrefix + cfg.RuntimeSocket
|
|
|
|
}
|
2019-12-10 23:16:26 +00:00
|
|
|
} else if cfg.PauseImage != "" {
|
|
|
|
argsMap["pod-infra-container-image"] = cfg.PauseImage
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-01-09 16:54:15 +00:00
|
|
|
if cfg.ListenAddress != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["address"] = cfg.ListenAddress
|
2019-01-09 16:54:15 +00:00
|
|
|
}
|
2019-05-29 18:53:51 +00:00
|
|
|
if cfg.ClientCA != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["anonymous-auth"] = "false"
|
2019-05-29 18:53:51 +00:00
|
|
|
argsMap["client-ca-file"] = cfg.ClientCA
|
2019-04-19 18:20:34 +00:00
|
|
|
}
|
2019-05-29 18:53:51 +00:00
|
|
|
if cfg.ServingKubeletCert != "" && cfg.ServingKubeletKey != "" {
|
|
|
|
argsMap["tls-cert-file"] = cfg.ServingKubeletCert
|
|
|
|
argsMap["tls-private-key-file"] = cfg.ServingKubeletKey
|
2019-01-09 16:54:15 +00:00
|
|
|
}
|
|
|
|
if cfg.NodeName != "" {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["hostname-override"] = cfg.NodeName
|
2019-01-09 16:54:15 +00:00
|
|
|
}
|
|
|
|
defaultIP, err := net.ChooseHostInterface()
|
|
|
|
if err != nil || defaultIP.String() != cfg.NodeIP {
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["node-ip"] = cfg.NodeIP
|
2019-01-09 16:54:15 +00:00
|
|
|
}
|
2021-03-02 08:57:40 +00:00
|
|
|
kubeletRoot, runtimeRoot, hasCFS, hasPIDs := CheckCgroups()
|
2019-03-04 05:00:47 +00:00
|
|
|
if !hasCFS {
|
2019-01-22 21:14:58 +00:00
|
|
|
logrus.Warn("Disabling CPU quotas due to missing cpu.cfs_period_us")
|
2019-04-05 00:43:00 +00:00
|
|
|
argsMap["cpu-cfs-quota"] = "false"
|
2019-01-22 21:14:58 +00:00
|
|
|
}
|
2019-04-12 23:45:59 +00:00
|
|
|
if !hasPIDs {
|
|
|
|
logrus.Warn("Disabling pod PIDs limit feature due to missing cgroup pids support")
|
|
|
|
argsMap["cgroups-per-qos"] = "false"
|
|
|
|
argsMap["enforce-node-allocatable"] = ""
|
|
|
|
argsMap["feature-gates"] = addFeatureGate(argsMap["feature-gates"], "SupportPodPidsLimit=false")
|
|
|
|
}
|
2020-12-09 19:39:33 +00:00
|
|
|
if kubeletRoot != "" {
|
|
|
|
argsMap["kubelet-cgroups"] = kubeletRoot
|
|
|
|
}
|
|
|
|
if runtimeRoot != "" {
|
|
|
|
argsMap["runtime-cgroups"] = runtimeRoot
|
2019-03-04 05:00:47 +00:00
|
|
|
}
|
2019-03-08 22:47:44 +00:00
|
|
|
if system.RunningInUserNS() {
|
2019-04-12 23:45:59 +00:00
|
|
|
argsMap["feature-gates"] = addFeatureGate(argsMap["feature-gates"], "DevicePlugins=false")
|
2019-03-08 22:47:44 +00:00
|
|
|
}
|
2019-01-01 08:23:01 +00:00
|
|
|
|
2019-05-07 23:47:07 +00:00
|
|
|
argsMap["node-labels"] = strings.Join(cfg.NodeLabels, ",")
|
|
|
|
if len(cfg.NodeTaints) > 0 {
|
|
|
|
argsMap["register-with-taints"] = strings.Join(cfg.NodeTaints, ",")
|
|
|
|
}
|
2019-10-15 21:17:26 +00:00
|
|
|
if !cfg.DisableCCM {
|
|
|
|
argsMap["cloud-provider"] = "external"
|
|
|
|
}
|
|
|
|
|
2021-05-10 22:58:41 +00:00
|
|
|
if ImageCredProvAvailable(cfg) {
|
|
|
|
logrus.Infof("Kubelet image credential provider bin dir and configuration file found.")
|
|
|
|
argsMap["feature-gates"] = addFeatureGate(argsMap["feature-gates"], "KubeletCredentialProviders=true")
|
|
|
|
argsMap["image-credential-provider-bin-dir"] = cfg.ImageCredProvBinDir
|
|
|
|
argsMap["image-credential-provider-config"] = cfg.ImageCredProvConfig
|
|
|
|
}
|
|
|
|
|
2019-10-19 10:18:51 +00:00
|
|
|
if cfg.Rootless {
|
2021-03-02 08:57:40 +00:00
|
|
|
// "/sys/fs/cgroup" is namespaced
|
|
|
|
cgroupfsWritable := unix.Access("/sys/fs/cgroup", unix.W_OK) == nil
|
|
|
|
if hasCFS && hasPIDs && cgroupfsWritable {
|
|
|
|
logrus.Info("cgroup v2 controllers are delegated for rootless.")
|
|
|
|
// cgroupfs v2, delegated for rootless by systemd
|
|
|
|
argsMap["cgroup-driver"] = "cgroupfs"
|
|
|
|
} else {
|
|
|
|
logrus.Warn("cgroup v2 controllers are not delegated for rootless. Setting cgroup driver to \"none\".")
|
|
|
|
// flags are from https://github.com/rootless-containers/usernetes/blob/v20190826.0/boot/kubelet.sh
|
|
|
|
argsMap["cgroup-driver"] = "none"
|
|
|
|
argsMap["feature-gates=SupportNoneCgroupDriver"] = "true"
|
|
|
|
argsMap["cgroups-per-qos"] = "false"
|
|
|
|
argsMap["enforce-node-allocatable"] = ""
|
|
|
|
}
|
2019-10-19 10:18:51 +00:00
|
|
|
}
|
|
|
|
|
2020-07-20 23:31:56 +00:00
|
|
|
if cfg.ProtectKernelDefaults {
|
|
|
|
argsMap["protect-kernel-defaults"] = "true"
|
|
|
|
}
|
|
|
|
|
2019-03-08 22:47:44 +00:00
|
|
|
args := config.GetArgsList(argsMap, cfg.ExtraKubeletArgs)
|
2020-04-27 17:09:58 +00:00
|
|
|
logrus.Infof("Running kubelet %s", config.ArgString(args))
|
2019-01-01 08:23:01 +00:00
|
|
|
|
2020-04-27 17:09:58 +00:00
|
|
|
return executor.Kubelet(args)
|
2019-01-01 08:23:01 +00:00
|
|
|
}
|
2019-01-22 21:14:58 +00:00
|
|
|
|
2019-04-12 23:45:59 +00:00
|
|
|
func addFeatureGate(current, new string) string {
|
|
|
|
if current == "" {
|
|
|
|
return new
|
|
|
|
}
|
|
|
|
return current + "," + new
|
|
|
|
}
|
|
|
|
|
2021-05-10 22:58:41 +00:00
|
|
|
// ImageCredProvAvailable checks to see if the kubelet image credential provider bin dir and config
|
|
|
|
// files exist and are of the correct types. This is exported so that it may be used by downstream projects.
|
|
|
|
func ImageCredProvAvailable(cfg *config.Agent) bool {
|
|
|
|
if info, err := os.Stat(cfg.ImageCredProvBinDir); err != nil || !info.IsDir() {
|
|
|
|
logrus.Debugf("Kubelet image credential provider bin directory check failed: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if info, err := os.Stat(cfg.ImageCredProvConfig); err != nil || info.IsDir() {
|
|
|
|
logrus.Debugf("Kubelet image credential provider config file check failed: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-03-02 08:57:40 +00:00
|
|
|
func CheckCgroups() (kubeletRoot, runtimeRoot string, hasCFS, hasPIDs bool) {
|
2021-02-10 00:12:58 +00:00
|
|
|
cgroupsModeV2 := cgroups.Mode() == cgroups.Unified
|
2019-01-22 21:14:58 +00:00
|
|
|
|
2021-02-10 00:12:58 +00:00
|
|
|
// For Unified (v2) cgroups we can directly check to see what controllers are mounted
|
|
|
|
// under the unified hierarchy.
|
|
|
|
if cgroupsModeV2 {
|
2021-01-22 13:08:08 +00:00
|
|
|
m, err := cgroupsv2.LoadManager("/sys/fs/cgroup", "/")
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false, false
|
|
|
|
}
|
|
|
|
controllers, err := m.Controllers()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false, false
|
|
|
|
}
|
2021-02-10 00:12:58 +00:00
|
|
|
// Intentionally using an expressionless switch to match the logic below
|
|
|
|
for _, controller := range controllers {
|
|
|
|
switch {
|
|
|
|
case controller == "cpu":
|
2021-01-22 13:08:08 +00:00
|
|
|
hasCFS = true
|
2021-02-10 00:12:58 +00:00
|
|
|
case controller == "pids":
|
2021-01-22 13:08:08 +00:00
|
|
|
hasPIDs = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-10 00:12:58 +00:00
|
|
|
f, err := os.Open("/proc/self/cgroup")
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false, false
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2019-01-22 21:14:58 +00:00
|
|
|
scan := bufio.NewScanner(f)
|
|
|
|
for scan.Scan() {
|
|
|
|
parts := strings.Split(scan.Text(), ":")
|
|
|
|
if len(parts) < 3 {
|
|
|
|
continue
|
|
|
|
}
|
2021-02-10 00:12:58 +00:00
|
|
|
controllers := strings.Split(parts[1], ",")
|
|
|
|
// For v1 or hybrid, controller can be a single value {"blkio"}, or a comounted set {"cpu","cpuacct"}
|
|
|
|
// For v2, controllers = {""} (only contains a single empty string)
|
|
|
|
for _, controller := range controllers {
|
|
|
|
switch {
|
|
|
|
case controller == "name=systemd" || cgroupsModeV2:
|
2020-12-09 19:39:33 +00:00
|
|
|
// If we detect that we are running under a `.scope` unit with systemd
|
|
|
|
// we can assume we are being directly invoked from the command line
|
|
|
|
// and thus need to set our kubelet root to something out of the context
|
|
|
|
// of `/user.slice` to ensure that `CPUAccounting` and `MemoryAccounting`
|
|
|
|
// are enabled, as they are generally disabled by default for `user.slice`
|
|
|
|
// Note that we are not setting the `runtimeRoot` as if we are running with
|
|
|
|
// `--docker`, we will inadvertently move the cgroup `dockerd` lives in
|
|
|
|
// which is not ideal and causes dockerd to become unmanageable by systemd.
|
|
|
|
last := parts[len(parts)-1]
|
|
|
|
i := strings.LastIndex(last, ".scope")
|
|
|
|
if i > 0 {
|
2020-12-09 20:32:27 +00:00
|
|
|
kubeletRoot = "/" + version.Program
|
2020-12-09 19:39:33 +00:00
|
|
|
}
|
2021-02-10 00:12:58 +00:00
|
|
|
case controller == "cpu":
|
|
|
|
// It is common for this to show up multiple times in /sys/fs/cgroup if the controllers are comounted:
|
|
|
|
// as "cpu" and "cpuacct", symlinked to the actual hierarchy at "cpu,cpuacct". Unfortunately the order
|
|
|
|
// listed in /proc/self/cgroups may not be the same order used in /sys/fs/cgroup, so this check
|
|
|
|
// can fail if we use the comma-separated name. Instead, we check for the controller using the symlink.
|
|
|
|
p := filepath.Join("/sys/fs/cgroup", controller, parts[2], "cpu.cfs_period_us")
|
|
|
|
if _, err := os.Stat(p); err == nil {
|
|
|
|
hasCFS = true
|
|
|
|
}
|
|
|
|
case controller == "pids":
|
|
|
|
hasPIDs = true
|
2020-12-07 03:23:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-10 00:12:58 +00:00
|
|
|
// If we're running with v1 and didn't find a scope assigned by systemd, we need to create our own root cgroup to avoid
|
|
|
|
// just inheriting from the parent process. The kubelet will take care of moving us into it when we start it up later.
|
2020-12-09 19:39:33 +00:00
|
|
|
if kubeletRoot == "" {
|
|
|
|
// Examine process ID 1 to see if there is a cgroup assigned to it.
|
|
|
|
// When we are not in a container, process 1 is likely to be systemd or some other service manager.
|
|
|
|
// It either lives at `/` or `/init.scope` according to https://man7.org/linux/man-pages/man7/systemd.special.7.html
|
|
|
|
// When containerized, process 1 will be generally be in a cgroup, otherwise, we may be running in
|
|
|
|
// a host PID scenario but we don't support this.
|
|
|
|
g, err := os.Open("/proc/1/cgroup")
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false, false
|
2020-12-07 03:23:44 +00:00
|
|
|
}
|
2020-12-09 19:39:33 +00:00
|
|
|
defer g.Close()
|
|
|
|
scan = bufio.NewScanner(g)
|
|
|
|
for scan.Scan() {
|
|
|
|
parts := strings.Split(scan.Text(), ":")
|
|
|
|
if len(parts) < 3 {
|
|
|
|
continue
|
|
|
|
}
|
2021-02-10 00:12:58 +00:00
|
|
|
controllers := strings.Split(parts[1], ",")
|
|
|
|
// For v1 or hybrid, controller can be a single value {"blkio"}, or a comounted set {"cpu","cpuacct"}
|
|
|
|
// For v2, controllers = {""} (only contains a single empty string)
|
|
|
|
for _, controller := range controllers {
|
|
|
|
switch {
|
|
|
|
case controller == "name=systemd" || cgroupsModeV2:
|
2020-12-09 19:39:33 +00:00
|
|
|
last := parts[len(parts)-1]
|
|
|
|
if last != "/" && last != "/init.scope" {
|
2020-12-09 20:32:27 +00:00
|
|
|
kubeletRoot = "/" + version.Program
|
|
|
|
runtimeRoot = "/" + version.Program
|
2020-12-09 19:39:33 +00:00
|
|
|
}
|
2019-01-22 21:14:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-12-09 19:39:33 +00:00
|
|
|
return kubeletRoot, runtimeRoot, hasCFS, hasPIDs
|
2019-01-22 21:14:58 +00:00
|
|
|
}
|