Skip to content

Commit 89430ff

Browse files
wkingrh-atomic-bot
authored andcommitted
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]: #686 (comment) [2]: https://en.wikipedia.org/wiki/Alphabetical_order#Language-specific_conventions [3]: golang/go#25340 Signed-off-by: W. Trevor King <[email protected]> Closes: #686 Approved by: mheon
1 parent 4b22913 commit 89430ff

File tree

5 files changed

+157
-20
lines changed

5 files changed

+157
-20
lines changed

libpod/container_internal.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,23 @@ import (
3232
"github.com/sirupsen/logrus"
3333
"github.com/ulule/deepcopier"
3434
"golang.org/x/sys/unix"
35+
"golang.org/x/text/language"
3536
)
3637

3738
const (
3839
// name of the directory holding the artifacts
3940
artifactsDir = "artifacts"
4041
)
4142

43+
var (
44+
// localeToLanguage maps from locale values to language tags.
45+
localeToLanguage = map[string]string{
46+
"": "und-u-va-posix",
47+
"c": "und-u-va-posix",
48+
"posix": "und-u-va-posix",
49+
}
50+
)
51+
4252
// rootFsSize gets the size of the container's root filesystem
4353
// A container FS is split into two parts. The first is the top layer, a
4454
// 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
12871297
return nil
12881298
}
12891299

