diff --git a/libcontainer/capabilities/capabilities.go b/libcontainer/capabilities/capabilities.go index 39b28d1ae77..04676b593be 100644 --- a/libcontainer/capabilities/capabilities.go +++ b/libcontainer/capabilities/capabilities.go @@ -3,10 +3,11 @@ package capabilities import ( - "fmt" + "sort" "strings" "github.com/opencontainers/runc/libcontainer/configs" + "github.com/sirupsen/logrus" "github.com/syndtr/gocapability/capability" ) @@ -34,27 +35,21 @@ func init() { } // New creates a new Caps from the given Capabilities config. Unknown Capabilities -// or Capabilities that are unavailable in the current environment produce an error. +// or Capabilities that are unavailable in the current environment are ignored, +// printing a warning instead. func New(capConfig *configs.Capabilities) (*Caps, error) { var ( - err error - c Caps + err error + c Caps + warnings = make(map[string]struct{}) ) - if c.caps[capability.BOUNDING], err = capSlice(capConfig.Bounding); err != nil { - return nil, err - } - if c.caps[capability.EFFECTIVE], err = capSlice(capConfig.Effective); err != nil { - return nil, err - } - if c.caps[capability.INHERITABLE], err = capSlice(capConfig.Inheritable); err != nil { - return nil, err - } - if c.caps[capability.PERMITTED], err = capSlice(capConfig.Permitted); err != nil { - return nil, err - } - if c.caps[capability.AMBIENT], err = capSlice(capConfig.Ambient); err != nil { - return nil, err + c.caps = map[capability.CapType][]capability.Cap{ + capability.BOUNDING: capSlice(capConfig.Bounding, warnings), + capability.EFFECTIVE: capSlice(capConfig.Effective, warnings), + capability.INHERITABLE: capSlice(capConfig.Inheritable, warnings), + capability.PERMITTED: capSlice(capConfig.Permitted, warnings), + capability.AMBIENT: capSlice(capConfig.Ambient, warnings), } if c.pid, err = capability.NewPid2(0); err != nil { return nil, err @@ -62,22 +57,36 @@ func New(capConfig *configs.Capabilities) (*Caps, error) { if err = c.pid.Load(); err != nil { return nil, err } + printWarnings(warnings) return &c, nil } -// capSlice converts the slice of capability names in caps, to their numeric +// capSlice converts the slice of capability names in newCaps, to their numeric // equivalent, and returns them as a slice. Unknown or unavailable capabilities -// produce an error. -func capSlice(caps []string) ([]capability.Cap, error) { - out := make([]capability.Cap, len(caps)) - for i, c := range caps { - v, ok := capabilityMap[c] - if !ok { - return nil, fmt.Errorf("unknown capability %q", c) +// are not returned, but appended to warnings, which can be used by the caller to +// print. +func capSlice(caps []string, warnings map[string]struct{}) []capability.Cap { + var out []capability.Cap + for _, c := range caps { + if v, ok := capabilityMap[c]; !ok { + warnings[c] = struct{}{} + } else { + out = append(out, v) } - out[i] = v } - return out, nil + return out +} + +func printWarnings(warnings map[string]struct{}) { + if len(warnings) == 0 { + return + } + var ignored []string + for c := range warnings { + ignored = append(ignored, c) + } + sort.Strings(ignored) + logrus.Warn("ignoring unknown or unavailable capabilities: " + strings.Join(ignored, ", ")) } // Caps holds the capabilities for a container. diff --git a/libcontainer/capabilities/capabilities_linux_test.go b/libcontainer/capabilities/capabilities_linux_test.go new file mode 100644 index 00000000000..d816fe589bc --- /dev/null +++ b/libcontainer/capabilities/capabilities_linux_test.go @@ -0,0 +1,72 @@ +package capabilities + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/syndtr/gocapability/capability" + + "github.com/opencontainers/runc/libcontainer/configs" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" +) + +func TestNewWarnings(t *testing.T) { + cs := []string{"CAP_CHOWN", "CAP_UNKNOWN", "CAP_UNKNOWN2"} + conf := configs.Capabilities{ + Bounding: cs, + Effective: cs, + Inheritable: cs, + Permitted: cs, + Ambient: cs, + } + + hook := test.NewGlobal() + defer hook.Reset() + + logrus.SetOutput(ioutil.Discard) + caps, err := New(&conf) + logrus.SetOutput(os.Stderr) + + if err != nil { + t.Error(err) + } + e := hook.AllEntries() + if len(e) != 1 { + t.Errorf("expected 1 warning, got %d", len(e)) + } + + expectedLogs := logrus.Entry{ + Level: logrus.WarnLevel, + Message: "ignoring unknown or unavailable capabilities: CAP_UNKNOWN, CAP_UNKNOWN2", + } + + l := hook.LastEntry() + if l == nil { + t.Fatal("expected a warning, but got none") + } + if l.Level != expectedLogs.Level { + t.Errorf("expected %q, got %q", expectedLogs.Level, l.Level) + } + if l.Message != expectedLogs.Message { + t.Errorf("expected %q, got %q", expectedLogs.Message, l.Message) + } + + if len(caps.caps) != 5 { + t.Errorf("expected 5 capability types, got %d: %v", len(caps.caps), caps.caps) + } + + for _, c := range caps.caps { + if len(c) != 1 { + t.Errorf("expected 1 capability, got %d: %v", len(c), c) + continue + } + if c[0] != capability.CAP_CHOWN { + t.Errorf("expected CAP_CHOWN, got %s: ", c[0]) + continue + } + } + + hook.Reset() +} diff --git a/vendor/github.com/sirupsen/logrus/hooks/test/test.go b/vendor/github.com/sirupsen/logrus/hooks/test/test.go new file mode 100644 index 00000000000..b16d06654ae --- /dev/null +++ b/vendor/github.com/sirupsen/logrus/hooks/test/test.go @@ -0,0 +1,91 @@ +// The Test package is used for testing logrus. +// It provides a simple hooks which register logged messages. +package test + +import ( + "io/ioutil" + "sync" + + "github.com/sirupsen/logrus" +) + +// Hook is a hook designed for dealing with logs in test scenarios. +type Hook struct { + // Entries is an array of all entries that have been received by this hook. + // For safe access, use the AllEntries() method, rather than reading this + // value directly. + Entries []logrus.Entry + mu sync.RWMutex +} + +// NewGlobal installs a test hook for the global logger. +func NewGlobal() *Hook { + + hook := new(Hook) + logrus.AddHook(hook) + + return hook + +} + +// NewLocal installs a test hook for a given local logger. +func NewLocal(logger *logrus.Logger) *Hook { + + hook := new(Hook) + logger.Hooks.Add(hook) + + return hook + +} + +// NewNullLogger creates a discarding logger and installs the test hook. +func NewNullLogger() (*logrus.Logger, *Hook) { + + logger := logrus.New() + logger.Out = ioutil.Discard + + return logger, NewLocal(logger) + +} + +func (t *Hook) Fire(e *logrus.Entry) error { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = append(t.Entries, *e) + return nil +} + +func (t *Hook) Levels() []logrus.Level { + return logrus.AllLevels +} + +// LastEntry returns the last entry that was logged or nil. +func (t *Hook) LastEntry() *logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + i := len(t.Entries) - 1 + if i < 0 { + return nil + } + return &t.Entries[i] +} + +// AllEntries returns all entries that were logged. +func (t *Hook) AllEntries() []*logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + // Make a copy so the returned value won't race with future log requests + entries := make([]*logrus.Entry, len(t.Entries)) + for i := 0; i < len(t.Entries); i++ { + // Make a copy, for safety + entries[i] = &t.Entries[i] + } + return entries +} + +// Reset removes all Entries from this test hook. +func (t *Hook) Reset() { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = make([]logrus.Entry, 0) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d968afe3c11..6c3dafc3d1f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -57,6 +57,7 @@ github.com/shurcooL/sanitized_anchor_name # github.com/sirupsen/logrus v1.7.0 ## explicit github.com/sirupsen/logrus +github.com/sirupsen/logrus/hooks/test # github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 ## explicit github.com/syndtr/gocapability/capability