Merge pull request #14907 from flouthoc/remove-hooks

pkg,libpod: remove `pkg/hooks` and use `hooks` from `c/common`
This commit is contained in:
OpenShift Merge Robot
2022-07-21 09:19:46 +02:00
committed by GitHub
25 changed files with 4 additions and 2897 deletions

View File

@ -77,7 +77,7 @@ BUILDTAGS_CROSS ?= containers_image_openpgp exclude_graphdriver_btrfs exclude_gr
CONTAINER_RUNTIME := $(shell command -v podman 2> /dev/null || echo docker) CONTAINER_RUNTIME := $(shell command -v podman 2> /dev/null || echo docker)
OCI_RUNTIME ?= "" OCI_RUNTIME ?= ""
MANPAGES_MD ?= $(wildcard docs/source/markdown/*.md pkg/*/docs/*.md) MANPAGES_MD ?= $(wildcard docs/source/markdown/*.md)
MANPAGES ?= $(MANPAGES_MD:%.md=%) MANPAGES ?= $(MANPAGES_MD:%.md=%)
MANPAGES_DEST ?= $(subst markdown,man, $(subst source,build,$(MANPAGES))) MANPAGES_DEST ?= $(subst markdown,man, $(subst source,build,$(MANPAGES)))
@ -776,9 +776,7 @@ install.modules-load: # This should only be used by distros which might use ipta
.PHONY: install.man .PHONY: install.man
install.man: install.man:
install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(MANDIR)/man1 install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(MANDIR)/man1
install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(MANDIR)/man5
install ${SELINUXOPT} -m 644 $(filter %.1,$(MANPAGES_DEST)) $(DESTDIR)$(MANDIR)/man1 install ${SELINUXOPT} -m 644 $(filter %.1,$(MANPAGES_DEST)) $(DESTDIR)$(MANDIR)/man1
install ${SELINUXOPT} -m 644 $(filter %.5,$(MANPAGES_DEST)) $(DESTDIR)$(MANDIR)/man5
install ${SELINUXOPT} -m 644 docs/source/markdown/links/*1 $(DESTDIR)$(MANDIR)/man1 install ${SELINUXOPT} -m 644 docs/source/markdown/links/*1 $(DESTDIR)$(MANDIR)/man1
.PHONY: install.completions .PHONY: install.completions

2
go.mod
View File

@ -21,7 +21,6 @@ require (
github.com/coreos/go-systemd/v22 v22.3.2 github.com/coreos/go-systemd/v22 v22.3.2
github.com/coreos/stream-metadata-go v0.0.0-20210225230131-70edb9eb47b3 github.com/coreos/stream-metadata-go v0.0.0-20210225230131-70edb9eb47b3
github.com/cyphar/filepath-securejoin v0.2.3 github.com/cyphar/filepath-securejoin v0.2.3
github.com/davecgh/go-spew v1.1.1
github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001
github.com/docker/distribution v2.8.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.17+incompatible github.com/docker/docker v20.10.17+incompatible
@ -51,7 +50,6 @@ require (
github.com/opencontainers/runtime-spec v1.0.3-0.20211214071223-8958f93039ab github.com/opencontainers/runtime-spec v1.0.3-0.20211214071223-8958f93039ab
github.com/opencontainers/runtime-tools v0.9.1-0.20220714195903-17b3287fafb7 github.com/opencontainers/runtime-tools v0.9.1-0.20220714195903-17b3287fafb7
github.com/opencontainers/selinux v1.10.1 github.com/opencontainers/selinux v1.10.1
github.com/pmezard/go-difflib v1.0.0
github.com/rootless-containers/rootlesskit v1.0.1 github.com/rootless-containers/rootlesskit v1.0.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0

View File

@ -22,12 +22,12 @@ import (
"github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/cgroups"
"github.com/containers/common/pkg/chown" "github.com/containers/common/pkg/chown"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/hooks"
"github.com/containers/common/pkg/hooks/exec"
cutil "github.com/containers/common/pkg/util" cutil "github.com/containers/common/pkg/util"
"github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/ctime" "github.com/containers/podman/v4/pkg/ctime"
"github.com/containers/podman/v4/pkg/hooks"
"github.com/containers/podman/v4/pkg/hooks/exec"
"github.com/containers/podman/v4/pkg/lookup" "github.com/containers/podman/v4/pkg/lookup"
"github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/podman/v4/pkg/selinux" "github.com/containers/podman/v4/pkg/selinux"

View File

@ -1,88 +0,0 @@
// Package hook is the 0.1.0 hook configuration structure.
package hook
import (
"encoding/json"
"errors"
"strings"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
rspec "github.com/opencontainers/runtime-spec/specs-go"
)
// Version is the hook configuration version defined in this package.
const Version = "0.1.0"
// Hook is the hook configuration structure.
type Hook struct {
Hook *string `json:"hook"`
Arguments []string `json:"arguments,omitempty"`
// https://github.com/cri-o/cri-o/pull/1235
Stages []string `json:"stages"`
Stage []string `json:"stage"`
Cmds []string `json:"cmds,omitempty"`
Cmd []string `json:"cmd,omitempty"`
Annotations []string `json:"annotations,omitempty"`
Annotation []string `json:"annotation,omitempty"`
HasBindMounts *bool `json:"hasbindmounts,omitempty"`
}
func Read(content []byte) (hook *current.Hook, err error) {
var raw Hook
if err = json.Unmarshal(content, &raw); err != nil {
return nil, err
}
if raw.Hook == nil {
return nil, errors.New("missing required property: hook")
}
if raw.Stages == nil {
raw.Stages = raw.Stage
} else if raw.Stage != nil {
return nil, errors.New("cannot set both 'stage' and 'stages'")
}
if raw.Stages == nil {
return nil, errors.New("missing required property: stages")
}
if raw.Cmds == nil {
raw.Cmds = raw.Cmd
} else if raw.Cmd != nil {
return nil, errors.New("cannot set both 'cmd' and 'cmds'")
}
if raw.Annotations == nil {
raw.Annotations = raw.Annotation
} else if raw.Annotation != nil {
return nil, errors.New("cannot set both 'annotation' and 'annotations'")
}
hook = &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: *raw.Hook,
},
When: current.When{
Commands: raw.Cmds,
HasBindMounts: raw.HasBindMounts,
Or: true,
},
Stages: raw.Stages,
}
if raw.Arguments != nil {
hook.Hook.Args = append([]string{*raw.Hook}, raw.Arguments...)
}
if raw.Annotations != nil {
hook.When.Annotations = map[string]string{
".*": strings.Join(raw.Annotations, "|"),
}
}
return hook, nil
}

View File

@ -1,182 +0,0 @@
package hook
import (
"testing"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
func TestGood(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stages\": [\"prestart\"], \"cmds\": [\"sh\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Commands: []string{"sh"},
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}
func TestInvalidJSON(t *testing.T) {
_, err := Read([]byte("{"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^unexpected end of JSON input$", err.Error())
}
func TestArguments(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"arguments\": [\"d\", \"e\"], \"stages\": [\"prestart\"], \"cmds\": [\"sh\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
Args: []string{"/a/b/c", "d", "e"},
},
When: current.When{
Commands: []string{"sh"},
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}
func TestEmptyObject(t *testing.T) {
_, err := Read([]byte("{}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^missing required property: hook$", err.Error())
}
func TestNoStages(t *testing.T) {
_, err := Read([]byte("{\"hook\": \"/a/b/c\"}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^missing required property: stages$", err.Error())
}
func TestStage(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stage\": [\"prestart\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{Or: true},
Stages: []string{"prestart"},
}, hook)
}
func TestStagesAndStage(t *testing.T) {
_, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stages\": [\"prestart\"], \"stage\": [\"prestart\"]}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^cannot set both 'stage' and 'stages'$", err.Error())
}
func TestCmd(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stage\": [\"prestart\"], \"cmd\": [\"sh\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Commands: []string{"sh"},
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}
func TestCmdsAndCmd(t *testing.T) {
_, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stages\": [\"prestart\"], \"cmds\": [\"sh\"], \"cmd\": [\"true\"]}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^cannot set both 'cmd' and 'cmds'$", err.Error())
}
func TestAnnotations(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stage\": [\"prestart\"], \"annotations\": [\"a\", \"b\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Annotations: map[string]string{".*": "a|b"},
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}
func TestAnnotation(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stage\": [\"prestart\"], \"annotation\": [\"a\", \"b\"]}"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Annotations: map[string]string{".*": "a|b"},
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}
func TestAnnotationsAndAnnotation(t *testing.T) {
_, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stages\": [\"prestart\"], \"annotations\": [\"a\"], \"annotation\": [\"b\"]}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^cannot set both 'annotation' and 'annotations'$", err.Error())
}
func TestHasBindMounts(t *testing.T) {
hook, err := Read([]byte("{\"hook\": \"/a/b/c\", \"stage\": [\"prestart\"], \"hasbindmounts\": true}"))
if err != nil {
t.Fatal(err)
}
hasBindMounts := true
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
HasBindMounts: &hasBindMounts,
Or: true,
},
Stages: []string{"prestart"},
}, hook)
}

View File

@ -1,89 +0,0 @@
// Package hook is the 1.0.0 hook configuration structure.
package hook
import (
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
rspec "github.com/opencontainers/runtime-spec/specs-go"
)
// Version is the hook configuration version defined in this package.
const Version = "1.0.0"
// Hook is the hook configuration structure.
type Hook struct {
Version string `json:"version"`
Hook rspec.Hook `json:"hook"`
When When `json:"when"`
Stages []string `json:"stages"`
}
// Read reads hook JSON bytes, verifies them, and returns the hook configuration.
func Read(content []byte) (hook *Hook, err error) {
if err = json.Unmarshal(content, &hook); err != nil {
return nil, err
}
return hook, nil
}
// Validate performs load-time hook validation.
func (hook *Hook) Validate(extensionStages []string) (err error) {
if hook == nil {
return errors.New("nil hook")
}
if hook.Version != Version {
return fmt.Errorf("unexpected hook version %q (expecting %v)", hook.Version, Version)
}
if hook.Hook.Path == "" {
return errors.New("missing required property: hook.path")
}
if _, err := os.Stat(hook.Hook.Path); err != nil {
return err
}
for key, value := range hook.When.Annotations {
if _, err = regexp.Compile(key); err != nil {
return fmt.Errorf("invalid annotation key %q: %w", key, err)
}
if _, err = regexp.Compile(value); err != nil {
return fmt.Errorf("invalid annotation value %q: %w", value, err)
}
}
for _, command := range hook.When.Commands {
if _, err = regexp.Compile(command); err != nil {
return fmt.Errorf("invalid command %q: %w", command, err)
}
}
if hook.Stages == nil {
return errors.New("missing required property: stages")
}
validStages := map[string]bool{
"createContainer": true,
"createRuntime": true,
"prestart": true,
"poststart": true,
"poststop": true,
"startContainer": true,
}
for _, stage := range extensionStages {
validStages[stage] = true
}
for _, stage := range hook.Stages {
if !validStages[stage] {
return fmt.Errorf("unknown stage %q", stage)
}
}
return nil
}

View File

@ -1,214 +0,0 @@
package hook
import (
"os"
"path/filepath"
"runtime"
"testing"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
// path is the path to an example hook executable.
var path string
func TestGoodRead(t *testing.T) {
hook, err := Read([]byte("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"/a/b/c\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}"))
if err != nil {
t.Fatal(err)
}
always := true
assert.Equal(t, &Hook{
Version: Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: When{
Always: &always,
},
Stages: []string{"prestart"},
}, hook)
}
func TestInvalidJSON(t *testing.T) {
_, err := Read([]byte("{"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^unexpected end of JSON input$", err.Error())
}
func TestGoodValidate(t *testing.T) {
always := true
hook := &Hook{
Version: Version,
Hook: rspec.Hook{
Path: path,
},
When: When{
Always: &always,
},
Stages: []string{"prestart"},
}
err := hook.Validate([]string{})
if err != nil {
t.Fatal(err)
}
}
func TestNilValidation(t *testing.T) {
var hook *Hook
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^nil hook$", err.Error())
}
func TestWrongVersion(t *testing.T) {
hook := Hook{Version: "0.1.0"}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^unexpected hook version \"0.1.0\" \\(expecting 1.0.0\\)$", err.Error())
}
func TestNoHookPath(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^missing required property: hook.path$", err.Error())
}
func TestUnknownHookPath(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: filepath.Join("does", "not", "exist"),
},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^stat does/not/exist: no such file or directory$", err.Error())
if !os.IsNotExist(err) {
t.Fatal("opaque wrapping for not-exist errors")
}
}
func TestNoStages(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^missing required property: stages$", err.Error())
}
func TestInvalidStage(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
Stages: []string{"does-not-exist"},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^unknown stage \"does-not-exist\"$", err.Error())
}
func TestExtensionStage(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
Stages: []string{"prestart", "b"},
}
err := hook.Validate([]string{"a", "b", "c"})
if err != nil {
t.Fatal(err)
}
}
func TestInvalidAnnotationKey(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
When: When{
Annotations: map[string]string{
"[": "a",
},
},
Stages: []string{"prestart"},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^invalid annotation key \"\\[\": error parsing regexp: .*", err.Error())
}
func TestInvalidAnnotationValue(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
When: When{
Annotations: map[string]string{
"a": "[",
},
},
Stages: []string{"prestart"},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^invalid annotation value \"\\[\": error parsing regexp: .*", err.Error())
}
func TestInvalidCommand(t *testing.T) {
hook := Hook{
Version: "1.0.0",
Hook: rspec.Hook{
Path: path,
},
When: When{
Commands: []string{"["},
},
Stages: []string{"prestart"},
}
err := hook.Validate([]string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^invalid command \"\\[\": error parsing regexp: .*", err.Error())
}
func init() {
if runtime.GOOS != "windows" {
path = "/bin/sh"
} else {
panic("we need a reliable executable path on Windows")
}
}

View File

@ -1,96 +0,0 @@
package hook
import (
"errors"
"fmt"
"regexp"
rspec "github.com/opencontainers/runtime-spec/specs-go"
)
// When holds hook-injection conditions.
type When struct {
Always *bool `json:"always,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Commands []string `json:"commands,omitempty"`
HasBindMounts *bool `json:"hasBindMounts,omitempty"`
// Or enables any-of matching.
//
// Deprecated: this property is for is backwards-compatibility with
// 0.1.0 hooks. It will be removed when we drop support for them.
Or bool `json:"-"`
}
// Match returns true if the given conditions match the configuration.
func (when *When) Match(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (match bool, err error) {
matches := 0
if when.Always != nil {
if *when.Always {
if when.Or {
return true, nil
}
matches++
} else if !when.Or {
return false, nil
}
}
if when.HasBindMounts != nil {
if *when.HasBindMounts && hasBindMounts {
if when.Or {
return true, nil
}
matches++
} else if !when.Or {
return false, nil
}
}
for keyPattern, valuePattern := range when.Annotations {
match := false
for key, value := range annotations {
match, err = regexp.MatchString(keyPattern, key)
if err != nil {
return false, fmt.Errorf("annotation key: %w", err)
}
if match {
match, err = regexp.MatchString(valuePattern, value)
if err != nil {
return false, fmt.Errorf("annotation value: %w", err)
}
if match {
break
}
}
}
if match {
if when.Or {
return true, nil
}
matches++
} else if !when.Or {
return false, nil
}
}
if config.Process != nil && len(when.Commands) > 0 {
if len(config.Process.Args) == 0 {
return false, errors.New("process.args must have at least one entry")
}
command := config.Process.Args[0]
for _, cmdPattern := range when.Commands {
match, err := regexp.MatchString(cmdPattern, command)
if err != nil {
return false, fmt.Errorf("command: %w", err)
}
if match {
return true, nil
}
}
return false, nil
}
return matches > 0, nil
}

View File

@ -1,329 +0,0 @@
package hook
import (
"fmt"
"testing"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
func TestNoMatch(t *testing.T) {
config := &rspec.Spec{}
for _, o := range []bool{true, false} {
or := o
t.Run(fmt.Sprintf("or %t", or), func(t *testing.T) {
when := When{Or: or}
match, err := when.Match(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, match)
})
}
}
func TestAlways(t *testing.T) {
config := &rspec.Spec{}
processStruct := &rspec.Process{
Args: []string{"/bin/sh", "a", "b"},
}
for _, a := range []bool{true, false} {
always := a
for _, o := range []bool{true, false} {
or := o
for _, p := range []*rspec.Process{processStruct, nil} {
process := p
t.Run(fmt.Sprintf("always %t, or %t, has process %t", always, or, process != nil), func(t *testing.T) {
config.Process = process
when := When{Always: &always, Or: or}
match, err := when.Match(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, always, match)
})
}
}
}
}
func TestHasBindMountsAnd(t *testing.T) {
hasBindMounts := true
when := When{HasBindMounts: &hasBindMounts}
config := &rspec.Spec{}
for _, b := range []bool{false, true} {
containerHasBindMounts := b
t.Run(fmt.Sprintf("%t", containerHasBindMounts), func(t *testing.T) {
match, err := when.Match(config, map[string]string{}, containerHasBindMounts)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, containerHasBindMounts, match)
})
}
}
func TestHasBindMountsOr(t *testing.T) {
hasBindMounts := true
when := When{HasBindMounts: &hasBindMounts, Or: true}
config := &rspec.Spec{}
for _, b := range []bool{false, true} {
containerHasBindMounts := b
t.Run(fmt.Sprintf("%t", containerHasBindMounts), func(t *testing.T) {
match, err := when.Match(config, map[string]string{}, containerHasBindMounts)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, containerHasBindMounts, match)
})
}
}
func TestAnnotations(t *testing.T) {
when := When{
Annotations: map[string]string{
"^a$": "^b$",
"^c$": "^d$",
},
}
config := &rspec.Spec{}
for _, tt := range []struct {
name string
annotations map[string]string
or bool
match bool
}{
{
name: "matching both, and",
annotations: map[string]string{
"a": "b",
"c": "d",
"e": "f",
},
or: false,
match: true,
},
{
name: "matching one, and",
annotations: map[string]string{
"a": "b",
},
or: false,
match: false,
},
{
name: "matching one, or",
annotations: map[string]string{
"a": "b",
},
or: true,
match: true,
},
{
name: "key-only, or",
annotations: map[string]string{
"a": "bc",
},
or: true,
match: false,
},
{
name: "value-only, or",
annotations: map[string]string{
"ac": "b",
},
or: true,
match: false,
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
when.Or = test.or
match, err := when.Match(config, test.annotations, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.match, match)
})
}
}
func TestCommands(t *testing.T) {
when := When{
Commands: []string{
"^/bin/sh$",
},
}
config := &rspec.Spec{}
for _, tt := range []struct {
name string
process *rspec.Process
match bool
}{
{
name: "good",
process: &rspec.Process{
Args: []string{"/bin/sh", "a", "b"},
},
match: true,
},
{
name: "extra characters",
process: &rspec.Process{
Args: []string{"/bin/shell", "a", "b"},
},
match: false,
},
{
name: "process unset",
match: false,
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
config.Process = test.process
match, err := when.Match(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.match, match)
})
}
}
func TestCommandsEmptyProcessArgs(t *testing.T) {
when := When{
Commands: []string{
"^/bin/sh$",
},
}
config := &rspec.Spec{
Process: &rspec.Process{},
}
_, err := when.Match(config, map[string]string{}, false)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^process\\.args must have at least one entry$", err.Error())
}
func TestHasBindMountsAndCommands(t *testing.T) {
hasBindMounts := true
when := When{
HasBindMounts: &hasBindMounts,
Commands: []string{
"^/bin/sh$",
},
}
config := &rspec.Spec{Process: &rspec.Process{}}
for _, tt := range []struct {
name string
command string
hasBindMounts bool
or bool
match bool
}{
{
name: "both, and",
command: "/bin/sh",
hasBindMounts: true,
or: false,
match: true,
},
{
name: "both, or",
command: "/bin/sh",
hasBindMounts: true,
or: true,
match: true,
},
{
name: "bind, and",
command: "/bin/shell",
hasBindMounts: true,
or: false,
match: false,
},
{
name: "bind, or",
command: "/bin/shell",
hasBindMounts: true,
or: true,
match: true,
},
{
name: "command, and",
command: "/bin/sh",
hasBindMounts: false,
or: false,
match: false,
},
{
name: "command, or",
command: "/bin/sh",
hasBindMounts: false,
or: true,
match: true,
},
{
name: "neither, and",
command: "/bin/shell",
hasBindMounts: false,
or: false,
match: false,
},
{
name: "neither, or",
command: "/bin/shell",
hasBindMounts: false,
or: true,
match: false,
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
config.Process.Args = []string{test.command}
when.Or = test.or
match, err := when.Match(config, map[string]string{}, test.hasBindMounts)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.match, match)
})
}
}
func TestInvalidRegexp(t *testing.T) {
config := &rspec.Spec{Process: &rspec.Process{Args: []string{"/bin/sh"}}}
for _, tt := range []struct {
name string
when When
expected string
}{
{
name: "invalid-annotation-key",
when: When{Annotations: map[string]string{"[": "a"}},
expected: "^annotation key: error parsing regexp: .*",
},
{
name: "invalid-annotation-value",
when: When{Annotations: map[string]string{"a": "["}},
expected: "^annotation value: error parsing regexp: .*",
},
{
name: "invalid-command",
when: When{Commands: []string{"["}},
expected: "^command: error parsing regexp: .*",
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
_, err := test.when.Match(config, map[string]string{"a": "b"}, false)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, test.expected, err.Error())
})
}
}

View File

@ -1,22 +0,0 @@
# OCI Hooks Configuration
For POSIX platforms, the [OCI runtime configuration][runtime-spec] supports [hooks][spec-hooks] for configuring custom actions related to the life cycle of the container.
The way you enable the hooks above is by editing the OCI runtime configuration before running the OCI runtime (e.g. [`runc`][runc]).
CRI-O and `podman create` create the OCI configuration for you, and this documentation allows developers to configure them to set their intended hooks.
One problem with hooks is that the runtime actually stalls execution of the container before running the hooks and stalls completion of the container, until all hooks complete.
This can cause some performance issues.
Also a lot of hooks just check if certain configuration is set and then exit early, without doing anything.
For example the [oci-systemd-hook][] only executes if the command is `init` or `systemd`, otherwise it just exits.
This means if we automatically enabled all hooks, every container would have to execute `oci-systemd-hook`, even if they don't run systemd inside of the container.
Performance would also suffer if we executed each hook at each stage ([pre-start][], [post-start][], and [post-stop][]).
The hooks configuration is documented in [`oci-hooks.5`](docs/oci-hooks.5.md).
[oci-systemd-hook]: https://github.com/projectatomic/oci-systemd-hook
[post-start]: https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#poststart
[post-stop]: https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#poststop
[pre-start]: https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#prestart
[runc]: https://github.com/opencontainers/runc
[runtime-spec]: https://github.com/opencontainers/runtime-spec/blob/v1.0.1/spec.md
[spec-hooks]: https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#posix-platform-hooks

View File

@ -1 +0,0 @@
*.5

View File

@ -1,182 +0,0 @@
% oci-hooks 5 OCI Hooks Configuration
% W. Trevor King
% MAY 2018
# NAME
oci-hooks - OCI hooks configuration directories
# SYNOPSIS
`/usr/share/containers/oci/hooks.d/*.json`
# DESCRIPTION
Provides a way for users to configure the intended hooks for Open Container Initiative containers so they will only be executed for containers that need their functionality, and then only for the stages where they're needed.
## Directories
Hooks are configured with JSON files (ending with a `.json` extension) in a series of hook directories.
The default directory is `/usr/share/containers/oci/hooks.d`, but tools consuming this format may change that default, include additional directories, or provide their callers with ways to adjust the configuration directories.
If multiple directories are configured, a JSON filename in a preferred directory masks entries with the same filename in directories with lower precedence. For example, if a consuming tool watches for hooks in `/etc/containers/oci/hooks.d` and `/usr/share/containers/oci/hooks.d` (in order of decreasing precedence), then a hook definition in `/etc/containers/oci/hooks.d/01-my-hook.json` will mask any definition in `/usr/share/containers/oci/hooks.d/01-my-hook.json`.
Tools consuming this format may also opt to monitor the hook directories for changes, in which case they will notice additions, changes, and removals to JSON files without needing to be restarted or otherwise signaled. When the tool monitors multiple hooks directories, the precedence discussed in the previous paragraph still applies. For example, if a consuming tool watches for hooks in `/etc/containers/oci/hooks.d` and `/usr/share/containers/oci/hooks.d` (in order of decreasing precedence), then writing a new hook definition to `/etc/containers/oci/hooks.d/01-my-hook.json` will mask the hook previously loaded from `/usr/share/containers/oci/hooks.d/01-my-hook.json`. Subsequent changes to `/usr/share/containers/oci/hooks.d/01-my-hook.json` will have no effect on the consuming tool as long as `/etc/containers/oci/hooks.d/01-my-hook.json` exists. Removing `/etc/containers/oci/hooks.d/01-my-hook.json` will reload the hook from `/usr/share/containers/oci/hooks.d/01-my-hook.json`.
Hooks are injected in the order obtained by sorting the JSON file names, after converting them to lower case, based on their Unicode code points.
For example, 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`.
It is strongly recommended to make the sort order unambiguous depending on an ASCII-only prefix (like the `01`/`02` above).
Each JSON file should contain an object with one of the following schemas.
## 1.0.0 Hook Schema
`version` (required string)
Sets the hook-definition version. For this schema version, the value be `1.0.0`.
`hook` (required object)
The hook to inject, with the hook-entry schema defined by the 1.0.1 OCI Runtime Specification.
`when` (required object)
Conditions under which the hook is injected. The following properties can be specified, and at least one must be specified:
* `always` (optional boolean)
If set `true`, this condition matches.
* `annotations` (optional object)
If all `annotations` key/value pairs match a key/value pair from the configured annotations, this condition matches.
Both keys and values must be POSIX extended regular expressions.
* `commands` (optional array of strings)
If the configured `process.args[0]` matches an entry, this condition matches.
Entries must be POSIX extended regular expressions.
* `hasBindMounts` (optional boolean)
If `hasBindMounts` is true and the caller requested host-to-container bind mounts, this condition matches.
`stages` (required array of strings)
Stages when the hook must be injected. Entries must be chosen from the 1.0.1 OCI Runtime Specification hook stages or from extension stages supported by the package consumer.
If *all* of the conditions set in `when` match, then the `hook` must be injected for the stages set in `stages`.
## 0.1.0 Hook Schema
`hook` (required string)
Sets `path` in the injected hook.
`arguments` (optional array of strings)
Additional arguments to pass to the hook. The injected hook's `args` is `hook` with `arguments` appended.
`stages` (required array of strings)
Stages when the hook must be injected. `stage` is an allowed synonym for this property, but you must not set both `stages` and `stage`. Entries must be chosen from the 1.0.1 OCI Runtime Specification hook stages or from extension stages supported by the package consumer.
`cmds` (optional array of strings)
The hook must be injected if the configured `process.args[0]` matches an entry. `cmd` is an allowed synonym for this property, but you must not set both `cmds` and `cmd`. Entries must be POSIX extended regular expressions.
`annotations` (optional array of strings)
The hook must be injected if an `annotations` entry matches a value from the configured annotations. `annotation` is an allowed synonym for this property, but you must not set both `annotations` and `annotation`. Entries must be POSIX extended regular expressions.
`hasbindmounts` (optional boolean)
The hook must be injected if `hasBindMounts` is true and the caller requested host-to-container bind mounts.
# EXAMPLE
## 1.0.0 Hook Schema
The following configuration injects `oci-systemd-hook` in the pre-start and post-stop stages if `process.args[0]` ends with `/init` or `/systemd`:
```console
$ cat /etc/containers/oci/hooks.d/oci-systemd-hook.json
{
"version": "1.0.0",
"hook": {
"path": "/usr/libexec/oci/hooks.d/oci-systemd-hook"
},
"when": {
"commands": [".*/init$" , ".*/systemd$"]
},
"stages": ["prestart", "poststop"]
}
```
The following example injects `oci-umount --debug` in the pre-start stage if the container is configured to bind-mount host directories into the container.
```console
$ cat /etc/containers/oci/hooks.d/oci-umount.json
{
"version": "1.0.0",
"hook": {
"path": "/usr/libexec/oci/hooks.d/oci-umount",
"args": ["oci-umount", "--debug"],
},
"when": {
"hasBindMounts": true
},
"stages": ["prestart"]
}
```
The following example injects `nvidia-container-runtime-hook prestart` with particular environment variables in the pre-start stage if the container is configured with an `annotations` entry whose key matches `^com\.example\.department$` and whose value matches `.*fluid-dynamics.*`.
```console
$ cat /etc/containers/oci/hooks.d/nvidia.json
{
"version": "1.0.0",
"hook": {
"path": "/usr/sbin/nvidia-container-runtime-hook",
"args": ["nvidia-container-runtime-hook", "prestart"],
"env": [
"NVIDIA_REQUIRE_CUDA=cuda>=9.1",
"NVIDIA_VISIBLE_DEVICES=GPU-fef8089b"
]
},
"when": {
"annotations": {
"^com\\.example\\.department$": ".*fluid-dynamics$"
}
},
"stages": ["prestart"]
}
```
## 0.1.0 Hook Schema
The following configuration injects `oci-systemd-hook` in the pre-start and post-stop stages if `process.args[0]` ends with `/init` or `/systemd`:
```console
$ cat /etc/containers/oci/hooks.d/oci-systemd-hook.json
{
"cmds": [".*/init$" , ".*/systemd$"],
"hook": "/usr/libexec/oci/hooks.d/oci-systemd-hook",
"stages": ["prestart", "poststop"]
}
```
The following example injects `oci-umount --debug` in the pre-start stage if the container is configured to bind-mount host directories into the container.
```console
$ cat /etc/containers/oci/hooks.d/oci-umount.json
{
"hook": "/usr/libexec/oci/hooks.d/oci-umount",
"arguments": ["--debug"],
"hasbindmounts": true,
"stages": ["prestart"]
}
```
The following example injects `nvidia-container-runtime-hook prestart` in the pre-start stage if the container is configured with an `annotations` entry whose value matches `.*fluid-dynamics.*`.
```console
$ cat /etc/containers/oci/hooks.d/osystemd-hook.json
{
"hook": "/usr/sbin/nvidia-container-runtime-hook",
"arguments": ["prestart"],
"annotations: [".*fluid-dynamics.*"],
"stages": ["prestart"]
}
```
# SEE ALSO
`oci-systemd-hook(1)`, `oci-umount(1)`, `locale(7)`
* [OCI Runtime Specification, 1.0.1, POSIX-platform hooks](https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#posix-platform-hooks)
* [OCI Runtime Specification, 1.0.1, process](https://github.com/opencontainers/runtime-spec/blob/v1.0.1/config.md#process)
* [POSIX extended regular expressions (EREs)](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04)

View File

@ -1,69 +0,0 @@
// Package exec provides utilities for executing Open Container Initiative runtime hooks.
package exec
import (
"bytes"
"context"
"fmt"
"io"
osexec "os/exec"
"time"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
// DefaultPostKillTimeout is the recommended default post-kill timeout.
const DefaultPostKillTimeout = time.Duration(10) * time.Second
// Run executes the hook and waits for it to complete or for the
// context or hook-specified timeout to expire.
func Run(ctx context.Context, hook *rspec.Hook, state []byte, stdout io.Writer, stderr io.Writer, postKillTimeout time.Duration) (hookErr, err error) {
cmd := osexec.Cmd{
Path: hook.Path,
Args: hook.Args,
Env: hook.Env,
Stdin: bytes.NewReader(state),
Stdout: stdout,
Stderr: stderr,
}
if cmd.Env == nil {
cmd.Env = []string{}
}
if hook.Timeout != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(*hook.Timeout)*time.Second)
defer cancel()
}
err = cmd.Start()
if err != nil {
return err, err
}
exit := make(chan error, 1)
go func() {
err := cmd.Wait()
if err != nil {
err = fmt.Errorf("executing %v: %w", cmd.Args, err)
}
exit <- err
}()
select {
case err = <-exit:
return err, err
case <-ctx.Done():
if err := cmd.Process.Kill(); err != nil {
logrus.Errorf("Failed to kill pid %v", cmd.Process)
}
timer := time.NewTimer(postKillTimeout)
defer timer.Stop()
select {
case <-timer.C:
err = fmt.Errorf("failed to reap process within %s of the kill signal", postKillTimeout)
case err = <-exit:
}
return err, ctx.Err()
}
}

View File

@ -1,222 +0,0 @@
package exec
import (
"bytes"
"context"
"fmt"
"os"
"runtime"
"strings"
"testing"
"time"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
// path is the path to an example hook executable.
var path string
// unavoidableEnvironmentKeys may be injected even if the hook
// executable is executed with a requested empty environment.
var unavoidableEnvironmentKeys []string
func TestRun(t *testing.T) {
ctx := context.Background()
hook := &rspec.Hook{
Path: path,
Args: []string{"sh", "-c", "cat"},
}
var stderr, stdout bytes.Buffer
hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
if err != nil {
t.Fatal(err)
}
if hookErr != nil {
t.Fatal(hookErr)
}
assert.Equal(t, "{}", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestRunIgnoreOutput(t *testing.T) {
ctx := context.Background()
hook := &rspec.Hook{
Path: path,
Args: []string{"sh", "-c", "cat"},
}
hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
if err != nil {
t.Fatal(err)
}
if hookErr != nil {
t.Fatal(hookErr)
}
}
func TestRunFailedStart(t *testing.T) {
ctx := context.Background()
hook := &rspec.Hook{
Path: "/does/not/exist",
}
hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
if err == nil {
t.Fatal("unexpected success")
}
if !os.IsNotExist(err) {
t.Fatal(err)
}
assert.Equal(t, err, hookErr)
}
func parseEnvironment(input string) (env map[string]string, err error) {
env = map[string]string{}
lines := strings.Split(input, "\n")
for i, line := range lines {
if line == "" && i == len(lines)-1 {
continue // no content after the terminal newline
}
keyValue := strings.SplitN(line, "=", 2)
if len(keyValue) < 2 {
return env, fmt.Errorf("no = in environment line: %q", line)
}
env[keyValue[0]] = keyValue[1]
}
for _, key := range unavoidableEnvironmentKeys {
delete(env, key)
}
return env, nil
}
func TestRunEnvironment(t *testing.T) {
ctx := context.Background()
hook := &rspec.Hook{
Path: path,
Args: []string{"sh", "-c", "env"},
}
for _, tt := range []struct {
name string
env []string
expected map[string]string
}{
{
name: "unset",
expected: map[string]string{},
},
{
name: "set empty",
env: []string{},
expected: map[string]string{},
},
{
name: "set",
env: []string{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm",
},
expected: map[string]string{
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM": "xterm",
},
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
var stderr, stdout bytes.Buffer
hook.Env = test.env
hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
if err != nil {
t.Fatal(err)
}
if hookErr != nil {
t.Fatal(hookErr)
}
assert.Equal(t, "", stderr.String())
env, err := parseEnvironment(stdout.String())
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expected, env)
})
}
}
func TestRunCancel(t *testing.T) {
hook := &rspec.Hook{
Path: path,
Args: []string{"sh", "-c", "echo waiting; sleep 2; echo done"},
}
one := 1
for _, tt := range []struct {
name string
contextTimeout time.Duration
hookTimeout *int
expectedHookError string
expectedRunError error
expectedStdout string
}{
{
name: "no timeouts",
expectedStdout: "waiting\ndone\n",
},
{
name: "context timeout",
contextTimeout: time.Duration(1) * time.Second,
expectedStdout: "waiting\n",
expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
{
name: "hook timeout",
hookTimeout: &one,
expectedStdout: "waiting\n",
expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
var stderr, stdout bytes.Buffer
if test.contextTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, test.contextTimeout)
defer cancel()
}
hook.Timeout = test.hookTimeout
hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
assert.Equal(t, test.expectedRunError, err)
if test.expectedHookError == "" {
if hookErr != nil {
t.Fatal(hookErr)
}
} else {
assert.Regexp(t, test.expectedHookError, hookErr.Error())
}
assert.Equal(t, "", stderr.String())
assert.Equal(t, test.expectedStdout, stdout.String())
})
}
}
func TestRunKillTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(500)*time.Millisecond)
defer cancel()
hook := &rspec.Hook{
Path: path,
Args: []string{"sh", "-c", "sleep 1"},
}
hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, time.Duration(0))
assert.Equal(t, context.DeadlineExceeded, err)
assert.Regexp(t, "^(failed to reap process within 0s of the kill signal|executing \\[sh -c sleep 1]: signal: killed)$", hookErr)
}
func init() {
if runtime.GOOS != "windows" {
path = "/bin/sh"
unavoidableEnvironmentKeys = []string{"PWD", "SHLVL", "_"}
} else {
panic("we need a reliable executable path on Windows")
}
}

View File

@ -1,72 +0,0 @@
package exec
import (
"bytes"
"context"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/davecgh/go-spew/spew"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pmezard/go-difflib/difflib"
"github.com/sirupsen/logrus"
)
var spewConfig = spew.ConfigState{
Indent: " ",
DisablePointerAddresses: true,
DisableCapacities: true,
SortKeys: true,
}
// RuntimeConfigFilter calls a series of hooks. But instead of
// passing container state on their standard input,
// RuntimeConfigFilter passes the proposed runtime configuration (and
// reads back a possibly-altered form from their standard output).
func RuntimeConfigFilter(ctx context.Context, hooks []spec.Hook, config *spec.Spec, postKillTimeout time.Duration) (hookErr, err error) {
data, err := json.Marshal(config)
if err != nil {
return nil, err
}
for i, hook := range hooks {
hook := hook
var stdout bytes.Buffer
hookErr, err = Run(ctx, &hook, data, &stdout, nil, postKillTimeout)
if err != nil {
return hookErr, err
}
data = stdout.Bytes()
var newConfig spec.Spec
err = json.Unmarshal(data, &newConfig)
if err != nil {
logrus.Debugf("invalid JSON from config-filter hook %d:\n%s", i, string(data))
return nil, fmt.Errorf("unmarshal output from config-filter hook %d: %w", i, err)
}
if !reflect.DeepEqual(config, &newConfig) {
oldConfig := spewConfig.Sdump(config)
newConfig := spewConfig.Sdump(&newConfig)
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(oldConfig),
B: difflib.SplitLines(newConfig),
FromFile: "Old",
FromDate: "",
ToFile: "New",
ToDate: "",
Context: 1,
})
if err == nil {
logrus.Debugf("precreate hook %d made configuration changes:\n%s", i, diff)
} else {
logrus.Warnf("Precreate hook %d made configuration changes, but we could not compute a diff: %v", i, err)
}
}
*config = newConfig
}
return nil, nil
}

View File

@ -1,265 +0,0 @@
package exec
import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
func TestRuntimeConfigFilter(t *testing.T) {
unexpectedEndOfJSONInput := json.Unmarshal([]byte("{\n"), nil) //nolint:govet // this should force the error
fileMode := os.FileMode(0600)
rootUint32 := uint32(0)
binUser := int(1)
for _, tt := range []struct {
name string
contextTimeout time.Duration
hooks []spec.Hook
input *spec.Spec
expected *spec.Spec
expectedHookError string
expectedRunError error
expectedRunErrorString string
}{
{
name: "no-op",
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", "cat"},
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
},
{
name: "device injection",
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", `sed 's|\("gid":0}\)|\1,{"path": "/dev/sda","type":"b","major":8,"minor":0,"fileMode":384,"uid":0,"gid":0}|'`},
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
Linux: &spec.Linux{
Devices: []spec.LinuxDevice{
{
Path: "/dev/fuse",
Type: "c",
Major: 10,
Minor: 229,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
},
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
Linux: &spec.Linux{
Devices: []spec.LinuxDevice{
{
Path: "/dev/fuse",
Type: "c",
Major: 10,
Minor: 229,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
{
Path: "/dev/sda",
Type: "b",
Major: 8,
Minor: 0,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
},
},
},
},
{
name: "chaining",
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", `sed 's|\("gid":0}\)|\1,{"path": "/dev/sda","type":"b","major":8,"minor":0,"fileMode":384,"uid":0,"gid":0}|'`},
},
{
Path: path,
Args: []string{"sh", "-c", `sed 's|/dev/sda|/dev/sdb|'`},
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
Linux: &spec.Linux{
Devices: []spec.LinuxDevice{
{
Path: "/dev/fuse",
Type: "c",
Major: 10,
Minor: 229,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
},
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
Linux: &spec.Linux{
Devices: []spec.LinuxDevice{
{
Path: "/dev/fuse",
Type: "c",
Major: 10,
Minor: 229,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
{
Path: "/dev/sdb",
Type: "b",
Major: 8,
Minor: 0,
FileMode: &fileMode,
UID: &rootUint32,
GID: &rootUint32,
},
},
},
},
},
{
name: "context timeout",
contextTimeout: time.Duration(1) * time.Second,
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", "sleep 2"},
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expectedHookError: "^executing \\[sh -c sleep 2]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
{
name: "hook timeout",
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", "sleep 2"},
Timeout: &binUser,
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expectedHookError: "^executing \\[sh -c sleep 2]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
{
name: "invalid JSON",
hooks: []spec.Hook{
{
Path: path,
Args: []string{"sh", "-c", "echo '{'"},
},
},
input: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expected: &spec.Spec{
Version: "1.0.0",
Root: &spec.Root{
Path: "rootfs",
},
},
expectedRunError: unexpectedEndOfJSONInput,
expectedRunErrorString: unexpectedEndOfJSONInput.Error(),
},
} {
test := tt
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
if test.contextTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, test.contextTimeout)
defer cancel()
}
hookErr, err := RuntimeConfigFilter(ctx, test.hooks, test.input, DefaultPostKillTimeout)
if test.expectedRunError != nil {
if test.expectedRunErrorString != "" {
assert.Contains(t, err.Error(), test.expectedRunErrorString)
} else {
assert.True(t, errors.Is(err, test.expectedRunError))
}
}
if test.expectedHookError == "" {
if hookErr != nil {
t.Fatal(hookErr)
}
} else {
assert.Regexp(t, test.expectedHookError, hookErr.Error())
}
assert.Equal(t, test.expected, test.input)
})
}
}

View File

@ -1,145 +0,0 @@
// Package hooks implements hook configuration and handling for CRI-O and libpod.
package hooks
import (
"context"
"fmt"
"os"
"sort"
"strings"
"sync"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
// Version is the current hook configuration version.
const Version = current.Version
const (
// DefaultDir is the default directory containing system hook configuration files.
DefaultDir = "/usr/share/containers/oci/hooks.d"
// OverrideDir is the directory for hook configuration files overriding the default entries.
OverrideDir = "/etc/containers/oci/hooks.d"
)
// Manager provides an opaque interface for managing CRI-O hooks.
type Manager struct {
hooks map[string]*current.Hook
directories []string
extensionStages []string
lock sync.Mutex
}
type namedHook struct {
name string
hook *current.Hook
}
// New creates a new hook manager. Directories are ordered by
// increasing preference (hook configurations in later directories
// override configurations with the same filename from earlier
// directories).
//
// extensionStages allows callers to add additional stages beyond
// those specified in the OCI Runtime Specification and to control
// OCI-defined stages instead of delegating to the OCI runtime. See
// Hooks() for more information.
func New(ctx context.Context, directories []string, extensionStages []string) (manager *Manager, err error) {
manager = &Manager{
hooks: map[string]*current.Hook{},
directories: directories,
extensionStages: extensionStages,
}
for _, dir := range directories {
err = ReadDir(dir, manager.extensionStages, manager.hooks)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
}
return manager, nil
}
// filenames returns sorted hook entries.
func (m *Manager) namedHooks() (hooks []*namedHook) {
m.lock.Lock()
defer m.lock.Unlock()
hooks = make([]*namedHook, len(m.hooks))
i := 0
for name, hook := range m.hooks {
hooks[i] = &namedHook{
name: name,
hook: hook,
}
i++
}
return hooks
}
// Hooks injects OCI runtime hooks for a given container configuration.
//
// If extensionStages was set when initializing the Manager,
// matching hooks requesting those stages will be returned in
// extensionStageHooks. This takes precedence over their inclusion in
// the OCI configuration. For example:
//
// manager, err := New(ctx, []string{DefaultDir}, []string{"poststop"})
// extensionStageHooks, err := manager.Hooks(config, annotations, hasBindMounts)
//
// will have any matching post-stop hooks in extensionStageHooks and
// will not insert them into config.Hooks.Poststop.
func (m *Manager) Hooks(config *rspec.Spec, annotations map[string]string, hasBindMounts bool) (extensionStageHooks map[string][]rspec.Hook, err error) {
hooks := m.namedHooks()
sort.Slice(hooks, func(i, j int) bool { return strings.ToLower(hooks[i].name) < strings.ToLower(hooks[j].name) })
localStages := map[string]bool{} // stages destined for extensionStageHooks
for _, stage := range m.extensionStages {
localStages[stage] = true
}
for _, namedHook := range hooks {
match, err := namedHook.hook.When.Match(config, annotations, hasBindMounts)
if err != nil {
return extensionStageHooks, fmt.Errorf("matching hook %q: %w", namedHook.name, err)
}
if match {
logrus.Debugf("hook %s matched; adding to stages %v", namedHook.name, namedHook.hook.Stages)
if config.Hooks == nil {
config.Hooks = &rspec.Hooks{}
}
for _, stage := range namedHook.hook.Stages {
if _, ok := localStages[stage]; ok {
if extensionStageHooks == nil {
extensionStageHooks = map[string][]rspec.Hook{}
}
extensionStageHooks[stage] = append(extensionStageHooks[stage], namedHook.hook.Hook)
} else {
switch stage {
case "createContainer":
config.Hooks.CreateContainer = append(config.Hooks.CreateContainer, namedHook.hook.Hook)
case "createRuntime":
config.Hooks.CreateRuntime = append(config.Hooks.CreateRuntime, namedHook.hook.Hook)
case "prestart":
config.Hooks.Prestart = append(config.Hooks.Prestart, namedHook.hook.Hook)
case "poststart":
config.Hooks.Poststart = append(config.Hooks.Poststart, namedHook.hook.Hook)
case "poststop":
config.Hooks.Poststop = append(config.Hooks.Poststop, namedHook.hook.Hook)
case "startContainer":
config.Hooks.StartContainer = append(config.Hooks.StartContainer, namedHook.hook.Hook)
default:
return extensionStageHooks, fmt.Errorf("hook %q: unknown stage %q", namedHook.name, stage)
}
}
}
} else {
logrus.Debugf("hook %s did not match", namedHook.name)
}
}
return extensionStageHooks, nil
}

View File

@ -1,218 +0,0 @@
package hooks
import (
"context"
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
"testing"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
// path is the path to an example hook executable.
var path string
func TestGoodNew(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
for i, name := range []string{
"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)
}
}
manager, err := New(ctx, []string{dir}, []string{})
if err != nil {
t.Fatal(err)
}
config := &rspec.Spec{}
extensionStageHooks, err := manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
one := 1
two := 2
three := 3
assert.Equal(t, &rspec.Hooks{
Prestart: []rspec.Hook{
{
Path: path,
Timeout: &one,
},
{
Path: path,
Timeout: &two,
},
{
Path: path,
Timeout: &three,
},
},
Poststart: []rspec.Hook{
{
Path: path,
Timeout: &one,
},
},
Poststop: []rspec.Hook{
{
Path: path,
Timeout: &one,
},
},
}, config.Hooks)
var nilExtensionStageHooks map[string][]rspec.Hook
assert.Equal(t, nilExtensionStageHooks, extensionStageHooks)
}
func TestBadNew(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
jsonPath := filepath.Join(dir, "a.json")
err := ioutil.WriteFile(jsonPath, []byte("{\"version\": \"-1\"}"), 0644)
if err != nil {
t.Fatal(err)
}
_, err = New(ctx, []string{dir}, []string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^parsing hook \"[^\"]*a.json\": unrecognized hook version: \"-1\"$", err.Error())
}
func TestBrokenMatch(t *testing.T) {
manager := Manager{
hooks: map[string]*current.Hook{
"a.json": {
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Commands: []string{"["},
},
Stages: []string{"prestart"},
},
},
}
config := &rspec.Spec{
Process: &rspec.Process{
Args: []string{"/bin/sh"},
},
}
extensionStageHooks, err := manager.Hooks(config, map[string]string{}, false)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^matching hook \"a\\.json\": command: error parsing regexp: .*", err.Error())
var nilExtensionStageHooks map[string][]rspec.Hook
assert.Equal(t, nilExtensionStageHooks, extensionStageHooks)
}
func TestInvalidStage(t *testing.T) {
always := true
manager := Manager{
hooks: map[string]*current.Hook{
"a.json": {
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Always: &always,
},
Stages: []string{"does-not-exist"},
},
},
}
extensionStageHooks, err := manager.Hooks(&rspec.Spec{}, map[string]string{}, false)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^hook \"a\\.json\": unknown stage \"does-not-exist\"$", err.Error())
var nilExtensionStageHooks map[string][]rspec.Hook
assert.Equal(t, nilExtensionStageHooks, extensionStageHooks)
}
func TestExtensionStage(t *testing.T) {
always := true
manager := Manager{
hooks: map[string]*current.Hook{
"a.json": {
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Always: &always,
},
Stages: []string{"prestart", "poststop", "a", "b"},
},
},
extensionStages: []string{"poststop", "a", "b", "c"},
}
config := &rspec.Spec{}
extensionStageHooks, err := manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &rspec.Hooks{
Prestart: []rspec.Hook{
{
Path: "/a/b/c",
},
},
}, config.Hooks)
assert.Equal(t, map[string][]rspec.Hook{
"poststop": {
{
Path: "/a/b/c",
},
},
"a": {
{
Path: "/a/b/c",
},
},
"b": {
{
Path: "/a/b/c",
},
},
}, extensionStageHooks)
}
func init() {
if runtime.GOOS != "windows" {
path = "/bin/sh"
} else {
panic("we need a reliable executable path on Windows")
}
}

View File

@ -1,66 +0,0 @@
package hooks
import (
"context"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
)
// Monitor dynamically monitors hook directories for additions,
// updates, and removals.
//
// This function writes two empty structs to the sync channel: the
// first is written after the watchers are established and the second
// when this function exits. The expected usage is:
//
// ctx, cancel := context.WithCancel(context.Background())
// sync := make(chan error, 2)
// go m.Monitor(ctx, sync)
// err := <-sync // block until writers are established
// if err != nil {
// return err // failed to establish watchers
// }
// // do stuff
// cancel()
// err = <-sync // block until monitor finishes
func (m *Manager) Monitor(ctx context.Context, sync chan<- error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
sync <- err
return
}
defer watcher.Close()
for _, dir := range m.directories {
err = watcher.Add(dir)
if err != nil {
logrus.Errorf("Failed to watch %q for hooks", dir)
sync <- err
return
}
logrus.Debugf("monitoring %q for hooks", dir)
}
sync <- nil
for {
select {
case event := <-watcher.Events:
m.hooks = make(map[string]*current.Hook)
for _, dir := range m.directories {
err = ReadDir(dir, m.extensionStages, m.hooks)
if err != nil {
logrus.Errorf("Failed loading hooks for %s: %v", event.Name, err)
}
}
case <-ctx.Done():
err = ctx.Err()
logrus.Debugf("hook monitoring canceled: %v", err)
sync <- err
close(sync)
return
}
}
}

View File

@ -1,324 +0,0 @@
package hooks
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
func TestMonitorOneDirGood(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
dir := t.TempDir()
manager, err := New(ctx, []string{dir}, []string{})
if err != nil {
t.Fatal(err)
}
sync := make(chan error, 2)
go manager.Monitor(ctx, sync)
err = <-sync
if err != nil {
t.Fatal(err)
}
jsonPath := filepath.Join(dir, "a.json")
t.Run("good-addition", func(t *testing.T) {
err = ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\", \"poststart\", \"poststop\"]}", path)), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &rspec.Hooks{
Prestart: []rspec.Hook{
{
Path: path,
},
},
Poststart: []rspec.Hook{
{
Path: path,
},
},
Poststop: []rspec.Hook{
{
Path: path,
},
},
}, config.Hooks)
})
t.Run("good-removal", func(t *testing.T) {
err = os.Remove(jsonPath)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
expected := config.Hooks
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, config.Hooks)
})
t.Run("bad-addition", func(t *testing.T) {
err = ioutil.WriteFile(jsonPath, []byte("{\"version\": \"-1\"}"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
expected := config.Hooks
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, config.Hooks)
err = os.Remove(jsonPath)
if err != nil {
t.Fatal(err)
}
})
cancel()
err = <-sync
assert.Equal(t, context.Canceled, err)
}
func TestMonitorTwoDirGood(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
primaryDir := t.TempDir()
fallbackDir := t.TempDir()
manager, err := New(ctx, []string{fallbackDir, primaryDir}, []string{})
if err != nil {
t.Fatal(err)
}
sync := make(chan error, 2)
go manager.Monitor(ctx, sync)
err = <-sync
if err != nil {
t.Fatal(err)
}
fallbackPath := filepath.Join(fallbackDir, "a.json")
fallbackJSON := []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}", path))
fallbackInjected := &rspec.Hooks{
Prestart: []rspec.Hook{
{
Path: path,
},
},
}
t.Run("good-fallback-addition", func(t *testing.T) {
err = ioutil.WriteFile(fallbackPath, fallbackJSON, 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, fallbackInjected, config.Hooks)
})
primaryPath := filepath.Join(primaryDir, "a.json")
primaryJSON := []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\", \"timeout\": 1}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}", path))
one := 1
primaryInjected := &rspec.Hooks{
Prestart: []rspec.Hook{
{
Path: path,
Timeout: &one,
},
},
}
t.Run("good-primary-override", func(t *testing.T) {
err = ioutil.WriteFile(primaryPath, primaryJSON, 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, primaryInjected, config.Hooks)
})
t.Run("good-fallback-removal", func(t *testing.T) {
err = os.Remove(fallbackPath)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, primaryInjected, config.Hooks) // masked by primary
})
t.Run("good-fallback-restore", func(t *testing.T) {
err = ioutil.WriteFile(fallbackPath, fallbackJSON, 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, primaryInjected, config.Hooks) // masked by primary
})
primaryPath2 := filepath.Join(primaryDir, "0a.json") // 0a because it will be before a.json alphabetically
t.Run("bad-primary-new-addition", func(t *testing.T) {
err = ioutil.WriteFile(primaryPath2, []byte("{\"version\": \"-1\"}"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
fmt.Println("expected: ", config.Hooks)
expected := primaryInjected // 0a.json is bad, a.json is still good
_, err = manager.Hooks(config, map[string]string{}, false)
fmt.Println("actual: ", config.Hooks)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, config.Hooks)
})
t.Run("bad-primary-same-addition", func(t *testing.T) {
err = ioutil.WriteFile(primaryPath, []byte("{\"version\": \"-1\"}"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
expected := fallbackInjected
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, config.Hooks)
})
t.Run("good-primary-removal", func(t *testing.T) {
err = os.Remove(primaryPath)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, fallbackInjected, config.Hooks)
})
t.Run("good-non-json-addition", func(t *testing.T) {
err = ioutil.WriteFile(filepath.Join(fallbackDir, "README"), []byte("Hello"), 0644)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, fallbackInjected, config.Hooks)
})
t.Run("good-fallback-removal", func(t *testing.T) {
err = os.Remove(fallbackPath)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) // wait for monitor to notice
config := &rspec.Spec{}
expected := config.Hooks
_, err = manager.Hooks(config, map[string]string{}, false)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, config.Hooks)
})
cancel()
err = <-sync
assert.Equal(t, context.Canceled, err)
}
func TestMonitorBadWatcher(t *testing.T) {
ctx := context.Background()
manager, err := New(ctx, []string{}, []string{})
if err != nil {
t.Fatal(err)
}
manager.directories = []string{"/does/not/exist"}
sync := make(chan error, 2)
go manager.Monitor(ctx, sync)
err = <-sync
if !os.IsNotExist(err) {
t.Fatal("opaque wrapping for not-exist errors")
}
}

View File

@ -1,101 +0,0 @@
// Package hooks implements CRI-O's hook handling.
package hooks
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
old "github.com/containers/podman/v4/pkg/hooks/0.1.0"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
"github.com/sirupsen/logrus"
)
type reader func(content []byte) (*current.Hook, error)
var (
// ErrNoJSONSuffix represents hook-add attempts where the filename
// does not end in '.json'.
ErrNoJSONSuffix = errors.New("hook filename does not end in '.json'")
// Readers registers per-version hook readers.
Readers = map[string]reader{}
)
// Read reads a hook JSON file, verifies it, and returns the hook configuration.
func Read(path string, extensionStages []string) (*current.Hook, error) {
if !strings.HasSuffix(path, ".json") {
return nil, ErrNoJSONSuffix
}
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
hook, err := read(content)
if err != nil {
return nil, fmt.Errorf("parsing hook %q: %w", path, err)
}
err = hook.Validate(extensionStages)
return hook, err
}
func read(content []byte) (hook *current.Hook, err error) {
var ver version
if err := json.Unmarshal(content, &ver); err != nil {
return nil, fmt.Errorf("version check: %w", err)
}
reader, ok := Readers[ver.Version]
if !ok {
return nil, fmt.Errorf("unrecognized hook version: %q", ver.Version)
}
hook, err = reader(content)
if err != nil {
return hook, fmt.Errorf("%v: %w", ver.Version, err)
}
return hook, err
}
// ReadDir reads hook JSON files from a directory into the given map,
// clobbering any previous entries with the same filenames.
func ReadDir(path string, extensionStages []string, hooks map[string]*current.Hook) error {
logrus.Debugf("reading hooks from %s", path)
files, err := ioutil.ReadDir(path)
if err != nil {
return err
}
res := err
for _, file := range files {
filePath := filepath.Join(path, file.Name())
hook, err := Read(filePath, extensionStages)
if err != nil {
if err == ErrNoJSONSuffix {
continue
}
if os.IsNotExist(err) {
if err2, ok := err.(*os.PathError); ok && err2.Path == filePath {
continue
}
}
if res == nil {
res = err
} else {
res = fmt.Errorf("%v: %w", err, res)
}
continue
}
hooks[file.Name()] = hook
logrus.Debugf("added hook %s", filePath)
}
return res
}
func init() {
Readers[current.Version] = current.Read
Readers[old.Version] = old.Read
Readers[""] = old.Read
}

View File

@ -1,194 +0,0 @@
package hooks
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
current "github.com/containers/podman/v4/pkg/hooks/1.0.0"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
)
func TestNoJSONSuffix(t *testing.T) {
_, err := Read("abc", []string{})
assert.Equal(t, err, ErrNoJSONSuffix)
}
func TestUnknownPath(t *testing.T) {
_, err := Read(filepath.Join("does", "not", "exist.json"), []string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^open does/not/exist.json: no such file or directory$", err.Error())
if !os.IsNotExist(err) {
t.Fatal("opaque wrapping for not-exist errors")
}
}
func TestGoodFile(t *testing.T) {
dir := t.TempDir()
jsonPath := filepath.Join(dir, "hook.json")
err := ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}", path)), 0644)
if err != nil {
t.Fatal(err)
}
hook, err := Read(jsonPath, []string{})
if err != nil {
t.Fatal(err)
}
always := true
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: path,
},
When: current.When{
Always: &always,
},
Stages: []string{"prestart"},
}, hook)
}
func TestBadFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "hook.json")
err := ioutil.WriteFile(path, []byte("{\"version\": \"1.0.0\", \"hook\": \"not-a-string\"}"), 0644)
if err != nil {
t.Fatal(err)
}
_, err = Read(path, []string{})
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^parsing hook \"[^\"]*hook.json\": 1.0.0: json: cannot unmarshal string into Go struct field Hook.hook of type specs.Hook$", err.Error())
}
func TestGoodBytes(t *testing.T) {
hook, err := read([]byte("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"/a/b/c\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}"))
if err != nil {
t.Fatal(err)
}
always := true
assert.Equal(t, &current.Hook{
Version: current.Version,
Hook: rspec.Hook{
Path: "/a/b/c",
},
When: current.When{
Always: &always,
},
Stages: []string{"prestart"},
}, hook)
}
func TestInvalidJSON(t *testing.T) {
_, err := read([]byte("{"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^version check: unexpected end of JSON input$", err.Error())
}
func TestInvalidVersion(t *testing.T) {
_, err := read([]byte("{\"version\": \"-1\"}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^unrecognized hook version: \"-1\"$", err.Error())
}
func TestInvalidCurrentJSON(t *testing.T) {
_, err := read([]byte("{\"version\": \"1.0.0\", \"hook\": \"not-a-string\"}"))
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^1.0.0: json: cannot unmarshal string into Go struct field Hook.hook of type specs.Hook$", err.Error())
}
func TestGoodDir(t *testing.T) {
dir := t.TempDir()
err := ioutil.WriteFile(filepath.Join(dir, "README"), []byte("not a hook"), 0644)
if err != nil {
t.Fatal(err)
}
jsonPath := filepath.Join(dir, "a.json")
err = ioutil.WriteFile(jsonPath, []byte(fmt.Sprintf("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"%s\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}", path)), 0644)
if err != nil {
t.Fatal(err)
}
hooks := map[string]*current.Hook{}
err = ReadDir(dir, []string{}, hooks)
if err != nil {
t.Fatal(err)
}
always := true
assert.Equal(t, map[string]*current.Hook{
"a.json": {
Version: current.Version,
Hook: rspec.Hook{
Path: path,
},
When: current.When{
Always: &always,
},
Stages: []string{"prestart"},
},
}, hooks)
}
func TestUnknownDir(t *testing.T) {
hooks := map[string]*current.Hook{}
err := ReadDir(filepath.Join("does", "not", "exist"), []string{}, hooks)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^open does/not/exist: no such file or directory$", err.Error())
if !os.IsNotExist(err) {
t.Fatal("opaque wrapping for not-exist errors")
}
}
func TestBadDir(t *testing.T) {
dir := t.TempDir()
jsonPath := filepath.Join(dir, "a.json")
err := ioutil.WriteFile(jsonPath, []byte("{\"version\": \"-1\"}"), 0644)
if err != nil {
t.Fatal(err)
}
hooks := map[string]*current.Hook{}
err = ReadDir(dir, []string{}, hooks)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^parsing hook \"[^\"]*a.json\": unrecognized hook version: \"-1\"$", err.Error())
}
func TestHookExecutableDoesNotExit(t *testing.T) {
dir := t.TempDir()
jsonPath := filepath.Join(dir, "hook.json")
err := ioutil.WriteFile(jsonPath, []byte("{\"version\": \"1.0.0\", \"hook\": {\"path\": \"/does/not/exist\"}, \"when\": {\"always\": true}, \"stages\": [\"prestart\"]}"), 0644)
if err != nil {
t.Fatal(err)
}
hooks := map[string]*current.Hook{}
err = ReadDir(dir, []string{}, hooks)
if err == nil {
t.Fatal("unexpected success")
}
assert.Regexp(t, "^stat /does/not/exist: no such file or directory$", err.Error())
}

View File

@ -1,6 +0,0 @@
package hooks
// version a structure for checking the version of a hook configuration.
type version struct {
Version string `json:"version"`
}

View File

@ -200,8 +200,6 @@ PODMAN_VERSION=%{version} %{__make} DESTDIR=%{buildroot} PREFIX=%{_prefix} ETCDI
install -d -p %{buildroot}/%{_datadir}/%{name}/test/system install -d -p %{buildroot}/%{_datadir}/%{name}/test/system
cp -pav test/system %{buildroot}/%{_datadir}/%{name}/test/ cp -pav test/system %{buildroot}/%{_datadir}/%{name}/test/
mv pkg/hooks/README.md pkg/hooks/README-hooks.md
# do not include docker and podman-remote man pages in main package # do not include docker and podman-remote man pages in main package
for file in `find %{buildroot}%{_mandir}/man[15] -type f | sed "s,%{buildroot},," | grep -v -e remote -e docker`; do for file in `find %{buildroot}%{_mandir}/man[15] -type f | sed "s,%{buildroot},," | grep -v -e remote -e docker`; do
echo "$file*" >> podman.file-list echo "$file*" >> podman.file-list
@ -211,7 +209,7 @@ done
# are going to be installed into target system where the rpm is installed. # are going to be installed into target system where the rpm is installed.
%files -f %{name}.file-list %files -f %{name}.file-list
%license LICENSE %license LICENSE
%doc README.md CONTRIBUTING.md pkg/hooks/README-hooks.md install.md transfer.md %doc README.md CONTRIBUTING.md install.md transfer.md
%{_bindir}/%{name} %{_bindir}/%{name}
%dir %{_libexecdir}/%{name} %dir %{_libexecdir}/%{name}
%{_libexecdir}/%{name}/rootlessport %{_libexecdir}/%{name}/rootlessport

2
vendor/modules.txt vendored
View File

@ -325,7 +325,6 @@ github.com/coreos/stream-metadata-go/stream/rhcos
## explicit ## explicit
github.com/cyphar/filepath-securejoin github.com/cyphar/filepath-securejoin
# github.com/davecgh/go-spew v1.1.1 # github.com/davecgh/go-spew v1.1.1
## explicit
github.com/davecgh/go-spew/spew github.com/davecgh/go-spew/spew
# github.com/digitalocean/go-libvirt v0.0.0-20201209184759-e2a69bcd5bd1 # github.com/digitalocean/go-libvirt v0.0.0-20201209184759-e2a69bcd5bd1
github.com/digitalocean/go-libvirt github.com/digitalocean/go-libvirt
@ -625,7 +624,6 @@ github.com/ostreedev/ostree-go/pkg/otbuiltin
# github.com/pkg/errors v0.9.1 # github.com/pkg/errors v0.9.1
github.com/pkg/errors github.com/pkg/errors
# github.com/pmezard/go-difflib v1.0.0 # github.com/pmezard/go-difflib v1.0.0
## explicit
github.com/pmezard/go-difflib/difflib github.com/pmezard/go-difflib/difflib
# github.com/proglottis/gpgme v0.1.3 # github.com/proglottis/gpgme v0.1.3
github.com/proglottis/gpgme github.com/proglottis/gpgme