summaryrefslogtreecommitdiff
path: root/pkg/hooks/1.0.0
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/hooks/1.0.0')
-rw-r--r--pkg/hooks/1.0.0/hook.go78
-rw-r--r--pkg/hooks/1.0.0/hook_test.go200
-rw-r--r--pkg/hooks/1.0.0/when.go92
-rw-r--r--pkg/hooks/1.0.0/when_test.go289
4 files changed, 659 insertions, 0 deletions
diff --git a/pkg/hooks/1.0.0/hook.go b/pkg/hooks/1.0.0/hook.go
new file mode 100644
index 000000000..cf623ee42
--- /dev/null
+++ b/pkg/hooks/1.0.0/hook.go
@@ -0,0 +1,78 @@
+// Package hook is the 1.0.0 hook configuration structure.
+package hook
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "regexp"
+
+ rspec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+)
+
+// 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() (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 errors.Wrapf(err, "invalid annotation key %q", key)
+ }
+ if _, err = regexp.Compile(value); err != nil {
+ return errors.Wrapf(err, "invalid annotation value %q", value)
+ }
+ }
+
+ for _, command := range hook.When.Commands {
+ if _, err = regexp.Compile(command); err != nil {
+ return errors.Wrapf(err, "invalid command %q", command)
+ }
+ }
+
+ if hook.Stages == nil {
+ return errors.New("missing required property: stages")
+ }
+
+ validStages := map[string]bool{"prestart": true, "poststart": true, "poststop": true}
+ for _, stage := range hook.Stages {
+ if !validStages[stage] {
+ return fmt.Errorf("unknown stage %q", stage)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/hooks/1.0.0/hook_test.go b/pkg/hooks/1.0.0/hook_test.go
new file mode 100644
index 000000000..003be34bb
--- /dev/null
+++ b/pkg/hooks/1.0.0/hook_test.go
@@ -0,0 +1,200 @@
+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()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestNilValidation(t *testing.T) {
+ var hook *Hook
+ err := hook.Validate()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ if err == nil {
+ t.Fatal("unexpected success")
+ }
+ assert.Regexp(t, "^unknown stage \"does-not-exist\"$", err.Error())
+}
+
+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()
+ 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()
+ 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()
+ 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")
+ }
+}
diff --git a/pkg/hooks/1.0.0/when.go b/pkg/hooks/1.0.0/when.go
new file mode 100644
index 000000000..3d2a5fd72
--- /dev/null
+++ b/pkg/hooks/1.0.0/when.go
@@ -0,0 +1,92 @@
+package hook
+
+import (
+ "regexp"
+
+ rspec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+)
+
+// When holds hook-injection conditions.
+type When struct {
+ Always *bool `json:"always,omitempty"`
+ Annotations map[string]string `json:"annotation,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, errors.Wrap(err, "annotation key")
+ }
+ if match {
+ match, err = regexp.MatchString(valuePattern, value)
+ if err != nil {
+ return false, errors.Wrap(err, "annotation value")
+ }
+ if match {
+ break
+ }
+ }
+ }
+ if match {
+ if when.Or {
+ return true, nil
+ }
+ matches++
+ } else if !when.Or {
+ return false, nil
+ }
+ }
+
+ if config.Process != nil {
+ command := config.Process.Args[0]
+ for _, cmdPattern := range when.Commands {
+ match, err := regexp.MatchString(cmdPattern, command)
+ if err != nil {
+ return false, errors.Wrap(err, "command")
+ }
+ if match {
+ return true, nil
+ }
+ }
+ return false, nil
+ }
+
+ return matches > 0, nil
+}
diff --git a/pkg/hooks/1.0.0/when_test.go b/pkg/hooks/1.0.0/when_test.go
new file mode 100644
index 000000000..9047f4c9f
--- /dev/null
+++ b/pkg/hooks/1.0.0/when_test.go
@@ -0,0 +1,289 @@
+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 _, or := range []bool{true, false} {
+ 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{}
+ for _, always := range []bool{true, false} {
+ for _, or := range []bool{true, false} {
+ t.Run(fmt.Sprintf("always %t, or %t", always, or), func(t *testing.T) {
+ 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 _, containerHasBindMounts := range []bool{false, true} {
+ 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 _, containerHasBindMounts := range []bool{false, true} {
+ 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 _, test := 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,
+ },
+ } {
+ 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{Process: &rspec.Process{}}
+ for _, test := range []struct {
+ name string
+ args []string
+ match bool
+ }{
+ {
+ name: "good",
+ args: []string{"/bin/sh", "a", "b"},
+ match: true,
+ },
+ {
+ name: "extra characters",
+ args: []string{"/bin/shell", "a", "b"},
+ match: false,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ config.Process.Args = test.args
+ match, err := when.Match(config, map[string]string{}, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.match, match)
+ })
+ }
+}
+
+func TestHasBindMountsAndCommands(t *testing.T) {
+ hasBindMounts := true
+ when := When{
+ HasBindMounts: &hasBindMounts,
+ Commands: []string{
+ "^/bin/sh$",
+ },
+ }
+ config := &rspec.Spec{Process: &rspec.Process{}}
+ for _, test := 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, and",
+ 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,
+ },
+ } {
+ 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 _, test := 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: .*",
+ },
+ } {
+ 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())
+ })
+ }
+}