1290-
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir})
1300+
var locale string
1301+
var ok bool
1302+
for _, envVar := range []string{
1303+
"LC_ALL",
1304+
"LC_COLLATE",
1305+
"LANG",
1306+
} {
1307+
locale, ok = os.LookupEnv(envVar)
1308+
if ok {
1309+
break
1310+
}
1311+
}
1312+
1313+
langString, ok := localeToLanguage[strings.ToLower(locale)]
1314+
if !ok {
1315+
langString = locale
1316+
}
1317+
1318+
lang, err := language.Parse(langString)
1319+
if err != nil {
1320+
logrus.Warnf("failed to parse language %q: %s", langString, err)
1321+
lang, err = language.Parse("und-u-va-posix")
1322+
if err != nil {
1323+
return err
1324+
}
1325+
}
1326+
1327+
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir}, lang)
12911328
if err != nil {
12921329
if c.runtime.config.HooksDirNotExistFatal || !os.IsNotExist(err) {
12931330
return err

pkg/hooks/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ For both `crio` and `podman`, hooks are read from `/usr/share/containers/oci/hoo
2323
For `crio`, hook JSON is also read from `/etc/containers/oci/hooks.d/*.json`.
2424
If files of with the same name exist in both directories, the one in `/etc/containers/oci/hooks.d` takes precedence.
2525

26+
Hooks MUST be injected in the JSON filename case- and width-insensitive collation order.
27+
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).
28+
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`.
29+
2630
Each JSON file should contain an object with the following properties:
2731

2832
### 1.0.0 Hook Schema
@@ -160,6 +164,10 @@ $ cat /etc/containers/oci/hooks.d/osystemd-hook.json
160164
```
161165

162166
[JSON]: https://tools.ietf.org/html/rfc8259
167+
[LANG]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02
168+
[LC_ALL]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02
169+
[LC_COLLATE]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_02
170+
[LC_COLLATE-POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_02_06
163171
[nvidia-container-runtime-hook]: https://github.com/NVIDIA/nvidia-container-runtime/tree/master/hook/nvidia-container-runtime-hook
164172
[oci-systemd-hook]: https://github.com/projectatomic/oci-systemd-hook
165173
[oci-umount]: https://github.com/projectatomic/oci-umount

pkg/hooks/hooks.go

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
rspec "github.com/opencontainers/runtime-spec/specs-go"
1111
"github.com/pkg/errors"
1212
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
13+
"golang.org/x/text/collate"
14+
"golang.org/x/text/language"
1315
)
1416

1517
// Version is the current hook configuration version.
@@ -26,18 +28,27 @@ const (
2628
// Manager provides an opaque interface for managing CRI-O hooks.
2729
type Manager struct {
2830
hooks map[string]*current.Hook
31+
language language.Tag
2932
directories []string
3033
lock sync.Mutex
3134
}
3235

36+
type namedHook struct {
37+
name string
38+
hook *current.Hook
39+
}
40+
41+
type namedHooks []*namedHook
42+
3343
// New creates a new hook manager. Directories are ordered by
3444
// increasing preference (hook configurations in later directories
3545
// override configurations with the same filename from earlier
3646
// directories).
37-
func New(ctx context.Context, directories []string) (manager *Manager, err error) {
47+
func New(ctx context.Context, directories []string, lang language.Tag) (manager *Manager, err error) {
3848
manager = &Manager{
3949
hooks: map[string]*current.Hook{},
4050
directories: directories,
51+
language: lang,
4152
}
4253

4354
for _, dir := range directories {
@@ -50,29 +61,48 @@ func New(ctx context.Context, directories []string) (manager *Manager, err error
5061
return manager, nil
5162
}
5263

53-
// Hooks injects OCI runtime hooks for a given container configuration.
54-
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (err error) {
64+
// filenames returns sorted hook entries.
65+
func (m *Manager) namedHooks() (hooks []*namedHook) {
5566
m.lock.Lock()
5667
defer m.lock.Unlock()
68+
69+
hooks = make([]*namedHook, len(m.hooks))
70+
i := 0
5771
for name, hook := range m.hooks {
58-
match, err := hook.When.Match(config, annotations, hasBindMounts)
72+
hooks[i] = &namedHook{
73+
name: name,
74+
hook: hook,
75+
}
76+
i++
77+
}
78+
79+
return hooks
80+
}
81+
82+
// Hooks injects OCI runtime hooks for a given container configuration.
83+
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (err error) {
84+
hooks := m.namedHooks()
85+
collator := collate.New(m.language, collate.IgnoreCase, collate.IgnoreWidth)
86+
collator.Sort(namedHooks(hooks))
87+
for _, namedHook := range hooks {
88+
match, err := namedHook.hook.When.Match(config, annotations, hasBindMounts)
5989
if err != nil {
60-
return errors.Wrapf(err, "matching hook %q", name)
90+
return errors.Wrapf(err, "matching hook %q", namedHook.name)
6191
}
6292
if match {
6393
if config.Hooks == nil {
6494
config.Hooks = &rspec.Hooks{}
6595
}
66-
for _, stage := range hook.Stages {
96+
for _, stage := range namedHook.hook.Stages {
6797
switch stage {
6898
case "prestart":
69-
config.Hooks.Prestart = append(config.Hooks.Prestart, hook.Hook)
99+
config.Hooks.Prestart = append(config.Hooks.Prestart, namedHook.hook.Hook)
70100
case "poststart":
71-
config.Hooks.Poststart = append(config.Hooks.Poststart, hook.Hook)
101+
config.Hooks.Poststart = append(config.Hooks.Poststart, namedHook.hook.Hook)
72102
case "poststop":
73-
config.Hooks.Poststop = append(config.Hooks.Poststop, hook.Hook)
103+
config.Hooks.Poststop = append(config.Hooks.Poststop, namedHook.hook.Hook)
74104
default:
75-
return fmt.Errorf("hook %q: unknown stage %q", name, stage)
105+
return fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage)
76106
}
77107
}
78108
}
@@ -102,3 +132,18 @@ func (m *Manager) add(path string) (err error) {
102132
m.hooks[filepath.Base(path)] = hook
103133
return nil
104134
}
135+
136+
// Len is part of the collate.Lister interface.
137+
func (hooks namedHooks) Len() int {
138+
return len(hooks)
139+
}
140+
141+
// Swap is part of the collate.Lister interface.
142+
func (hooks namedHooks) Swap(i, j int) {
143+
hooks[i], hooks[j] = hooks[j], hooks[i]
144+
}
145+
146+
// Bytes is part of the collate.Lister interface.
147+
func (hooks namedHooks) Bytes(i int) []byte {
148+
return []byte(hooks[i].name)
149+
}

pkg/hooks/hooks_test.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
rspec "github.com/opencontainers/runtime-spec/specs-go"
1313
current "github.com/projectatomic/libpod/pkg/hooks/1.0.0"
1414
"github.com/stretchr/testify/assert"
15+
"golang.org/x/text/language"
1516
)
1617

1718
// path is the path to an example hook executable.
@@ -26,13 +27,28 @@ func TestGoodNew(t *testing.T) {
2627
}
2728
defer os.RemoveAll(dir)
2829

29-
jsonPath := filepath.Join(dir, "a.json")
30-
err = ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\", \"poststart\", \"poststop\"]}", path)), 0644)
30+
for i, name := range []string{
31+
"01-my-hook.json",
32+
"01-UPPERCASE.json",
33+
"02-another-hook.json",
34+
} {
35+
jsonPath := filepath.Join(dir, name)
36+
var extraStages string
37+
if i == 0 {
38+
extraStages = ", \"poststart\", \"poststop\""
39+
}
40+
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)
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
}
45+
46+
lang, err := language.Parse("und-u-va-posix")
3147
if err != nil {
3248
t.Fatal(err)
3349
}
3450

35-
manager, err := New(ctx, []string{dir})
51+
manager, err := New(ctx, []string{dir}, lang)
3652
if err != nil {
3753
t.Fatal(err)
3854
}
@@ -43,20 +59,34 @@ func TestGoodNew(t *testing.T) {
4359
t.Fatal(err)
4460
}
4561

62+
one := 1
63+
two := 2
64+
three := 3
4665
assert.Equal(t, &rspec.Hooks{
4766
Prestart: []rspec.Hook{
4867
{
49-
Path: path,
68+
Path: path,
69+
Timeout: &one,
70+
},
71+
{
72+
Path: path,
73+
Timeout: &two,
74+
},
75+
{
76+
Path: path,
77+
Timeout: &three,
5078
},
5179
},
5280
Poststart: []rspec.Hook{
5381
{
54-
Path: path,
82+
Path: path,
83+
Timeout: &one,
5584
},
5685
},
5786
Poststop: []rspec.Hook{
5887
{
59-
Path: path,
88+
Path: path,
89+
Timeout: &one,
6090
},
6191
},
6292
}, config.Hooks)
@@ -77,7 +107,12 @@ func TestBadNew(t *testing.T) {
77107
t.Fatal(err)
78108
}
79109

80-
_, err = New(ctx, []string{dir})
110+
lang, err := language.Parse("und-u-va-posix")
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
115+
_, err = New(ctx, []string{dir}, lang)
81116
if err == nil {
82117
t.Fatal("unexpected success")
83118
}

pkg/hooks/monitor_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
rspec "github.com/opencontainers/runtime-spec/specs-go"
1313
"github.com/stretchr/testify/assert"
14+
"golang.org/x/text/language"
1415
)
1516

1617
func TestMonitorGood(t *testing.T) {
@@ -21,7 +22,12 @@ func TestMonitorGood(t *testing.T) {
2122
}
2223
defer os.RemoveAll(dir)
2324

24-
manager, err := New(ctx, []string{dir})
25+
lang, err := language.Parse("und-u-va-posix")
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
manager, err := New(ctx, []string{dir}, lang)
2531
if err != nil {
2632
t.Fatal(err)
2733
}
@@ -114,7 +120,13 @@ func TestMonitorGood(t *testing.T) {
114120

115121
func TestMonitorBadWatcher(t *testing.T) {
116122
ctx := context.Background()
117-
manager, err := New(ctx, []string{})
123+
124+
lang, err := language.Parse("und-u-va-posix")
125+
if err != nil {
126+
t.Fatal(err)
127+
}
128+
129+
manager, err := New(ctx, []string{}, lang)
118130
if err != nil {
119131
t.Fatal(err)
120132
}

0 commit comments

Comments
 (0)