Skip to content

Commit 45838b9

Browse files
wkingrh-atomic-bot
authored andcommitted
hooks: Add package support for extension stages
We aren't consuming this yet, but these pkg/hooks changes lay the groundwork for future libpod changes to support post-exit hooks [1,2]. [1]: #730 [2]: opencontainers/runc#1797 Signed-off-by: W. Trevor King <[email protected]> Closes: #758 Approved by: rhatdan
1 parent 69a6cb2 commit 45838b9

File tree

9 files changed

+137
-53
lines changed

9 files changed

+137
-53
lines changed

libpod/container_internal.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@ func (c *Container) setupOCIHooks(ctx context.Context, g *generate.Generator) er
13331333
}
13341334
}
13351335

1336-
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir}, lang)
1336+
manager, err := hooks.New(ctx, []string{c.runtime.config.HooksDir}, []string{}, lang)
13371337
if err != nil {
13381338
if c.runtime.config.HooksDirNotExistFatal || !os.IsNotExist(err) {
13391339
return err
@@ -1342,5 +1342,6 @@ func (c *Container) setupOCIHooks(ctx context.Context, g *generate.Generator) er
13421342
return nil
13431343
}
13441344

1345-
return manager.Hooks(g.Spec(), c.Spec().Annotations, len(c.config.UserVolumes) > 0)
1345+
_, err = manager.Hooks(g.Spec(), c.Spec().Annotations, len(c.config.UserVolumes) > 0)
1346+
return err
13461347
}

pkg/hooks/1.0.0/hook.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func Read(content []byte) (hook *Hook, err error) {
3131
}
3232

3333
// Validate performs load-time hook validation.
34-
func (hook *Hook) Validate() (err error) {
34+
func (hook *Hook) Validate(extensionStages []string) (err error) {
3535
if hook == nil {
3636
return errors.New("nil hook")
3737
}
@@ -68,6 +68,10 @@ func (hook *Hook) Validate() (err error) {
6868
}
6969

7070
validStages := map[string]bool{"prestart": true, "poststart": true, "poststop": true}
71+
for _, stage := range extensionStages {
72+
validStages[stage] = true
73+
}
74+
7175
for _, stage := range hook.Stages {
7276
if !validStages[stage] {
7377
return fmt.Errorf("unknown stage %q", stage)

pkg/hooks/1.0.0/hook_test.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ func TestGoodValidate(t *testing.T) {
5151
},
5252
Stages: []string{"prestart"},
5353
}
54-
err := hook.Validate()
54+
err := hook.Validate([]string{})
5555
if err != nil {
5656
t.Fatal(err)
5757
}
5858
}
5959

6060
func TestNilValidation(t *testing.T) {
6161
var hook *Hook
62-
err := hook.Validate()
62+
err := hook.Validate([]string{})
6363
if err == nil {
6464
t.Fatal("unexpected success")
6565
}
@@ -68,7 +68,7 @@ func TestNilValidation(t *testing.T) {
6868

6969
func TestWrongVersion(t *testing.T) {
7070
hook := Hook{Version: "0.1.0"}
71-
err := hook.Validate()
71+
err := hook.Validate([]string{})
7272
if err == nil {
7373
t.Fatal("unexpected success")
7474
}
@@ -80,7 +80,7 @@ func TestNoHookPath(t *testing.T) {
8080
Version: "1.0.0",
8181
Hook: rspec.Hook{},
8282
}
83-
err := hook.Validate()
83+
err := hook.Validate([]string{})
8484
if err == nil {
8585
t.Fatal("unexpected success")
8686
}
@@ -94,7 +94,7 @@ func TestUnknownHookPath(t *testing.T) {
9494
Path: filepath.Join("does", "not", "exist"),
9595
},
9696
}
97-
err := hook.Validate()
97+
err := hook.Validate([]string{})
9898
if err == nil {
9999
t.Fatal("unexpected success")
100100
}
@@ -111,7 +111,7 @@ func TestNoStages(t *testing.T) {
111111
Path: path,
112112
},
113113
}
114-
err := hook.Validate()
114+
err := hook.Validate([]string{})
115115
if err == nil {
116116
t.Fatal("unexpected success")
117117
}
@@ -126,13 +126,27 @@ func TestInvalidStage(t *testing.T) {
126126
},
127127
Stages: []string{"does-not-exist"},
128128
}
129-
err := hook.Validate()
129+
err := hook.Validate([]string{})
130130
if err == nil {
131131
t.Fatal("unexpected success")
132132
}
133133
assert.Regexp(t, "^unknown stage \"does-not-exist\"$", err.Error())
134134
}
135135

136+
func TestExtensionStage(t *testing.T) {
137+
hook := Hook{
138+
Version: "1.0.0",
139+
Hook: rspec.Hook{
140+
Path: path,
141+
},
142+
Stages: []string{"prestart", "b"},
143+
}
144+
err := hook.Validate([]string{"a", "b", "c"})
145+
if err != nil {
146+
t.Fatal(err)
147+
}
148+
}
149+
136150
func TestInvalidAnnotationKey(t *testing.T) {
137151
hook := Hook{
138152
Version: "1.0.0",
@@ -146,7 +160,7 @@ func TestInvalidAnnotationKey(t *testing.T) {
146160
},
147161
Stages: []string{"prestart"},
148162
}
149-
err := hook.Validate()
163+
err := hook.Validate([]string{})
150164
if err == nil {
151165
t.Fatal("unexpected success")
152166
}
@@ -166,7 +180,7 @@ func TestInvalidAnnotationValue(t *testing.T) {
166180
},
167181
Stages: []string{"prestart"},
168182
}
169-
err := hook.Validate()
183+
err := hook.Validate([]string{})
170184
if err == nil {
171185
t.Fatal("unexpected success")
172186
}
@@ -184,7 +198,7 @@ func TestInvalidCommand(t *testing.T) {
184198
},
185199
Stages: []string{"prestart"},
186200
}
187-
err := hook.Validate()
201+
err := hook.Validate([]string{})
188202
if err == nil {
189203
t.Fatal("unexpected success")
190204
}

