mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 05:17:02 +00:00
Merge pull request #14090 from samuelkarp/mask-interrupts
pkg/oci: mask thermal interrupt info
This commit is contained in:
6 files changed
+344
-17
No files matched your search
@@ -18,16 +18,19 @@ package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/pkg/identifiers"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/containerd/v2/pkg/oci"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// LoadBundle loads an existing bundle from disk
|
||||
@@ -106,10 +109,21 @@ func NewBundle(ctx context.Context, root, state, id string, spec typeurl.Any) (b
|
||||
// Spec may be nil for some sandboxers that do not initialize the spec, for
|
||||
// example the shim sandboxer with a hostNetwork container.
|
||||
if spec != nil {
|
||||
if spec := spec.GetValue(); spec != nil {
|
||||
if specVal := spec.GetValue(); specVal != nil {
|
||||
logger := log.G(ctx)
|
||||
if logger.Logger.IsLevelEnabled(log.TraceLevel) && typeurl.Is(spec, &specs.Spec{}) {
|
||||
var s specs.Spec
|
||||
if err := json.Unmarshal(specVal, &s); err == nil && s.Linux != nil {
|
||||
logger.WithFields(log.Fields{
|
||||
"bundle": b.Path,
|
||||
"maskedPaths": s.Linux.MaskedPaths,
|
||||
"readonlyPaths": s.Linux.ReadonlyPaths,
|
||||
}).Trace("configured bundle paths")
|
||||
}
|
||||
}
|
||||
// write the spec to the bundle
|
||||
specPath := filepath.Join(b.Path, oci.ConfigFilename)
|
||||
err = os.WriteFile(specPath, spec, 0666)
|
||||
err = os.WriteFile(specPath, specVal, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write bundle spec: %w", err)
|
||||
}
|
||||
|
||||
+81
-13
@@ -19,9 +19,15 @@ package oci
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@@ -186,19 +192,7 @@ func populateDefaultUnixSpec(ctx context.Context, s *Spec, id string) error {
|
||||
},
|
||||
},
|
||||
Linux: &specs.Linux{
|
||||
MaskedPaths: []string{
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap",
|
||||
"/proc/scsi",
|
||||
},
|
||||
MaskedPaths: defaultLinuxMaskedPaths(),
|
||||
ReadonlyPaths: []string{
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
@@ -264,3 +258,77 @@ func DescriptorToProto(d ocispec.Descriptor) *types.Descriptor {
|
||||
Annotations: d.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
var cachedDefaultLinuxMaskedPaths = sync.OnceValue(func() []string {
|
||||
maskedPaths := []string{
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/interrupts",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap",
|
||||
"/proc/scsi",
|
||||
}
|
||||
|
||||
return appendCPUThrottlePaths(maskedPaths, possibleCPUs())
|
||||
})
|
||||
|
||||
func defaultLinuxMaskedPaths() []string {
|
||||
return slices.Clone(cachedDefaultLinuxMaskedPaths())
|
||||
}
|
||||
|
||||
func possibleCPUs() []int {
|
||||
if cpus, err := possibleCPUsParsed(); err == nil && len(cpus) > 0 {
|
||||
return cpus
|
||||
}
|
||||
|
||||
var cpus []int
|
||||
for i := range runtime.NumCPU() {
|
||||
cpus = append(cpus, i)
|
||||
}
|
||||
return cpus
|
||||
}
|
||||
|
||||
func parsePossibleCPUs(content string) ([]int, error) {
|
||||
if content == "" {
|
||||
return nil, errors.New("empty possible cpu string")
|
||||
}
|
||||
ranges := strings.Split(content, ",")
|
||||
var cpus []int
|
||||
for _, r := range ranges {
|
||||
if rStart, rEnd, ok := strings.Cut(r, "-"); ok {
|
||||
start, err := strconv.Atoi(rStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cpu range start %q: %w", rStart, err)
|
||||
}
|
||||
if start < 0 {
|
||||
return nil, fmt.Errorf("negative cpu range start %d", start)
|
||||
}
|
||||
end, err := strconv.Atoi(rEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cpu range end %q: %w", rEnd, err)
|
||||
}
|
||||
if start > end {
|
||||
return nil, fmt.Errorf("invalid cpu range %d-%d: start greater than end", start, end)
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
cpus = append(cpus, i)
|
||||
}
|
||||
} else {
|
||||
cpu, err := strconv.Atoi(rStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cpu number %q: %w", rStart, err)
|
||||
}
|
||||
if cpu < 0 {
|
||||
return nil, fmt.Errorf("negative cpu number %d", cpu)
|
||||
}
|
||||
cpus = append(cpus, cpu)
|
||||
}
|
||||
}
|
||||
return cpus, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright The containerd 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 oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var possibleCPUsParsed = sync.OnceValues(func() ([]int, error) {
|
||||
data, err := os.ReadFile("/sys/devices/system/cpu/possible")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsePossibleCPUs(strings.TrimSpace(string(data)))
|
||||
})
|
||||
|
||||
func appendCPUThrottlePaths(paths []string, cpus []int) []string {
|
||||
for _, cpu := range cpus {
|
||||
path := fmt.Sprintf("/sys/devices/system/cpu/cpu%d/thermal_throttle", cpu)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright The containerd 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 oci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPossibleCPUs_HostSysfs(t *testing.T) {
|
||||
const possiblePath = "/sys/devices/system/cpu/possible"
|
||||
data, err := os.ReadFile(possiblePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
t.Skipf("%s not present on host", possiblePath)
|
||||
}
|
||||
t.Fatalf("failed to read %s: %v", possiblePath, err)
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(string(data))
|
||||
parsed, err := parsePossibleCPUs(content)
|
||||
require.NoError(t, err, "host %s content %q failed to parse", possiblePath, content)
|
||||
require.NotEmpty(t, parsed, "parsed CPU list should not be empty")
|
||||
|
||||
cachedParsed, err := possibleCPUsParsed()
|
||||
require.NoError(t, err, "possibleCPUsParsed() returned error")
|
||||
assert.Equal(t, parsed, cachedParsed, "possibleCPUsParsed() should match parsePossibleCPUs")
|
||||
|
||||
cpus := possibleCPUs()
|
||||
assert.Equal(t, parsed, cpus, "possibleCPUs() should return parsed possible CPUs without falling back to NumCPU counting")
|
||||
assert.GreaterOrEqual(t, len(cpus), runtime.NumCPU(), "possible CPUs count should be >= runtime.NumCPU()")
|
||||
}
|
||||
|
||||
func TestDefaultLinuxMaskedPaths(t *testing.T) {
|
||||
paths := defaultLinuxMaskedPaths()
|
||||
assert.Contains(t, paths, "/proc/interrupts")
|
||||
|
||||
root := "/sys/devices/system/cpu"
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
t.Skipf("%s not present on host", root)
|
||||
}
|
||||
t.Fatalf("failed to read %s: %v", root, err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !strings.HasPrefix(name, "cpu") {
|
||||
continue
|
||||
}
|
||||
if _, err := strconv.Atoi(strings.TrimPrefix(name, "cpu")); err != nil {
|
||||
continue
|
||||
}
|
||||
thermalPath := filepath.Join(root, name, "thermal_throttle")
|
||||
if _, err := os.Stat(thermalPath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
t.Fatalf("unexpected error stating %s: %v", thermalPath, err)
|
||||
}
|
||||
assert.Contains(t, paths, thermalPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLinuxMaskedPaths_Clone(t *testing.T) {
|
||||
paths1 := defaultLinuxMaskedPaths()
|
||||
paths2 := defaultLinuxMaskedPaths()
|
||||
require.Equal(t, paths1, paths2)
|
||||
require.NotEmpty(t, paths1)
|
||||
|
||||
paths1[0] = "/mutated"
|
||||
paths3 := defaultLinuxMaskedPaths()
|
||||
assert.NotEqual(t, paths1[0], paths3[0])
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build !linux
|
||||
|
||||
/*
|
||||
Copyright The containerd 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 oci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var possibleCPUsParsed = sync.OnceValues(func() ([]int, error) {
|
||||
return nil, errors.New("not supported")
|
||||
})
|
||||
|
||||
func appendCPUThrottlePaths(paths []string, _ []int) []string {
|
||||
return paths
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/sys/user"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -479,3 +480,76 @@ func TestGroupLookup_Symlink(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePossibleCPUs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "continuous range",
|
||||
input: "0-3",
|
||||
expected: []int{0, 1, 2, 3},
|
||||
},
|
||||
{
|
||||
name: "large range",
|
||||
input: "0-127",
|
||||
expected: func() []int {
|
||||
res := make([]int, 128)
|
||||
for i := range res {
|
||||
res[i] = i
|
||||
}
|
||||
return res
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "non-continuous range",
|
||||
input: "0-2,4,6-7",
|
||||
expected: []int{0, 1, 2, 4, 6, 7},
|
||||
},
|
||||
{
|
||||
name: "single cpu",
|
||||
input: "5",
|
||||
expected: []int{5},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid range",
|
||||
input: "0-2,invalid",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "malformed range with multiple dashes",
|
||||
input: "0-2-3",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "reversed range",
|
||||
input: "3-0",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "negative start",
|
||||
input: "-1-3",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := parsePossibleCPUs(test.input)
|
||||
if test.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user