diff --git a/libcontainer/capabilities/capabilities.go b/libcontainer/capabilities/capabilities.go index adbf6330c48..60022ab99b2 100644 --- a/libcontainer/capabilities/capabilities.go +++ b/libcontainer/capabilities/capabilities.go @@ -3,10 +3,10 @@ package capabilities import ( - "fmt" "strings" "github.com/opencontainers/runc/libcontainer/configs" + "github.com/sirupsen/logrus" "github.com/syndtr/gocapability/capability" ) @@ -24,27 +24,31 @@ func init() { } } -// New creates a new Caps from the given Capabilities config. +// New creates a new Caps from the given Capabilities config. Unknown Capabilities +// 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 - caps Caps + err error + caps Caps + ignored []string + warnings = make([]string, 0) ) - if caps.bounding, err = capSlice(capConfig.Bounding); err != nil { - return nil, err + if caps.bounding, ignored = capSlice(capConfig.Bounding); len(ignored) > 0 { + warnings = append(warnings, ignored...) } - if caps.effective, err = capSlice(capConfig.Effective); err != nil { - return nil, err + if caps.effective, ignored = capSlice(capConfig.Effective); len(ignored) > 0 { + warnings = append(warnings, ignored...) } - if caps.inheritable, err = capSlice(capConfig.Inheritable); err != nil { - return nil, err + if caps.inheritable, ignored = capSlice(capConfig.Inheritable); len(ignored) > 0 { + warnings = append(warnings, ignored...) } - if caps.permitted, err = capSlice(capConfig.Permitted); err != nil { - return nil, err + if caps.permitted, ignored = capSlice(capConfig.Permitted); len(ignored) > 0 { + warnings = append(warnings, ignored...) } - if caps.ambient, err = capSlice(capConfig.Ambient); err != nil { - return nil, err + if caps.ambient, ignored = capSlice(capConfig.Ambient); len(ignored) > 0 { + warnings = append(warnings, ignored...) } if caps.pid, err = capability.NewPid2(0); err != nil { return nil, err @@ -52,19 +56,37 @@ func New(capConfig *configs.Capabilities) (*Caps, error) { if err = caps.pid.Load(); err != nil { return nil, err } + printWarnings(warnings) return &caps, nil } -func capSlice(caps []string) ([]capability.Cap, error) { +func printWarnings(warnings []string) { + if len(warnings) == 0 { + return + } + var unknownCaps = map[string]struct{}{} + for _, c := range warnings { + if _, ok := unknownCaps[c]; !ok { + logrus.WithField("capability", c).Warn("ignoring unknown or unavailable capability") + unknownCaps[c] = struct{}{} + } + } +} + +// capSlice converts the given slice of capability names, and converts them +// to their numeric equivalent. Names of unknown or unavailable capabilities +// are returned and can be used by the caller to print as a warning. +func capSlice(caps []string) ([]capability.Cap, []string) { out := make([]capability.Cap, len(caps)) + ignored := make([]string, 0) for i, c := range caps { v, ok := capabilityMap[c] if !ok { - return nil, fmt.Errorf("unknown capability %q", c) + ignored = append(ignored, c) } out[i] = v } - return out, nil + return out, 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..4bb6db9955f --- /dev/null +++ b/libcontainer/capabilities/capabilities_linux_test.go @@ -0,0 +1,59 @@ +package capabilities + +import ( + "io/ioutil" + "os" + "testing" + + "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"} + conf := configs.Capabilities{ + Bounding: cs, + Effective: cs, + Inheritable: cs, + Permitted: cs, + Ambient: cs, + } + + hook := test.NewGlobal() + defer hook.Reset() + + logrus.SetOutput(ioutil.Discard) + _, 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)) + } + + expected := logrus.Entry{ + Data: logrus.Fields{"capability": "CAP_UNKNOWN"}, + Level: logrus.WarnLevel, + Message: "ignoring unknown or unavailable capability", + } + + l := hook.LastEntry() + if l == nil { + t.Fatal("expected a warning, but got none") + } + if l.Level != expected.Level { + t.Errorf("expected %s, got %s", expected.Level, l.Level) + } + if l.Data["capability"] != expected.Data["capability"] { + t.Errorf("expected %v, got %v", expected.Data["capability"], l.Data["capability"]) + } + if l.Message != expected.Message { + t.Errorf("expected %s, got %s", expected.Message, l.Message) + } + + 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 940300a1e18..f9f836c3236 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