pkg/hooks/README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Each JSON file should contain an object with the following properties:
4444
Entries MUST be [POSIX extended regular expressions][POSIX-ERE].
4545
* **`hasBindMounts`** (OPTIONAL, boolean) If `hasBindMounts` is true and the caller requested host-to-container bind mounts (beyond those that CRI-O or libpod use by default), this condition matches.
4646
* **`stages`** (REQUIRED, array of strings) Stages when the hook MUST be injected.
47-
Entries MUST be chosen from the 1.0.1 OCI Runtime Specification [hook stages][spec-hooks].
47+
Entries MUST be chosen from the 1.0.1 OCI Runtime Specification [hook stages][spec-hooks] or from extention stages supported by the package consumer.
4848

4949
If *all* of the conditions set in `when` match, then the `hook` MUST be injected for the stages set in `stages`.
5050

@@ -114,10 +114,7 @@ Previous versions of CRI-O and libpod supported the 0.1.0 hook schema:
114114
The injected hook's [`args`][spec-hooks] is `hook` with `arguments` appended.
115115
* **`stages`** (REQUIRED, array of strings) Stages when the hook MUST be injected.
116116
`stage` is an allowed synonym for this property, but you MUST NOT set both `stages` and `stage`.
117-
Entries MUST be chosen from:
118-
* **`prestart`**, to inject [pre-start][].
119-
* **`poststart`**, to inject [post-start][].
120-
* **`poststop`**, to inject [post-stop][].
117+
Entries MUST be chosen from the 1.0.1 OCI Runtime Specification [hook stages][spec-hooks] or from extention stages supported by the package consumer.
121118
* **`cmds`** (OPTIONAL, array of strings) The hook MUST be injected if the configured [`process.args[0]`][spec-process] matches an entry.
122119
`cmd` is an allowed synonym for this property, but you MUST NOT set both `cmds` and `cmd`.
123120
Entries MUST be [POSIX extended regular expressions][POSIX-ERE].

pkg/hooks/hooks.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ const (
2727

2828
// Manager provides an opaque interface for managing CRI-O hooks.
2929
type Manager struct {
30-
hooks map[string]*current.Hook
31-
language language.Tag
32-
directories []string
33-
lock sync.Mutex
30+
hooks map[string]*current.Hook
31+
directories []string
32+
extensionStages []string
33+
language language.Tag
34+
lock sync.Mutex
3435
}
3536

3637
type namedHook struct {
@@ -44,15 +45,16 @@ type namedHooks []*namedHook
4445
// increasing preference (hook configurations in later directories
4546
// override configurations with the same filename from earlier
4647
// directories).
47-
func New(ctx context.Context, directories []string, lang language.Tag) (manager *Manager, err error) {
48+
func New(ctx context.Context, directories []string, extensionStages []string, lang language.Tag) (manager *Manager, err error) {
4849
manager = &Manager{
49-
hooks: map[string]*current.Hook{},
50-
directories: directories,
51-
language: lang,
50+
hooks: map[string]*current.Hook{},
51+
directories: directories,
52+
extensionStages: extensionStages,
53+
language: lang,
5254
}
5355

5456
for _, dir := range directories {
55-
err = ReadDir(dir, manager.hooks)
57+
err = ReadDir(dir, manager.extensionStages, manager.hooks)
5658
if err != nil {
5759
return nil, err
5860
}
@@ -80,14 +82,18 @@ func (m *Manager) namedHooks() (hooks []*namedHook) {
8082
}
8183

8284
// 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) {
85+
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (extensionStages map[string][]rspec.Hook, err error) {
8486
hooks := m.namedHooks()
8587
collator := collate.New(m.language, collate.IgnoreCase, collate.IgnoreWidth)
8688
collator.Sort(namedHooks(hooks))
89+
validStages := map[string]bool{} // beyond the OCI stages
90+
for _, stage := range m.extensionStages {
91+
validStages[stage] = true
92+
}
8793
for _, namedHook := range hooks {
8894
match, err := namedHook.hook.When.Match(config, annotations, hasBindMounts)
8995
if err != nil {
90-
return errors.Wrapf(err, "matching hook %q", namedHook.name)
96+
return extensionStages, errors.Wrapf(err, "matching hook %q", namedHook.name)
9197
}
9298
if match {
9399
if config.Hooks == nil {
@@ -102,12 +108,19 @@ func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBi
102108
case "poststop":
103109
config.Hooks.Poststop = append(config.Hooks.Poststop, namedHook.hook.Hook)
104110
default:
105-
return fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage)
111+
if !validStages[stage] {
112+
return extensionStages, fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage)
113+
}
114+
if extensionStages == nil {
115+
extensionStages = map[string][]rspec.Hook{}
116+
}
117+
extensionStages[stage] = append(extensionStages[stage], namedHook.hook.Hook)
106118
}
107119
}
108120
}
109121
}
110-
return nil
122+
123+
return extensionStages, nil
111124
}
112125

113126
// remove remove a hook by name.
@@ -125,7 +138,7 @@ func (m *Manager) remove(hook string) (ok bool) {
125138
func (m *Manager) add(path string) (err error) {
126139
m.lock.Lock()
127140
defer m.lock.Unlock()
128-
hook, err := Read(path)
141+
hook, err := Read(path, m.extensionStages)
129142
if err != nil {
130143
return err
131144
}

pkg/hooks/hooks_test.go

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ func TestGoodNew(t *testing.T) {
4848
t.Fatal(err)
4949
}
5050

51-
manager, err := New(ctx, []string{dir}, lang)
51+
manager, err := New(ctx, []string{dir}, []string{}, lang)
5252
if err != nil {
5353
t.Fatal(err)
5454
}
5555

5656
config := &rspec.Spec{}
57-
err = manager.Hooks(config, map[string]string{}, false)
57+
extensionStages, err := manager.Hooks(config, map[string]string{}, false)
5858
if err != nil {
5959
t.Fatal(err)
6060
}
@@ -90,6 +90,9 @@ func TestGoodNew(t *testing.T) {
9090
},
9191
},
9292
}, config.Hooks)
93+
94+
var nilExtensionStages map[string][]rspec.Hook
95+
assert.Equal(t, nilExtensionStages, extensionStages)
9396
}
9497

