blob: 859ee7476c94eae7e046bb725bd98845f3f72914 [file] [log] [blame]
Lorenz Brun878f5f92020-05-12 16:15:39 +02001Copyright 2020 The Monogon Project Authors.
2
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License.
14
15
16From b16e57cc52a437465bbd12c24fb05fe5790afe1d Mon Sep 17 00:00:00 2001
17From: Lorenz Brun <lorenz@brun.one>
18Date: Tue, 17 Mar 2020 21:41:08 +0100
19Subject: [PATCH 2/3] Add a native volume metrics implementation
20
21---
22 pkg/volume/BUILD | 3 +
23 pkg/volume/metrics_native.go | 101 +++++++++++++++++++++++++++++
24 pkg/volume/metrics_native_test.go | 102 ++++++++++++++++++++++++++++++
25 3 files changed, 206 insertions(+)
26 create mode 100644 pkg/volume/metrics_native.go
27 create mode 100644 pkg/volume/metrics_native_test.go
28
29diff --git a/pkg/volume/BUILD b/pkg/volume/BUILD
30index 720b13406dc..b6e4b7e6d6f 100644
31--- a/pkg/volume/BUILD
32+++ b/pkg/volume/BUILD
33@@ -7,6 +7,7 @@ go_library(
34 "metrics_cached.go",
35 "metrics_du.go",
36 "metrics_errors.go",
37+ "metrics_native.go",
38 "metrics_nil.go",
39 "metrics_statfs.go",
40 "noop_expandable_plugin.go",
41@@ -35,6 +36,7 @@ go_library(
42 "@io_k8s_client_go//tools/cache:go_default_library",
43 "@io_k8s_client_go//tools/record:go_default_library",
44 "@io_k8s_cloud_provider//:go_default_library",
45+ "@org_golang_x_sys//unix:go_default_library",
Lorenz Brunb876fc32020-07-14 13:54:01 +020046 "@io_k8s_klog_v2//:go_default_library",
Lorenz Brun878f5f92020-05-12 16:15:39 +020047 "@io_k8s_utils//exec:go_default_library",
48 "@io_k8s_utils//mount:go_default_library",
49@@ -55,6 +57,7 @@ go_test(
50 name = "go_default_test",
51 srcs = [
52 "metrics_du_test.go",
53+ "metrics_native_test.go",
54 "metrics_nil_test.go",
55 "metrics_statfs_test.go",
56 "plugins_test.go",
57diff --git a/pkg/volume/metrics_native.go b/pkg/volume/metrics_native.go
58new file mode 100644
59index 00000000000..3934b946f2e
60--- /dev/null
61+++ b/pkg/volume/metrics_native.go
62@@ -0,0 +1,101 @@
63+/*
64+Copyright 2020 The Kubernetes Authors.
65+
66+Licensed under the Apache License, Version 2.0 (the "License");
67+you may not use this file except in compliance with the License.
68+You may obtain a copy of the License at
69+
70+ http://www.apache.org/licenses/LICENSE-2.0
71+
72+Unless required by applicable law or agreed to in writing, software
73+distributed under the License is distributed on an "AS IS" BASIS,
74+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
75+See the License for the specific language governing permissions and
76+limitations under the License.
77+*/
78+
79+package volume
80+
81+import (
82+ "os"
83+ "path/filepath"
84+
85+ "golang.org/x/sys/unix"
86+ "k8s.io/apimachinery/pkg/api/resource"
87+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
88+)
89+
90+var _ MetricsProvider = &metricsNative{}
91+
92+// MetricsNative represents a MetricsProvider that calculates the volume metrics with either a quota
93+// or by walking the path using syscalls.
94+type metricsNative struct {
95+ path string
96+}
97+
98+// NewMetricsNative returns a new metricsNative provider
99+func NewMetricsNative(path string) MetricsProvider {
100+ return &metricsNative{path}
101+}
102+
103+// GetMetrics returns an empty Metrics and an error.
104+// See MetricsProvider.GetMetrics
105+func (m *metricsNative) GetMetrics() (*Metrics, error) {
106+ var inodesCount int64
107+ var bytesCount int64
108+
109+ var getMetricsRecursive func(path string) error
110+ getMetricsRecursive = func(path string) error {
111+ var stat unix.Stat_t
112+ err := unix.Lstat(path, &stat)
113+ if os.IsNotExist(err) {
114+ return nil
115+ } else if err != nil {
116+ return err
117+ }
118+ // TODO: This double-counts hardlinks
119+ bytesCount += stat.Blocks * 512
120+ inodesCount++
121+ if stat.Mode&unix.S_IFDIR != 0 && stat.Mode&unix.S_IFLNK == 0 {
122+ fd, err := os.Open(path)
123+ if os.IsNotExist(err) {
124+ return nil
125+ } else if err != nil {
126+ return err
127+ }
128+ // We manually close fd before recursing, otherwise we have too many FDs open
129+
130+ entries, err := fd.Readdirnames(0)
131+ if err != nil {
132+ fd.Close()
133+ return err
134+ }
135+ fd.Close()
136+ for _, entry := range entries {
137+ if err := getMetricsRecursive(filepath.Join(path, entry)); err != nil {
138+ return err
139+ }
140+ }
141+ }
142+ return nil
143+ }
144+
145+ if err := getMetricsRecursive(m.path); err != nil {
146+ return &Metrics{}, err
147+ }
148+
149+ var statfs unix.Statfs_t
150+ if err := unix.Statfs(m.path, &statfs); err != nil {
151+ return &Metrics{}, err
152+ }
153+
154+ return &Metrics{
155+ Time: metav1.Now(),
156+ Used: resource.NewQuantity(bytesCount, resource.BinarySI),
157+ InodesUsed: resource.NewQuantity(inodesCount, resource.BinarySI),
158+ Available: resource.NewQuantity(int64(statfs.Bavail)*statfs.Bsize, resource.BinarySI),
159+ Capacity: resource.NewQuantity(int64(statfs.Blocks)*statfs.Bsize, resource.BinarySI),
160+ Inodes: resource.NewQuantity(int64(statfs.Files)*statfs.Bsize, resource.BinarySI),
161+ InodesFree: resource.NewQuantity(int64(statfs.Ffree)*statfs.Bsize, resource.BinarySI),
162+ }, nil
163+}
164diff --git a/pkg/volume/metrics_native_test.go b/pkg/volume/metrics_native_test.go
165new file mode 100644
166index 00000000000..2d5546591ce
167--- /dev/null
168+++ b/pkg/volume/metrics_native_test.go
169@@ -0,0 +1,102 @@
170+// +build linux
171+
172+/*
173+Copyright 2015 The Kubernetes Authors.
174+
175+Licensed under the Apache License, Version 2.0 (the "License");
176+you may not use this file except in compliance with the License.
177+You may obtain a copy of the License at
178+
179+ http://www.apache.org/licenses/LICENSE-2.0
180+
181+Unless required by applicable law or agreed to in writing, software
182+distributed under the License is distributed on an "AS IS" BASIS,
183+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
184+See the License for the specific language governing permissions and
185+limitations under the License.
186+*/
187+
188+package volume_test
189+
190+import (
191+ "io/ioutil"
192+ "os"
193+ "path/filepath"
194+ "testing"
195+
196+ utiltesting "k8s.io/client-go/util/testing"
197+ . "k8s.io/kubernetes/pkg/volume"
198+ volumetest "k8s.io/kubernetes/pkg/volume/testing"
199+)
200+
201+// TestMetricsNativeGetCapacity tests that MetricsNative can read disk usage
202+// for path
203+func TestMetricsNativeGetCapacity(t *testing.T) {
204+ tmpDir, err := utiltesting.MkTmpdir("metrics_du_test")
205+ if err != nil {
206+ t.Fatalf("Can't make a tmp dir: %v", err)
207+ }
208+ defer os.RemoveAll(tmpDir)
209+ metrics := NewMetricsNative(tmpDir)
210+
211+ expectedEmptyDirUsage, err := volumetest.FindEmptyDirectoryUsageOnTmpfs()
212+ if err != nil {
213+ t.Errorf("Unexpected error finding expected empty directory usage on tmpfs: %v", err)
214+ }
215+
216+ actual, err := metrics.GetMetrics()
217+ if err != nil {
218+ t.Errorf("Unexpected error when calling GetMetrics %v", err)
219+ }
220+ if e, a := expectedEmptyDirUsage.Value(), actual.Used.Value(); e != a {
221+ t.Errorf("Unexpected value for empty directory; expected %v, got %v", e, a)
222+ }
223+
224+ // TODO(pwittroc): Figure out a way to test these values for correctness, maybe by formatting and mounting a file
225+ // as a filesystem
226+ if a := actual.Capacity.Value(); a <= 0 {
227+ t.Errorf("Expected Capacity %d to be greater than 0.", a)
228+ }
229+ if a := actual.Available.Value(); a <= 0 {
230+ t.Errorf("Expected Available %d to be greater than 0.", a)
231+ }
232+
233+ // Write a file in a directory and expect Used to increase
234+ os.MkdirAll(filepath.Join(tmpDir, "d1"), 0755)
235+ ioutil.WriteFile(filepath.Join(tmpDir, "d1", "f1"), []byte("Hello World"), os.ModeTemporary)
236+ actual, err = metrics.GetMetrics()
237+ if err != nil {
238+ t.Errorf("Unexpected error when calling GetMetrics %v", err)
239+ }
240+ if e, a := (2*expectedEmptyDirUsage.Value() + getExpectedBlockSize(filepath.Join(tmpDir, "d1", "f1"))), actual.Used.Value(); e != a {
241+ t.Errorf("Unexpected Used for directory with file. Expected %v, got %d.", e, a)
242+ }
243+}
244+
245+// TestMetricsNativeRequireInit tests that if MetricsNative is not initialized with a path, GetMetrics
246+// returns an error
247+func TestMetricsNativeRequirePath(t *testing.T) {
248+ metrics := NewMetricsNative("")
249+ actual, err := metrics.GetMetrics()
250+ expected := &Metrics{}
251+ if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) {
252+ t.Errorf("Expected empty Metrics from uninitialized MetricsNative, actual %v", *actual)
253+ }
254+ if err == nil {
255+ t.Errorf("Expected error when calling GetMetrics on uninitialized MetricsNative, actual nil")
256+ }
257+}
258+
259+// TestMetricsNativeRealDirectory tests that if MetricsNative is initialized to a non-existent path, GetMetrics
260+// returns an error
261+func TestMetricsNativeRequireRealDirectory(t *testing.T) {
262+ metrics := NewMetricsNative("/not/a/real/directory")
263+ actual, err := metrics.GetMetrics()
264+ expected := &Metrics{}
265+ if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) {
266+ t.Errorf("Expected empty Metrics from incorrectly initialized MetricsNative, actual %v", *actual)
267+ }
268+ if err == nil {
269+ t.Errorf("Expected error when calling GetMetrics on incorrectly initialized MetricsNative, actual nil")
270+ }
271+}
272--
2732.25.1
274