mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-13 02:33:34 +00:00
vendor files
This commit is contained in:
78
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/BUILD
generated
vendored
Normal file
78
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/BUILD
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"empty_dir.go",
|
||||
"empty_dir_unsupported.go",
|
||||
] + select({
|
||||
"@io_bazel_rules_go//go/platform:linux_amd64": [
|
||||
"empty_dir_linux.go",
|
||||
],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
importpath = "k8s.io/kubernetes/pkg/volume/empty_dir",
|
||||
deps = [
|
||||
"//pkg/apis/core/v1/helper:go_default_library",
|
||||
"//pkg/util/mount:go_default_library",
|
||||
"//pkg/util/strings:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//pkg/volume/util:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
] + select({
|
||||
"@io_bazel_rules_go//go/platform:linux_amd64": [
|
||||
"//vendor/golang.org/x/sys/unix:go_default_library",
|
||||
],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = select({
|
||||
"@io_bazel_rules_go//go/platform:linux_amd64": [
|
||||
"empty_dir_test.go",
|
||||
],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
importpath = "k8s.io/kubernetes/pkg/volume/empty_dir",
|
||||
library = ":go_default_library",
|
||||
deps = select({
|
||||
"@io_bazel_rules_go//go/platform:linux_amd64": [
|
||||
"//pkg/util/mount:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//pkg/volume/testing:go_default_library",
|
||||
"//pkg/volume/util:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/client-go/util/testing:go_default_library",
|
||||
],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
14
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/OWNERS
generated
vendored
Normal file
14
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/OWNERS
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
approvers:
|
||||
- pmorie
|
||||
- saad-ali
|
||||
- thockin
|
||||
- matchstick
|
||||
reviewers:
|
||||
- ivan4th
|
||||
- rata
|
||||
- sjenning
|
||||
- saad-ali
|
||||
- jsafrane
|
||||
- rootfs
|
||||
- jingxu97
|
||||
- msau42
|
19
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/doc.go
generated
vendored
Normal file
19
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/doc.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package empty_dir contains the internal representation of emptyDir
|
||||
// volumes.
|
||||
package empty_dir // import "k8s.io/kubernetes/pkg/volume/empty_dir"
|
440
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir.go
generated
vendored
Normal file
440
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir.go
generated
vendored
Normal file
@ -0,0 +1,440 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package empty_dir
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
stringsutil "k8s.io/kubernetes/pkg/util/strings"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
||||
)
|
||||
|
||||
// TODO: in the near future, this will be changed to be more restrictive
|
||||
// and the group will be set to allow containers to use emptyDir volumes
|
||||
// from the group attribute.
|
||||
//
|
||||
// http://issue.k8s.io/2630
|
||||
const perm os.FileMode = 0777
|
||||
|
||||
// This is the primary entrypoint for volume plugins.
|
||||
func ProbeVolumePlugins() []volume.VolumePlugin {
|
||||
return []volume.VolumePlugin{
|
||||
&emptyDirPlugin{nil},
|
||||
}
|
||||
}
|
||||
|
||||
type emptyDirPlugin struct {
|
||||
host volume.VolumeHost
|
||||
}
|
||||
|
||||
var _ volume.VolumePlugin = &emptyDirPlugin{}
|
||||
|
||||
const (
|
||||
emptyDirPluginName = "kubernetes.io/empty-dir"
|
||||
hugePagesPageSizeMountOption = "pagesize"
|
||||
)
|
||||
|
||||
func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
|
||||
return host.GetPodVolumeDir(uid, stringsutil.EscapeQualifiedNameForDisk(emptyDirPluginName), volName)
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) Init(host volume.VolumeHost) error {
|
||||
plugin.host = host
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) GetPluginName() string {
|
||||
return emptyDirPluginName
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
|
||||
volumeSource, _ := getVolumeSource(spec)
|
||||
if volumeSource == nil {
|
||||
return "", fmt.Errorf("Spec does not reference an EmptyDir volume type")
|
||||
}
|
||||
|
||||
// Return user defined volume name, since this is an ephemeral volume type
|
||||
return spec.Name(), nil
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) CanSupport(spec *volume.Spec) bool {
|
||||
if spec.Volume != nil && spec.Volume.EmptyDir != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) RequiresRemount() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) SupportsMountOption() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) SupportsBulkVolumeVerification() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) {
|
||||
return plugin.newMounterInternal(spec, pod, plugin.host.GetMounter(plugin.GetPluginName()), &realMountDetector{plugin.host.GetMounter(plugin.GetPluginName())}, opts)
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) newMounterInternal(spec *volume.Spec, pod *v1.Pod, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions) (volume.Mounter, error) {
|
||||
medium := v1.StorageMediumDefault
|
||||
|
||||
if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir.
|
||||
medium = spec.Volume.EmptyDir.Medium
|
||||
}
|
||||
|
||||
return &emptyDir{
|
||||
pod: pod,
|
||||
volName: spec.Name(),
|
||||
medium: medium,
|
||||
mounter: mounter,
|
||||
mountDetector: mountDetector,
|
||||
plugin: plugin,
|
||||
MetricsProvider: volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) {
|
||||
// Inject real implementations here, test through the internal function.
|
||||
return plugin.newUnmounterInternal(volName, podUID, plugin.host.GetMounter(plugin.GetPluginName()), &realMountDetector{plugin.host.GetMounter(plugin.GetPluginName())})
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) newUnmounterInternal(volName string, podUID types.UID, mounter mount.Interface, mountDetector mountDetector) (volume.Unmounter, error) {
|
||||
ed := &emptyDir{
|
||||
pod: &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: podUID}},
|
||||
volName: volName,
|
||||
medium: v1.StorageMediumDefault, // might be changed later
|
||||
mounter: mounter,
|
||||
mountDetector: mountDetector,
|
||||
plugin: plugin,
|
||||
MetricsProvider: volume.NewMetricsDu(getPath(podUID, volName, plugin.host)),
|
||||
}
|
||||
return ed, nil
|
||||
}
|
||||
|
||||
func (plugin *emptyDirPlugin) ConstructVolumeSpec(volName, mountPath string) (*volume.Spec, error) {
|
||||
emptyDirVolume := &v1.Volume{
|
||||
Name: volName,
|
||||
VolumeSource: v1.VolumeSource{
|
||||
EmptyDir: &v1.EmptyDirVolumeSource{},
|
||||
},
|
||||
}
|
||||
return volume.NewSpecFromVolume(emptyDirVolume), nil
|
||||
}
|
||||
|
||||
// mountDetector abstracts how to find what kind of mount a path is backed by.
|
||||
type mountDetector interface {
|
||||
// GetMountMedium determines what type of medium a given path is backed
|
||||
// by and whether that path is a mount point. For example, if this
|
||||
// returns (mediumMemory, false, nil), the caller knows that the path is
|
||||
// on a memory FS (tmpfs on Linux) but is not the root mountpoint of
|
||||
// that tmpfs.
|
||||
GetMountMedium(path string) (storageMedium, bool, error)
|
||||
}
|
||||
|
||||
type storageMedium int
|
||||
|
||||
const (
|
||||
mediumUnknown storageMedium = 0 // assume anything we don't explicitly handle is this
|
||||
mediumMemory storageMedium = 1 // memory (e.g. tmpfs on linux)
|
||||
mediumHugepages storageMedium = 2 // hugepages
|
||||
)
|
||||
|
||||
// EmptyDir volumes are temporary directories exposed to the pod.
|
||||
// These do not persist beyond the lifetime of a pod.
|
||||
type emptyDir struct {
|
||||
pod *v1.Pod
|
||||
volName string
|
||||
medium v1.StorageMedium
|
||||
mounter mount.Interface
|
||||
mountDetector mountDetector
|
||||
plugin *emptyDirPlugin
|
||||
volume.MetricsProvider
|
||||
}
|
||||
|
||||
func (ed *emptyDir) GetAttributes() volume.Attributes {
|
||||
return volume.Attributes{
|
||||
ReadOnly: false,
|
||||
Managed: true,
|
||||
SupportsSELinux: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Checks prior to mount operations to verify that the required components (binaries, etc.)
|
||||
// to mount the volume are available on the underlying node.
|
||||
// If not, it returns an error
|
||||
func (b *emptyDir) CanMount() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUp creates new directory.
|
||||
func (ed *emptyDir) SetUp(fsGroup *int64) error {
|
||||
return ed.SetUpAt(ed.GetPath(), fsGroup)
|
||||
}
|
||||
|
||||
// SetUpAt creates new directory.
|
||||
func (ed *emptyDir) SetUpAt(dir string, fsGroup *int64) error {
|
||||
notMnt, err := ed.mounter.IsLikelyNotMountPoint(dir)
|
||||
// Getting an os.IsNotExist err from is a contingency; the directory
|
||||
// may not exist yet, in which case, setup should run.
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the plugin readiness file is present for this volume, and the
|
||||
// storage medium is the default, then the volume is ready. If the
|
||||
// medium is memory, and a mountpoint is present, then the volume is
|
||||
// ready.
|
||||
if volumeutil.IsReady(ed.getMetaDir()) {
|
||||
if ed.medium == v1.StorageMediumMemory && !notMnt {
|
||||
return nil
|
||||
} else if ed.medium == v1.StorageMediumDefault {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch ed.medium {
|
||||
case v1.StorageMediumDefault:
|
||||
err = ed.setupDir(dir)
|
||||
case v1.StorageMediumMemory:
|
||||
err = ed.setupTmpfs(dir)
|
||||
case v1.StorageMediumHugePages:
|
||||
err = ed.setupHugepages(dir)
|
||||
default:
|
||||
err = fmt.Errorf("unknown storage medium %q", ed.medium)
|
||||
}
|
||||
|
||||
volume.SetVolumeOwnership(ed, fsGroup)
|
||||
|
||||
if err == nil {
|
||||
volumeutil.SetReady(ed.getMetaDir())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// setupTmpfs creates a tmpfs mount at the specified directory.
|
||||
func (ed *emptyDir) setupTmpfs(dir string) error {
|
||||
if ed.mounter == nil {
|
||||
return fmt.Errorf("memory storage requested, but mounter is nil")
|
||||
}
|
||||
if err := ed.setupDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
// Make SetUp idempotent.
|
||||
medium, isMnt, err := ed.mountDetector.GetMountMedium(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If the directory is a mountpoint with medium memory, there is no
|
||||
// work to do since we are already in the desired state.
|
||||
if isMnt && medium == mediumMemory {
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.V(3).Infof("pod %v: mounting tmpfs for volume %v", ed.pod.UID, ed.volName)
|
||||
return ed.mounter.Mount("tmpfs", dir, "tmpfs", nil /* options */)
|
||||
}
|
||||
|
||||
// setupHugepages creates a hugepage mount at the specified directory.
|
||||
func (ed *emptyDir) setupHugepages(dir string) error {
|
||||
if ed.mounter == nil {
|
||||
return fmt.Errorf("memory storage requested, but mounter is nil")
|
||||
}
|
||||
if err := ed.setupDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
// Make SetUp idempotent.
|
||||
medium, isMnt, err := ed.mountDetector.GetMountMedium(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If the directory is a mountpoint with medium hugepages, there is no
|
||||
// work to do since we are already in the desired state.
|
||||
if isMnt && medium == mediumHugepages {
|
||||
return nil
|
||||
}
|
||||
|
||||
pageSizeMountOption, err := getPageSizeMountOptionFromPod(ed.pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
glog.V(3).Infof("pod %v: mounting hugepages for volume %v", ed.pod.UID, ed.volName)
|
||||
return ed.mounter.Mount("nodev", dir, "hugetlbfs", []string{pageSizeMountOption})
|
||||
}
|
||||
|
||||
// getPageSizeMountOptionFromPod retrieves pageSize mount option from Pod's resources
|
||||
// and validates pageSize options in all containers of given Pod.
|
||||
func getPageSizeMountOptionFromPod(pod *v1.Pod) (string, error) {
|
||||
pageSizeFound := false
|
||||
pageSize := resource.Quantity{}
|
||||
// In some rare cases init containers can also consume Huge pages.
|
||||
containers := append(pod.Spec.Containers, pod.Spec.InitContainers...)
|
||||
for _, container := range containers {
|
||||
// We can take request because limit and requests must match.
|
||||
for requestName := range container.Resources.Requests {
|
||||
if v1helper.IsHugePageResourceName(requestName) {
|
||||
currentPageSize, err := v1helper.HugePageSizeFromResourceName(requestName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// PageSize for all volumes in a POD are equal, except for the first one discovered.
|
||||
if pageSizeFound && pageSize.Cmp(currentPageSize) != 0 {
|
||||
return "", fmt.Errorf("multiple pageSizes for huge pages in a single PodSpec")
|
||||
}
|
||||
pageSize = currentPageSize
|
||||
pageSizeFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pageSizeFound {
|
||||
return "", fmt.Errorf("hugePages storage requested, but there is no resource request for huge pages.")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s=%s", hugePagesPageSizeMountOption, pageSize.String()), nil
|
||||
|
||||
}
|
||||
|
||||
// setupDir creates the directory with the default permissions specified by the perm constant.
|
||||
func (ed *emptyDir) setupDir(dir string) error {
|
||||
// Create the directory if it doesn't already exist.
|
||||
if err := os.MkdirAll(dir, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// stat the directory to read permission bits
|
||||
fileinfo, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileinfo.Mode().Perm() != perm.Perm() {
|
||||
// If the permissions on the created directory are wrong, the
|
||||
// kubelet is probably running with a umask set. In order to
|
||||
// avoid clearing the umask for the entire process or locking
|
||||
// the thread, clearing the umask, creating the dir, restoring
|
||||
// the umask, and unlocking the thread, we do a chmod to set
|
||||
// the specific bits we need.
|
||||
err := os.Chmod(dir, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileinfo, err = os.Lstat(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileinfo.Mode().Perm() != perm.Perm() {
|
||||
glog.Errorf("Expected directory %q permissions to be: %s; got: %s", dir, perm.Perm(), fileinfo.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ed *emptyDir) GetPath() string {
|
||||
return getPath(ed.pod.UID, ed.volName, ed.plugin.host)
|
||||
}
|
||||
|
||||
// TearDown simply discards everything in the directory.
|
||||
func (ed *emptyDir) TearDown() error {
|
||||
return ed.TearDownAt(ed.GetPath())
|
||||
}
|
||||
|
||||
// TearDownAt simply discards everything in the directory.
|
||||
func (ed *emptyDir) TearDownAt(dir string) error {
|
||||
if pathExists, pathErr := volumeutil.PathExists(dir); pathErr != nil {
|
||||
return fmt.Errorf("Error checking if path exists: %v", pathErr)
|
||||
} else if !pathExists {
|
||||
glog.Warningf("Warning: Unmount skipped because path does not exist: %v", dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Figure out the medium.
|
||||
medium, isMnt, err := ed.mountDetector.GetMountMedium(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isMnt {
|
||||
if medium == mediumMemory {
|
||||
ed.medium = v1.StorageMediumMemory
|
||||
return ed.teardownTmpfsOrHugetlbfs(dir)
|
||||
} else if medium == mediumHugepages {
|
||||
ed.medium = v1.StorageMediumHugePages
|
||||
return ed.teardownTmpfsOrHugetlbfs(dir)
|
||||
}
|
||||
}
|
||||
// assume StorageMediumDefault
|
||||
return ed.teardownDefault(dir)
|
||||
}
|
||||
|
||||
func (ed *emptyDir) teardownDefault(dir string) error {
|
||||
// Renaming the directory is not required anymore because the operation executor
|
||||
// now handles duplicate operations on the same volume
|
||||
err := os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ed *emptyDir) teardownTmpfsOrHugetlbfs(dir string) error {
|
||||
if ed.mounter == nil {
|
||||
return fmt.Errorf("memory storage requested, but mounter is nil")
|
||||
}
|
||||
if err := ed.mounter.Unmount(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ed *emptyDir) getMetaDir() string {
|
||||
return path.Join(ed.plugin.host.GetPodPluginDir(ed.pod.UID, stringsutil.EscapeQualifiedNameForDisk(emptyDirPluginName)), ed.volName)
|
||||
}
|
||||
|
||||
func getVolumeSource(spec *volume.Spec) (*v1.EmptyDirVolumeSource, bool) {
|
||||
var readOnly bool
|
||||
var volumeSource *v1.EmptyDirVolumeSource
|
||||
|
||||
if spec.Volume != nil && spec.Volume.EmptyDir != nil {
|
||||
volumeSource = spec.Volume.EmptyDir
|
||||
readOnly = spec.ReadOnly
|
||||
}
|
||||
|
||||
return volumeSource, readOnly
|
||||
}
|
58
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_linux.go
generated
vendored
Normal file
58
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_linux.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package empty_dir
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
)
|
||||
|
||||
// Defined by Linux - the type number for tmpfs mounts.
|
||||
const (
|
||||
linuxTmpfsMagic = 0x01021994
|
||||
linuxHugetlbfsMagic = 0x958458f6
|
||||
)
|
||||
|
||||
// realMountDetector implements mountDetector in terms of syscalls.
|
||||
type realMountDetector struct {
|
||||
mounter mount.Interface
|
||||
}
|
||||
|
||||
func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, error) {
|
||||
glog.V(5).Infof("Determining mount medium of %v", path)
|
||||
notMnt, err := m.mounter.IsLikelyNotMountPoint(path)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("IsLikelyNotMountPoint(%q): %v", path, err)
|
||||
}
|
||||
buf := unix.Statfs_t{}
|
||||
if err := unix.Statfs(path, &buf); err != nil {
|
||||
return 0, false, fmt.Errorf("statfs(%q): %v", path, err)
|
||||
}
|
||||
|
||||
glog.V(5).Infof("Statfs_t of %v: %+v", path, buf)
|
||||
if buf.Type == linuxTmpfsMagic {
|
||||
return mediumMemory, !notMnt, nil
|
||||
} else if int64(buf.Type) == linuxHugetlbfsMagic {
|
||||
return mediumHugepages, !notMnt, nil
|
||||
}
|
||||
return mediumUnknown, !notMnt, nil
|
||||
}
|
441
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_test.go
generated
vendored
Normal file
441
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_test.go
generated
vendored
Normal file
@ -0,0 +1,441 @@
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package empty_dir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
||||
"k8s.io/kubernetes/pkg/volume/util"
|
||||
)
|
||||
|
||||
// Construct an instance of a plugin, by name.
|
||||
func makePluginUnderTest(t *testing.T, plugName, basePath string) volume.VolumePlugin {
|
||||
plugMgr := volume.VolumePluginMgr{}
|
||||
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(basePath, nil, nil))
|
||||
|
||||
plug, err := plugMgr.FindPluginByName(plugName)
|
||||
if err != nil {
|
||||
t.Errorf("Can't find the plugin by name")
|
||||
}
|
||||
return plug
|
||||
}
|
||||
|
||||
func TestCanSupport(t *testing.T) {
|
||||
tmpDir, err := utiltesting.MkTmpdir("emptydirTest")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make a temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", tmpDir)
|
||||
|
||||
if plug.GetPluginName() != "kubernetes.io/empty-dir" {
|
||||
t.Errorf("Wrong name: %s", plug.GetPluginName())
|
||||
}
|
||||
if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}}) {
|
||||
t.Errorf("Expected true")
|
||||
}
|
||||
if plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{}}}) {
|
||||
t.Errorf("Expected false")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeMountDetector struct {
|
||||
medium storageMedium
|
||||
isMount bool
|
||||
}
|
||||
|
||||
func (fake *fakeMountDetector) GetMountMedium(path string) (storageMedium, bool, error) {
|
||||
return fake.medium, fake.isMount, nil
|
||||
}
|
||||
|
||||
func TestPluginEmptyRootContext(t *testing.T) {
|
||||
doTestPlugin(t, pluginTestConfig{
|
||||
medium: v1.StorageMediumDefault,
|
||||
expectedSetupMounts: 0,
|
||||
expectedTeardownMounts: 0})
|
||||
}
|
||||
|
||||
func TestPluginHugetlbfs(t *testing.T) {
|
||||
doTestPlugin(t, pluginTestConfig{
|
||||
medium: v1.StorageMediumHugePages,
|
||||
expectedSetupMounts: 1,
|
||||
expectedTeardownMounts: 0,
|
||||
shouldBeMountedBeforeTeardown: true,
|
||||
})
|
||||
}
|
||||
|
||||
type pluginTestConfig struct {
|
||||
medium v1.StorageMedium
|
||||
idempotent bool
|
||||
expectedSetupMounts int
|
||||
shouldBeMountedBeforeTeardown bool
|
||||
expectedTeardownMounts int
|
||||
}
|
||||
|
||||
// doTestPlugin sets up a volume and tears it back down.
|
||||
func doTestPlugin(t *testing.T, config pluginTestConfig) {
|
||||
basePath, err := utiltesting.MkTmpdir("emptydir_volume_test")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make a temp rootdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(basePath)
|
||||
|
||||
var (
|
||||
volumePath = path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/test-volume")
|
||||
metadataDir = path.Join(basePath, "pods/poduid/plugins/kubernetes.io~empty-dir/test-volume")
|
||||
|
||||
plug = makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath)
|
||||
volumeName = "test-volume"
|
||||
spec = &v1.Volume{
|
||||
Name: volumeName,
|
||||
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: config.medium}},
|
||||
}
|
||||
|
||||
physicalMounter = mount.FakeMounter{}
|
||||
mountDetector = fakeMountDetector{}
|
||||
pod = &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID("poduid"),
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if config.idempotent {
|
||||
physicalMounter.MountPoints = []mount.MountPoint{
|
||||
{
|
||||
Path: volumePath,
|
||||
},
|
||||
}
|
||||
util.SetReady(metadataDir)
|
||||
}
|
||||
|
||||
mounter, err := plug.(*emptyDirPlugin).newMounterInternal(volume.NewSpecFromVolume(spec),
|
||||
pod,
|
||||
&physicalMounter,
|
||||
&mountDetector,
|
||||
volume.VolumeOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Mounter: %v", err)
|
||||
}
|
||||
if mounter == nil {
|
||||
t.Errorf("Got a nil Mounter")
|
||||
}
|
||||
|
||||
volPath := mounter.GetPath()
|
||||
if volPath != volumePath {
|
||||
t.Errorf("Got unexpected path: %s", volPath)
|
||||
}
|
||||
|
||||
if err := mounter.SetUp(nil); err != nil {
|
||||
t.Errorf("Expected success, got: %v", err)
|
||||
}
|
||||
|
||||
// Stat the directory and check the permission bits
|
||||
fileinfo, err := os.Stat(volPath)
|
||||
if !config.idempotent {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("SetUp() failed, volume path not created: %s", volPath)
|
||||
} else {
|
||||
t.Errorf("SetUp() failed: %v", err)
|
||||
}
|
||||
}
|
||||
if e, a := perm, fileinfo.Mode().Perm(); e != a {
|
||||
t.Errorf("Unexpected file mode for %v: expected: %v, got: %v", volPath, e, a)
|
||||
}
|
||||
} else if err == nil {
|
||||
// If this test is for idempotency and we were able
|
||||
// to stat the volume path, it's an error.
|
||||
t.Errorf("Volume directory was created unexpectedly")
|
||||
}
|
||||
|
||||
// Check the number of mounts performed during setup
|
||||
if e, a := config.expectedSetupMounts, len(physicalMounter.Log); e != a {
|
||||
t.Errorf("Expected %v physicalMounter calls during setup, got %v", e, a)
|
||||
} else if config.expectedSetupMounts == 1 &&
|
||||
(physicalMounter.Log[0].Action != mount.FakeActionMount || (physicalMounter.Log[0].FSType != "tmpfs" && physicalMounter.Log[0].FSType != "hugetlbfs")) {
|
||||
t.Errorf("Unexpected physicalMounter action during setup: %#v", physicalMounter.Log[0])
|
||||
}
|
||||
physicalMounter.ResetLog()
|
||||
|
||||
// Make an unmounter for the volume
|
||||
teardownMedium := mediumUnknown
|
||||
if config.medium == v1.StorageMediumMemory {
|
||||
teardownMedium = mediumMemory
|
||||
}
|
||||
unmounterMountDetector := &fakeMountDetector{medium: teardownMedium, isMount: config.shouldBeMountedBeforeTeardown}
|
||||
unmounter, err := plug.(*emptyDirPlugin).newUnmounterInternal(volumeName, types.UID("poduid"), &physicalMounter, unmounterMountDetector)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Unmounter: %v", err)
|
||||
}
|
||||
if unmounter == nil {
|
||||
t.Errorf("Got a nil Unmounter")
|
||||
}
|
||||
|
||||
// Tear down the volume
|
||||
if err := unmounter.TearDown(); err != nil {
|
||||
t.Errorf("Expected success, got: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(volPath); err == nil {
|
||||
t.Errorf("TearDown() failed, volume path still exists: %s", volPath)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Errorf("TearDown() failed: %v", err)
|
||||
}
|
||||
|
||||
// Check the number of physicalMounter calls during tardown
|
||||
if e, a := config.expectedTeardownMounts, len(physicalMounter.Log); e != a {
|
||||
t.Errorf("Expected %v physicalMounter calls during teardown, got %v", e, a)
|
||||
} else if config.expectedTeardownMounts == 1 && physicalMounter.Log[0].Action != mount.FakeActionUnmount {
|
||||
t.Errorf("Unexpected physicalMounter action during teardown: %#v", physicalMounter.Log[0])
|
||||
}
|
||||
physicalMounter.ResetLog()
|
||||
}
|
||||
|
||||
func TestPluginBackCompat(t *testing.T) {
|
||||
basePath, err := utiltesting.MkTmpdir("emptydirTest")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make a temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(basePath)
|
||||
|
||||
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", basePath)
|
||||
|
||||
spec := &v1.Volume{
|
||||
Name: "vol1",
|
||||
}
|
||||
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
|
||||
mounter, err := plug.NewMounter(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Mounter: %v", err)
|
||||
}
|
||||
if mounter == nil {
|
||||
t.Fatalf("Got a nil Mounter")
|
||||
}
|
||||
|
||||
volPath := mounter.GetPath()
|
||||
if volPath != path.Join(basePath, "pods/poduid/volumes/kubernetes.io~empty-dir/vol1") {
|
||||
t.Errorf("Got unexpected path: %s", volPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMetrics tests that MetricProvider methods return sane values.
|
||||
func TestMetrics(t *testing.T) {
|
||||
// Create an empty temp directory for the volume
|
||||
tmpDir, err := utiltesting.MkTmpdir("empty_dir_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Can't make a tmp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", tmpDir)
|
||||
|
||||
spec := &v1.Volume{
|
||||
Name: "vol1",
|
||||
}
|
||||
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
|
||||
mounter, err := plug.NewMounter(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to make a new Mounter: %v", err)
|
||||
}
|
||||
if mounter == nil {
|
||||
t.Fatalf("Got a nil Mounter")
|
||||
}
|
||||
|
||||
// Need to create the subdirectory
|
||||
os.MkdirAll(mounter.GetPath(), 0755)
|
||||
|
||||
expectedEmptyDirUsage, err := volumetest.FindEmptyDirectoryUsageOnTmpfs()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error finding expected empty directory usage on tmpfs: %v", err)
|
||||
}
|
||||
|
||||
// TODO(pwittroc): Move this into a reusable testing utility
|
||||
metrics, err := mounter.GetMetrics()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when calling GetMetrics %v", err)
|
||||
}
|
||||
if e, a := expectedEmptyDirUsage.Value(), metrics.Used.Value(); e != a {
|
||||
t.Errorf("Unexpected value for empty directory; expected %v, got %v", e, a)
|
||||
}
|
||||
if metrics.Capacity.Value() <= 0 {
|
||||
t.Errorf("Expected Capacity to be greater than 0")
|
||||
}
|
||||
if metrics.Available.Value() <= 0 {
|
||||
t.Errorf("Expected Available to be greater than 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHugePagesMountOptions(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
pod *v1.Pod
|
||||
shouldFail bool
|
||||
expectedResult string
|
||||
}{
|
||||
"testWithProperValues": {
|
||||
pod: &v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldFail: false,
|
||||
expectedResult: "pagesize=2Mi",
|
||||
},
|
||||
"testWithProperValuesAndDifferentPageSize": {
|
||||
pod: &v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("4Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldFail: false,
|
||||
expectedResult: "pagesize=1Gi",
|
||||
},
|
||||
"InitContainerAndContainerHasProperValues": {
|
||||
pod: &v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("4Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldFail: false,
|
||||
expectedResult: "pagesize=1Gi",
|
||||
},
|
||||
"InitContainerAndContainerHasDifferentPageSizes": {
|
||||
pod: &v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-2Mi"): resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("4Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldFail: true,
|
||||
expectedResult: "",
|
||||
},
|
||||
"ContainersWithMultiplePageSizes": {
|
||||
pod: &v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldFail: true,
|
||||
expectedResult: "",
|
||||
},
|
||||
"PodWithNoHugePagesRequest": {
|
||||
pod: &v1.Pod{},
|
||||
shouldFail: true,
|
||||
expectedResult: "",
|
||||
},
|
||||
}
|
||||
|
||||
for testCaseName, testCase := range testCases {
|
||||
value, err := getPageSizeMountOptionFromPod(testCase.pod)
|
||||
if testCase.shouldFail && err == nil {
|
||||
t.Errorf("Expected an error in %v", testCaseName)
|
||||
} else if !testCase.shouldFail && err != nil {
|
||||
t.Errorf("Unexpected error in %v, got %v", testCaseName, err)
|
||||
} else if testCase.expectedResult != value {
|
||||
t.Errorf("Unexpected mountOptions for Pod. Expected %v, got %v", testCase.expectedResult, value)
|
||||
}
|
||||
}
|
||||
}
|
32
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_unsupported.go
generated
vendored
Normal file
32
vendor/k8s.io/kubernetes/pkg/volume/empty_dir/empty_dir_unsupported.go
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package empty_dir
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/util/mount"
|
||||
)
|
||||
|
||||
// realMountDetector pretends to implement mediumer.
|
||||
type realMountDetector struct {
|
||||
mounter mount.Interface
|
||||
}
|
||||
|
||||
func (m *realMountDetector) GetMountMedium(path string) (storageMedium, bool, error) {
|
||||
return mediumUnknown, false, nil
|
||||
}
|
Reference in New Issue
Block a user