9598
func TestBadNew(t *testing.T) {
@@ -112,7 +115,7 @@ func TestBadNew(t *testing.T) {
112115
t.Fatal(err)
113116
}
114117

115-
_, err = New(ctx, []string{dir}, lang)
118+
_, err = New(ctx, []string{dir}, []string{}, lang)
116119
if err == nil {
117120
t.Fatal("unexpected success")
118121
}
@@ -139,11 +142,14 @@ func TestBrokenMatch(t *testing.T) {
139142
Args: []string{"/bin/sh"},
140143
},
141144
}
142-
err := manager.Hooks(config, map[string]string{}, false)
145+
extensionStages, err := manager.Hooks(config, map[string]string{}, false)
143146
if err == nil {
144147
t.Fatal("unexpected success")
145148
}
146149
assert.Regexp(t, "^matching hook \"a\\.json\": command: error parsing regexp: .*", err.Error())
150+
151+
var nilExtensionStages map[string][]rspec.Hook
152+
assert.Equal(t, nilExtensionStages, extensionStages)
147153
}
148154

149155
func TestInvalidStage(t *testing.T) {
@@ -162,11 +168,60 @@ func TestInvalidStage(t *testing.T) {
162168
},
163169
},
164170
}
165-
err := manager.Hooks(&rspec.Spec{}, map[string]string{}, false)
171+
extensionStages, err := manager.Hooks(&rspec.Spec{}, map[string]string{}, false)
166172
if err == nil {
167173
t.Fatal("unexpected success")
168174
}
169175
assert.Regexp(t, "^hook \"a\\.json\": unknown stage \"does-not-exist\"$", err.Error())
176+
177+
var nilExtensionStages map[string][]rspec.Hook
178+
assert.Equal(t, nilExtensionStages, extensionStages)
179+
}
180+
181+
func TestExtensionStage(t *testing.T) {
182+
always := true
183+
manager := Manager{
184+
hooks: map[string]*current.Hook{
185+
"a.json": {
186+
Version: current.Version,
187+
Hook: rspec.Hook{
188+
Path: "/a/b/c",
189+
},
190+
When: current.When{
191+
Always: &always,
192+
},
193+
Stages: []string{"prestart", "a", "b"},
194+
},
195+
},
196+
extensionStages: []string{"a", "b", "c"},
197+
}
198+
199+
config := &rspec.Spec{}
200+
extensionStages, err := manager.Hooks(config, map[string]string{}, false)
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
205+
assert.Equal(t, &rspec.Hooks{
206+
Prestart: []rspec.Hook{
207+
{
208+
Path: "/a/b/c",
209+
},
210+
},
211+
}, config.Hooks)
212+
213+
assert.Equal(t, map[string][]rspec.Hook{
214+
"a": {
215+
{
216+
Path: "/a/b/c",
217+
},
218+
},
219+
"b": {
220+
{
221+
Path: "/a/b/c",
222+
},
223+
},
224+
}, extensionStages)
170225
}
171226

172227
func init() {

0 commit comments

Comments
 (0)