1
#pragma once
2

            
3
#include <cstdint>
4

            
5
#include "envoy/filesystem/filesystem.h"
6

            
7
#include "source/common/singleton/threadsafe_singleton.h"
8

            
9
#include "absl/strings/string_view.h"
10
#include "absl/types/optional.h"
11

            
12
namespace Envoy {
13

            
14
/**
15
 * Interface for cgroup CPU detection. Allows mocking in tests.
16
 */
17
class CgroupDetector {
18
public:
19
9
  virtual ~CgroupDetector() = default;
20

            
21
  /**
22
   * Detects CPU limit from `cgroup` subsystem.
23
   * @param fs Filesystem instance for file operations.
24
   * @return CPU limit or absl::nullopt if no `cgroup` limit found.
25
   */
26
  virtual absl::optional<uint32_t> getCpuLimit(Filesystem::Instance& fs) PURE;
27
};
28

            
29
/**
30
 * Production implementation of cgroup CPU detection.
31
 */
32
class CgroupDetectorImpl : public CgroupDetector {
33
public:
34
  absl::optional<uint32_t> getCpuLimit(Filesystem::Instance& fs) override;
35
};
36

            
37
using CgroupDetectorSingleton = ThreadSafeSingleton<CgroupDetectorImpl>;
38

            
39
/**
40
 * `Cgroup` filesystem mount information.
41
 */
42
struct CgroupMount {
43
  std::string mount_point;
44
  std::string filesystem_type;
45
  std::string mount_options;
46
  bool has_cpu_controller = false;
47
};
48

            
49
/**
50
 * `Cgroup` path information with version detection from `/proc/self/cgroup` parsing.
51
 * Follows Envoy's pattern like LegacyLbPolicyConfigHelper::Result.
52
 */
53
struct CgroupPathInfo {
54
  std::string relative_path; // Relative `cgroup` path like "/docker/abc123" or "/"
55
  std::string version;       // "v1" or "v2" detected from hierarchy parsing
56

            
57
  // Constructor for easy creation
58
  CgroupPathInfo(std::string path, std::string ver)
59
6
      : relative_path(std::move(path)), version(std::move(ver)) {}
60
};
61

            
62
/**
63
 * Combined `cgroup` information with path and version.
64
 */
65
struct CgroupInfo {
66
  std::string full_path; // Combined mount + relative path
67
  std::string version;   // "v1" or "v2"
68
};
69

            
70
/**
71
 * CPU `cgroup` files with cached file content.
72
 */
73
struct CpuFiles {
74
  std::string version;
75
  std::string quota_content;  // Store actual file content, not path
76
  std::string period_content; // Store actual file content, not path (empty for v2)
77
};
78

            
79
/**
80
 * Utility class for detecting CPU limits from `cgroup` subsystem.
81
 */
82
class CgroupCpuUtil {
83
public:
84
  class TestUtil;
85
  friend class TestUtil;
86
  /**
87
   * Detects CPU limit from `cgroup` `v2` or `v1` with hierarchy scanning.
88
   * Scans `cgroup` hierarchy and takes minimum effective limit for container-aware CPU detection.
89
   * @param fs Filesystem instance for file operations.
90
   * @return CPU limit or absl::nullopt if no `cgroup` limit found.
91
   */
92
  static absl::optional<uint32_t> getCpuLimit(Filesystem::Instance& fs);
93

            
94
private:
95
  /**
96
   * Reads CPU limit from specific `cgroup` `v1` paths.
97
   * @param fs Filesystem instance.
98
   * @param quota_path Path to `cpu.cfs_quota_us` file.
99
   * @param period_path Path to `cpu.cfs_period_us` file.
100
   * @return CPU limit or absl::nullopt if not available/unlimited.
101
   */
102

            
103
  // Validates `cgroup` file content following Go's strict requirements:
104
  // - Content must end with newline (matching Go's validation)
105
  // Returns string_view without trailing newline on success, nullopt on failure.
106
  static absl::optional<absl::string_view> validateCgroupFileContent(const std::string& content,
107
                                                                     const std::string& file_path);
108

            
109
  // Parses `/proc/self/cgroup` to find the current process's `cgroup` path with priority handling.
110

            
111
  /**
112
   * Gets the current process `cgroup` path and version by parsing `/proc/self/cgroup`.
113
   * Determines version from hierarchy ID and controller info.
114
   * @param fs Filesystem instance.
115
   * @return CgroupPathInfo with relative path and version, nullopt if not found.
116
   */
117
  static absl::optional<CgroupPathInfo> getCurrentCgroupPath(Filesystem::Instance& fs);
118

            
119
  /**
120
   * Discovers cgroup filesystem mounts by parsing `/proc/self/mountinfo`.
121
   * Priority handling: `cgroup` `v1` with CPU controller wins over `cgroup` `v2`.
122
   * @param fs Filesystem instance.
123
   * @return Mount point string on success, nullopt if no suitable `cgroup` found.
124
   */
125
  static absl::optional<std::string> discoverCgroupMount(Filesystem::Instance& fs);
126

            
127
  /**
128
   * Parses a single line from `/proc/self/mountinfo` to extract cgroup mount point.
129
   * Format: `mountID parentID major:minor root mountPoint options - fsType source superOptions`
130
   * We extract field 5 (mount point) for `cgroup`/`cgroup2` filesystem only.
131
   * @param line Single line from `/proc/self/mountinfo`
132
   * @return Mount point string if line contains `cgroup` filesystem, nullopt if not a `cgroup`
133
   * line.
134
   */
135
  static absl::optional<std::string> parseMountInfoLine(const std::string& line);
136

            
137
  /**
138
   * Unescapes octal escape sequences in paths from `/proc/self/mountinfo`.
139
   * Linux's `show_path` converts `\`, ` `, `\t`, and `\n` to octal escape sequences
140
   * like `\040` for space, `\134` for backslash.
141
   * @param path The escaped path string from `mountinfo`.
142
   * @return The unescaped path string.
143
   */
144
  static std::string unescapePath(const std::string& path);
145

            
146
  /**
147
   * Constructs complete `cgroup` path by combining mount point and process assignment.
148
   * Logic: Use provided mount point (already discovered)
149
   *        Call process assignment → Get relative path
150
   *        Combine mount point and relative path
151
   * @param mount_point The `cgroup` mount point (from discoverCgroupMount).
152
   * @param fs Filesystem instance.
153
   * @return CgroupInfo with combined path + final version, nullopt if not found.
154
   */
155
  static absl::optional<CgroupInfo> constructCgroupPath(const std::string& mount_point,
156
                                                        Filesystem::Instance& fs);
157

            
158
  /**
159
   * Accesses `cgroup` CPU files with version-specific filename appending.
160
   * Logic: Get combined path from Step 3
161
   *        Append version-specific filenames (`v1`: quota+period, `v2`: `cpu.max`)
162
   *        Open files with `O_RDONLY`|`O_CLOEXEC` flags
163
   *        Buffer reuse: Same buffer for different file paths
164
   *        Error handling: File not found → return nullopt
165
   * @param cgroup_info Combined path and version from step 3.
166
   * @param fs Filesystem instance.
167
   * @return CpuFiles struct with cached file content, nullopt if files not accessible.
168
   */
169
  static absl::optional<CpuFiles> accessCgroupFiles(const CgroupInfo& cgroup_info,
170
                                                    Filesystem::Instance& fs);
171

            
172
  /**
173
   * Accesses `cgroup` `v1` CPU files (quota and period).
174
   * @param cgroup_info Combined path and version from step 3.
175
   * @param fs Filesystem instance.
176
   * @return CpuFiles struct with cached file content, nullopt if files not accessible.
177
   */
178
  static absl::optional<CpuFiles> accessCgroupV1Files(const CgroupInfo& cgroup_info,
179
                                                      Filesystem::Instance& fs);
180

            
181
  /**
182
   * Accesses `cgroup` `v2` CPU file (`cpu.max`).
183
   * @param cgroup_info Combined path and version from step 3.
184
   * @param fs Filesystem instance.
185
   * @return CpuFiles struct with cached file content, nullopt if files not accessible.
186
   */
187
  static absl::optional<CpuFiles> accessCgroupV2Files(const CgroupInfo& cgroup_info,
188
                                                      Filesystem::Instance& fs);
189

            
190
  /**
191
   * Reads actual CPU limits from `cgroup` files with version-specific parsing.
192
   * Logic: Use cached file paths from Step 4
193
   *        Read from offset 0 for fresh data
194
   *        Version-specific parsing (`v1`: divide quota/period, `v2`: parse "quota period")
195
   *        Handle special cases (`v1`: quota=-1, `v2`: quota="max" means no limit)
196
   * @param cpu_files Cached file paths from step 4.
197
   * @param fs Filesystem instance.
198
   * @return CPU limit as float64 ratio, nullopt if unlimited/invalid.
199
   */
200
  static absl::optional<double> readActualLimits(const CpuFiles& cpu_files,
201
                                                 Filesystem::Instance& fs);
202

            
203
  /**
204
   * Reads actual CPU limits from `cgroup` `v1` files with quota/period parsing.
205
   * @param cpu_files Cached file content from `v1` files.
206
   * @return CPU limit as float64 ratio, nullopt if unlimited/invalid.
207
   */
208
  static absl::optional<double> readActualLimitsV1(const CpuFiles& cpu_files);
209

            
210
  /**
211
   * Reads actual CPU limits from `cgroup` `v2` files with "quota period" parsing.
212
   * @param cpu_files Cached file content from `v2` files.
213
   * @return CPU limit as float64 ratio, nullopt if unlimited/invalid.
214
   */
215
  static absl::optional<double> readActualLimitsV2(const CpuFiles& cpu_files);
216

            
217
  // `Cgroup` `v2` paths
218
  static constexpr absl::string_view CGROUP_V2_CPU_MAX = "/sys/fs/cgroup/cpu.max";
219
  static constexpr absl::string_view CGROUP_V2_BASE_PATH = "/sys/fs/cgroup";
220

            
221
  // `Cgroup` `v1` paths
222
  static constexpr absl::string_view CGROUP_V1_CPU_QUOTA = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us";
223
  static constexpr absl::string_view CGROUP_V1_CPU_PERIOD = "/sys/fs/cgroup/cpu/cpu.cfs_period_us";
224
  static constexpr absl::string_view CGROUP_V1_BASE_PATH = "/sys/fs/cgroup/cpu";
225

            
226
  // Process `cgroup` info
227
  static constexpr absl::string_view PROC_CGROUP_PATH = "/proc/self/cgroup";
228
  static constexpr absl::string_view PROC_MOUNTINFO_PATH = "/proc/self/mountinfo";
229

            
230
  // `Cgroup` filename constants
231
  static constexpr absl::string_view CGROUP_V1_QUOTA_FILE = "/cpu.cfs_quota_us";
232
  static constexpr absl::string_view CGROUP_V1_PERIOD_FILE = "/cpu.cfs_period_us";
233
  static constexpr absl::string_view CGROUP_V2_CPU_MAX_FILE = "/cpu.max";
234
};
235

            
236
/**
237
 * Test utility class to provide access to private methods.
238
 */
239
class CgroupCpuUtil::TestUtil {
240
public:
241
7
  static absl::optional<CgroupPathInfo> getCurrentCgroupPath(Filesystem::Instance& fs) {
242
7
    return CgroupCpuUtil::getCurrentCgroupPath(fs);
243
7
  }
244

            
245
5
  static absl::optional<std::string> discoverCgroupMount(Filesystem::Instance& fs) {
246
5
    return CgroupCpuUtil::discoverCgroupMount(fs);
247
5
  }
248

            
249
7
  static absl::optional<std::string> parseMountInfoLine(const std::string& line) {
250
7
    return CgroupCpuUtil::parseMountInfoLine(line);
251
7
  }
252

            
253
18
  static std::string unescapePath(const std::string& path) {
254
18
    return CgroupCpuUtil::unescapePath(path);
255
18
  }
256

            
257
  static absl::optional<absl::string_view> validateCgroupFileContent(const std::string& content,
258
3
                                                                     const std::string& file_path) {
259
3
    return CgroupCpuUtil::validateCgroupFileContent(content, file_path);
260
3
  }
261

            
262
  // TestUtil wrappers for our new `modularized` functions
263
  static absl::optional<CpuFiles> accessCgroupV1Files(const CgroupInfo& cgroup_info,
264
4
                                                      Filesystem::Instance& fs) {
265
4
    return CgroupCpuUtil::accessCgroupV1Files(cgroup_info, fs);
266
4
  }
267

            
268
  static absl::optional<CpuFiles> accessCgroupV2Files(const CgroupInfo& cgroup_info,
269
3
                                                      Filesystem::Instance& fs) {
270
3
    return CgroupCpuUtil::accessCgroupV2Files(cgroup_info, fs);
271
3
  }
272

            
273
7
  static absl::optional<double> readActualLimitsV1(const CpuFiles& cpu_files) {
274
7
    return CgroupCpuUtil::readActualLimitsV1(cpu_files);
275
7
  }
276

            
277
8
  static absl::optional<double> readActualLimitsV2(const CpuFiles& cpu_files) {
278
8
    return CgroupCpuUtil::readActualLimitsV2(cpu_files);
279
8
  }
280
};
281

            
282
} // namespace Envoy