mirror of
https://github.com/containers/podman.git
synced 2025-10-17 03:04:21 +08:00
hooks: Order injection by collated JSON filename
We also considered ordering with sort.Strings, but Matthew rejected that because it uses a byte-by-byte UTF-8 comparison [1] which would fail many language-specific conventions [2]. There's some more discussion of the localeToLanguage mapping in [3]. Currently language.Parse does not handle either 'C' or 'POSIX', returning: und, language: tag is not well-formed for both. [1]: https://github.com/projectatomic/libpod/pull/686#issuecomment-387914358 [2]: https://en.wikipedia.org/wiki/Alphabetical_order#Language-specific_conventions [3]: https://github.com/golang/go/issues/25340 Signed-off-by: W. Trevor King <wking@tremily.us> Closes: #686 Approved by: mheon
This commit is contained in:

committed by
Atomic Bot

parent
4b22913e11
commit
89430ffe65
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/ulule/deepcopier"
|
"github.com/ulule/deepcopier"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -39,6 +40,15 @@ const (
|
|||||||
artifactsDir = "artifacts"
|
artifactsDir = "artifacts"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// localeToLanguage maps from locale values to language tags.
|
||||||
|
localeToLanguage = map[string]string{
|
||||||
|
"": "und-u-va-posix",
|
||||||
|
"c": "und-u-va-posix",
|
||||||
|
"posix": "und-u-va-posix",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// rootFsSize gets the size of the container's root filesystem
|
// rootFsSize gets the size of the container's root filesystem
|
||||||
// A container FS is split into two parts. The first is the top layer, a
|
// A container FS is split into two parts. The first is the top layer, a
|
||||||
// mutable layer, and the rest is the RootFS: the set of immutable layers
|
// mutable layer, and the rest is the RootFS: the set of immutable layers
|
||||||
@ -1287,7 +1297,34 @@ func (c *Container) setupOCIHooks(ctx context.Context, g *generate.Generator) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir})
|
var locale string
|
||||||
|
var ok bool
|
||||||
|
for _, envVar := range []string{
|
||||||
|
"LC_ALL",
|
||||||
|
"LC_COLLATE",
|
||||||
|
"LANG",
|
||||||
|
} {
|
||||||
|
locale, ok = os.LookupEnv(envVar)
|
||||||
|
if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
langString, ok := localeToLanguage[strings.ToLower(locale)]
|
||||||
|
if !ok {
|
||||||
|
langString = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
lang, err := language.Parse(langString)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("failed to parse language %q: %s", langString, err)
|
||||||
|
lang, err = language.Parse("und-u-va-posix")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir}, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.runtime.config.HooksDirNotExistFatal || !os.IsNotExist(err) {
|
if c.runtime.config.HooksDirNotExistFatal || !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
|
@ -23,6 +23,10 @@ For both `crio` and `podman`, hooks are read from `/usr/share/containers/oci/hoo
|
|||||||
For `crio`, hook JSON is also read from `/etc/containers/oci/hooks.d/*.json`.
|
For `crio`, hook JSON is also read from `/etc/containers/oci/hooks.d/*.json`.
|
||||||
If files of with the same name exist in both directories, the one in `/etc/containers/oci/hooks.d` takes precedence.
|
If files of with the same name exist in both directories, the one in `/etc/containers/oci/hooks.d` takes precedence.
|
||||||
|
|
||||||
|
Hooks MUST be injected in the JSON filename case- and width-insensitive collation order.
|
||||||
|
Collation order depends on your locale, as set by [`LC_ALL`][LC_ALL], [`LC_COLLATE`][LC_COLLATE], or [`LANG`][LANG] (in order of decreasing precedence).
|
||||||
|
For example, in the [POSIX locale][LC_COLLATE-POSIX], a matching hook defined in `01-my-hook.json` would be injected before matching hooks defined in `02-another-hook.json` and `01-UPPERCASE.json`.
|
||||||
|
|
||||||
Each JSON file should contain an object with the following properties:
|
Each JSON file should contain an object with the following properties:
|
||||||
|
|
||||||
### 1.0.0 Hook Schema
|
### 1.0.0 Hook Schema
|
||||||
@ -160,6 +164,10 @@ $ cat /etc/containers/oci/hooks.d/osystemd-hook.json
|
|||||||
```
|
```
|
||||||
|
|
||||||
[JSON]: https://tools.ietf.org/html/rfc8259
|
[JSON]: https://tools.ietf.org/html/rfc8259
|
||||||
|
[LANG]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02
|
||||||
|
[LC_ALL]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02
|
||||||
|
[LC_COLLATE]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_02
|
||||||
|
[LC_COLLATE-POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_02_06
|
||||||
[nvidia-container-runtime-hook]: https://github.com/NVIDIA/nvidia-container-runtime/tree/master/hook/nvidia-container-runtime-hook
|
[nvidia-container-runtime-hook]: https://github.com/NVIDIA/nvidia-container-runtime/tree/master/hook/nvidia-container-runtime-hook
|
||||||
[oci-systemd-hook]: https://github.com/projectatomic/oci-systemd-hook
|
[oci-systemd-hook]: https://github.com/projectatomic/oci-systemd-hook
|
||||||
[oci-umount]: https://github.com/projectatomic/oci-umount
|
[oci-umount]: https://github.com/projectatomic/oci-umount
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
|
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
|
||||||
|
"golang.org/x/text/collate"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is the current hook configuration version.
|
// Version is the current hook configuration version.
|
||||||
@ -26,18 +28,27 @@ const (
|
|||||||
// Manager provides an opaque interface for managing CRI-O hooks.
|
// Manager provides an opaque interface for managing CRI-O hooks.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
hooks map[string]*current.Hook
|
hooks map[string]*current.Hook
|
||||||
|
language language.Tag
|
||||||
directories []string
|
directories []string
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type namedHook struct {
|
||||||
|
name string
|
||||||
|
hook *current.Hook
|
||||||
|
}
|
||||||
|
|
||||||
|
type namedHooks []*namedHook
|
||||||
|
|
||||||
// New creates a new hook manager. Directories are ordered by
|
// New creates a new hook manager. Directories are ordered by
|
||||||
// increasing preference (hook configurations in later directories
|
// increasing preference (hook configurations in later directories
|
||||||
// override configurations with the same filename from earlier
|
// override configurations with the same filename from earlier
|
||||||
// directories).
|
// directories).
|
||||||
func New(ctx context.Context, directories []string) (manager *Manager, err error) {
|
func New(ctx context.Context, directories []string, lang language.Tag) (manager *Manager, err error) {
|
||||||
manager = &Manager{
|
manager = &Manager{
|
||||||
hooks: map[string]*current.Hook{},
|
hooks: map[string]*current.Hook{},
|
||||||
directories: directories,
|
directories: directories,
|
||||||
|
language: lang,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range directories {
|
for _, dir := range directories {
|
||||||
@ -50,29 +61,48 @@ func New(ctx context.Context, directories []string) (manager *Manager, err error
|
|||||||
return manager, nil
|
return manager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks injects OCI runtime hooks for a given container configuration.
|
// filenames returns sorted hook entries.
|
||||||
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (err error) {
|
func (m *Manager) namedHooks() (hooks []*namedHook) {
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
defer m.lock.Unlock()
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
hooks = make([]*namedHook, len(m.hooks))
|
||||||
|
i := 0
|
||||||
for name, hook := range m.hooks {
|
for name, hook := range m.hooks {
|
||||||
match, err := hook.When.Match(config, annotations, hasBindMounts)
|
hooks[i] = &namedHook{
|
||||||
|
name: name,
|
||||||
|
hook: hook,
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks injects OCI runtime hooks for a given container configuration.
|
||||||
|
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (err error) {
|
||||||
|
hooks := m.namedHooks()
|
||||||
|
collator := collate.New(m.language, collate.IgnoreCase, collate.IgnoreWidth)
|
||||||
|
collator.Sort(namedHooks(hooks))
|
||||||
|
for _, namedHook := range hooks {
|
||||||
|
match, err := namedHook.hook.When.Match(config, annotations, hasBindMounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "matching hook %q", name)
|
return errors.Wrapf(err, "matching hook %q", namedHook.name)
|
||||||
}
|
}
|
||||||
if match {
|
if match {
|
||||||
if config.Hooks == nil {
|
if config.Hooks == nil {
|
||||||
config.Hooks = &rspec.Hooks{}
|
config.Hooks = &rspec.Hooks{}
|
||||||
}
|
}
|
||||||
for _, stage := range hook.Stages {
|
for _, stage := range namedHook.hook.Stages {
|
||||||
switch stage {
|
switch stage {
|
||||||
case "prestart":
|
case "prestart":
|
||||||
config.Hooks.Prestart = append(config.Hooks.Prestart, hook.Hook)
|
config.Hooks.Prestart = append(config.Hooks.Prestart, namedHook.hook.Hook)
|
||||||
case "poststart":
|
case "poststart":
|
||||||
config.Hooks.Poststart = append(config.Hooks.Poststart, hook.Hook)
|
config.Hooks.Poststart = append(config.Hooks.Poststart, namedHook.hook.Hook)
|
||||||
case "poststop":
|
case "poststop":
|
||||||
config.Hooks.Poststop = append(config.Hooks.Poststop, hook.Hook)
|
config.Hooks.Poststop = append(config.Hooks.Poststop, namedHook.hook.Hook)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("hook %q: unknown stage %q", name, stage)
|
return fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,3 +132,18 @@ func (m *Manager) add(path string) (err error) {
|
|||||||
m.hooks[filepath.Base(path)] = hook
|
m.hooks[filepath.Base(path)] = hook
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len is part of the collate.Lister interface.
|
||||||
|
func (hooks namedHooks) Len() int {
|
||||||
|
return len(hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap is part of the collate.Lister interface.
|
||||||
|
func (hooks namedHooks) Swap(i, j int) {
|
||||||
|
hooks[i], hooks[j] = hooks[j], hooks[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes is part of the collate.Lister interface.
|
||||||
|
func (hooks namedHooks) Bytes(i int) []byte {
|
||||||
|
return []byte(hooks[i].name)
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
|
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// path is the path to an example hook executable.
|
// path is the path to an example hook executable.
|
||||||
@ -26,13 +27,28 @@ func TestGoodNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
jsonPath := filepath.Join(dir, "a.json")
|
for i, name := range []string{
|
||||||
err = ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\", \"poststart\", \"poststop\"]}", path)), 0644)
|
"01-my-hook.json",
|
||||||
|
"01-UPPERCASE.json",
|
||||||
|
"02-another-hook.json",
|
||||||
|
} {
|
||||||
|
jsonPath := filepath.Join(dir, name)
|
||||||
|
var extraStages string
|
||||||
|
if i == 0 {
|
||||||
|
extraStages = ", \"poststart\", \"poststop\""
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\", \"timeout\": %d}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"%s]}", path, i+1, extraStages)), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lang, err := language.Parse("und-u-va-posix")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := New(ctx, []string{dir})
|
manager, err := New(ctx, []string{dir}, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -43,20 +59,34 @@ func TestGoodNew(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
one := 1
|
||||||
|
two := 2
|
||||||
|
three := 3
|
||||||
assert.Equal(t, &rspec.Hooks{
|
assert.Equal(t, &rspec.Hooks{
|
||||||
Prestart: []rspec.Hook{
|
Prestart: []rspec.Hook{
|
||||||
{
|
{
|
||||||
Path: path,
|
Path: path,
|
||||||
|
Timeout: &one,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: path,
|
||||||
|
Timeout: &two,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: path,
|
||||||
|
Timeout: &three,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Poststart: []rspec.Hook{
|
Poststart: []rspec.Hook{
|
||||||
{
|
{
|
||||||
Path: path,
|
Path: path,
|
||||||
|
Timeout: &one,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Poststop: []rspec.Hook{
|
Poststop: []rspec.Hook{
|
||||||
{
|
{
|
||||||
Path: path,
|
Path: path,
|
||||||
|
Timeout: &one,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, config.Hooks)
|
}, config.Hooks)
|
||||||
@ -77,7 +107,12 @@ func TestBadNew(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = New(ctx, []string{dir})
|
lang, err := language.Parse("und-u-va-posix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = New(ctx, []string{dir}, lang)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("unexpected success")
|
t.Fatal("unexpected success")
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMonitorGood(t *testing.T) {
|
func TestMonitorGood(t *testing.T) {
|
||||||
@ -21,7 +22,12 @@ func TestMonitorGood(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
manager, err := New(ctx, []string{dir})
|
lang, err := language.Parse("und-u-va-posix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := New(ctx, []string{dir}, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -114,7 +120,13 @@ func TestMonitorGood(t *testing.T) {
|
|||||||
|
|
||||||
func TestMonitorBadWatcher(t *testing.T) {
|
func TestMonitorBadWatcher(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
manager, err := New(ctx, []string{})
|
|
||||||
|
lang, err := language.Parse("und-u-va-posix")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := New(ctx, []string{}, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user