diff --git a/common/pkg/hooks/0.1.0/hook.go b/common/pkg/hooks/0.1.0/hook.go index 54dea2e6f7..5bddd23373 100644 --- a/common/pkg/hooks/0.1.0/hook.go +++ b/common/pkg/hooks/0.1.0/hook.go @@ -7,7 +7,7 @@ import ( "strings" rspec "github.com/opencontainers/runtime-spec/specs-go" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) // Version is the hook configuration version defined in this package. diff --git a/common/pkg/hooks/0.1.0/hook_test.go b/common/pkg/hooks/0.1.0/hook_test.go index 73847f5bcf..1c3d946279 100644 --- a/common/pkg/hooks/0.1.0/hook_test.go +++ b/common/pkg/hooks/0.1.0/hook_test.go @@ -5,7 +5,7 @@ import ( rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) func TestGood(t *testing.T) { diff --git a/common/pkg/hooks/1.0.0/hook.go b/common/pkg/hooks/1.0.0/hook.go index dbac6e64bf..556c40e658 100644 --- a/common/pkg/hooks/1.0.0/hook.go +++ b/common/pkg/hooks/1.0.0/hook.go @@ -3,87 +3,18 @@ package hook import ( "encoding/json" - "errors" - "fmt" - "regexp" - rspec "github.com/opencontainers/runtime-spec/specs-go" - "go.podman.io/storage/pkg/fileutils" + current "go.podman.io/common/pkg/hooks/1.1.0" ) // 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) { +func Read(content []byte) (hook *current.Hook, err error) { if err = json.Unmarshal(content, &hook); err != nil { return nil, err } + hook.Version = current.Version 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 := fileutils.Exists(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 -} diff --git a/common/pkg/hooks/1.0.0/hook_test.go b/common/pkg/hooks/1.0.0/hook_test.go index 5aea7fda2f..583de5788d 100644 --- a/common/pkg/hooks/1.0.0/hook_test.go +++ b/common/pkg/hooks/1.0.0/hook_test.go @@ -1,31 +1,25 @@ package hook import ( - "errors" - "os" - "path/filepath" - "runtime" "testing" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" + current "go.podman.io/common/pkg/hooks/1.1.0" ) -// 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, + assert.Equal(t, ¤t.Hook{ + Version: current.Version, Hook: rspec.Hook{ Path: "/a/b/c", }, - When: When{ + When: current.When{ Always: &always, }, Stages: []string{"prestart"}, @@ -39,177 +33,3 @@ func TestInvalidJSON(t *testing.T) { } 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, "^(faccessat|stat) does/not/exist: no such file or directory$", err.Error()) - if !errors.Is(err, os.ErrNotExist) { - 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") - } -} diff --git a/common/pkg/hooks/1.1.0/hook.go b/common/pkg/hooks/1.1.0/hook.go new file mode 100644 index 0000000000..03d727f86a --- /dev/null +++ b/common/pkg/hooks/1.1.0/hook.go @@ -0,0 +1,98 @@ +// Package hook is the 1.1.0 hook configuration structure. +package hook + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + + rspec "github.com/opencontainers/runtime-spec/specs-go" + "go.podman.io/storage/pkg/fileutils" +) + +// Version is the hook configuration version defined in this package. +const Version = "1.1.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 := fileutils.Exists(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 key, value := range hook.When.AnnotationsOr { + if _, err = regexp.Compile(key); err != nil { + return fmt.Errorf("invalid annotation_or key %q: %w", key, err) + } + if _, err = regexp.Compile(value); err != nil { + return fmt.Errorf("invalid annotation_or 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 +} diff --git a/common/pkg/hooks/1.1.0/hook_test.go b/common/pkg/hooks/1.1.0/hook_test.go new file mode 100644 index 0000000000..91acfba46b --- /dev/null +++ b/common/pkg/hooks/1.1.0/hook_test.go @@ -0,0 +1,215 @@ +package hook + +import ( + "errors" + "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.1.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.1.0\\)$", err.Error()) +} + +func TestNoHookPath(t *testing.T) { + hook := Hook{ + Version: "1.1.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.1.0", + Hook: rspec.Hook{ + Path: filepath.Join("does", "not", "exist"), + }, + } + err := hook.Validate([]string{}) + if err == nil { + t.Fatal("unexpected success") + } + assert.Regexp(t, "^(faccessat|stat) does/not/exist: no such file or directory$", err.Error()) + if !errors.Is(err, os.ErrNotExist) { + t.Fatal("opaque wrapping for not-exist errors") + } +} + +func TestNoStages(t *testing.T) { + hook := Hook{ + Version: "1.1.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.1.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.1.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.1.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.1.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.1.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") + } +} diff --git a/common/pkg/hooks/1.1.0/when.go b/common/pkg/hooks/1.1.0/when.go new file mode 100644 index 0000000000..b61a739632 --- /dev/null +++ b/common/pkg/hooks/1.1.0/when.go @@ -0,0 +1,124 @@ +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"` + AnnotationsOr map[string]string `json:"annotationsOr,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 + } + } + + matched := false + for keyPattern, valuePattern := range when.AnnotationsOr { + for key, value := range annotations { + keyMatch, err := regexp.MatchString(keyPattern, key) + if err != nil { + return false, fmt.Errorf("annotation_or key: %w", err) + } + valueMatch, err := regexp.MatchString(valuePattern, value) + if err != nil { + return false, fmt.Errorf("annotation_or value: %w", err) + } + + if keyMatch && valueMatch { + matched = true + break + } + } + if matched { + matches++ + break + } + } + + if len(when.AnnotationsOr) > 0 && !matched { + 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 +} diff --git a/common/pkg/hooks/1.1.0/when_test.go b/common/pkg/hooks/1.1.0/when_test.go new file mode 100644 index 0000000000..45da91f91e --- /dev/null +++ b/common/pkg/hooks/1.1.0/when_test.go @@ -0,0 +1,474 @@ +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 TestAnnotationsOr(t *testing.T) { + alwaysTrue := true + config := &rspec.Spec{} + for _, tt := range []struct { + name string + when When + annotations map[string]string + match bool + }{ + { + name: "one entry matches", + when: When{ + AnnotationsOr: map[string]string{ + "^a$": "^b$", + "^c$": "^d$", + }, + }, + annotations: map[string]string{"a": "b"}, + match: true, + }, + { + name: "no entries match", + when: When{ + AnnotationsOr: map[string]string{ + "^a$": "^b$", + "^c$": "^d$", + }, + }, + annotations: map[string]string{"e": "f"}, + match: false, + }, + { + name: "all entries match", + when: When{ + AnnotationsOr: map[string]string{ + "^a$": "^b$", + "^c$": "^d$", + }, + }, + annotations: map[string]string{"a": "b", "c": "d"}, + match: true, + }, + { + name: "key matches but value doesn't", + when: When{ + AnnotationsOr: map[string]string{ + "^a$": "^b$", + }, + }, + annotations: map[string]string{"a": "bc"}, + match: false, + }, + { + name: "annotationsOr unset, always true", + when: When{ + Always: &alwaysTrue, + }, + annotations: map[string]string{}, + match: true, + }, + } { + test := tt + t.Run(test.name, func(t *testing.T) { + match, err := test.when.Match(config, test.annotations, false) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, test.match, match) + }) + } +} + +func TestAnnotationsOrIgnoresDeprecatedOr(t *testing.T) { + when := When{ + Or: true, //nolint:staticcheck // SA1019: intentionally testing that Or does not interact with AnnotationsOr. + AnnotationsOr: map[string]string{ + "^a$": "^b$", + }, + } + config := &rspec.Spec{} + match, err := when.Match(config, map[string]string{"x": "y"}, false) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, false, match) +} + +func TestAnnotationsAndAnnotationsOr(t *testing.T) { + config := &rspec.Spec{} + when := When{ + Annotations: map[string]string{ + "^a$": "^b$", + }, + AnnotationsOr: map[string]string{ + "^c$": "^d$", + "^e$": "^f$", + }, + } + for _, tt := range []struct { + name string + annotations map[string]string + match bool + }{ + { + name: "and matches, or matches", + annotations: map[string]string{"a": "b", "c": "d"}, + match: true, + }, + { + name: "and matches, or fails", + annotations: map[string]string{"a": "b", "g": "h"}, + match: false, + }, + { + name: "and fails, or matches", + annotations: map[string]string{"z": "y", "c": "d"}, + match: false, + }, + { + name: "and fails, or fails", + annotations: map[string]string{"z": "y"}, + match: false, + }, + } { + test := tt + t.Run(test.name, func(t *testing.T) { + 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-annotationsOr-key", + when: When{AnnotationsOr: map[string]string{"[": "a"}}, + expected: "^annotation_or key: error parsing regexp: .*", + }, + { + name: "invalid-annotationsOr-value", + when: When{AnnotationsOr: map[string]string{"a": "["}}, + expected: "^annotation_or 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()) + }) + } +} diff --git a/common/pkg/hooks/hooks.go b/common/pkg/hooks/hooks.go index cdcc7b8953..9d20937deb 100644 --- a/common/pkg/hooks/hooks.go +++ b/common/pkg/hooks/hooks.go @@ -13,7 +13,7 @@ import ( rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) // Version is the current hook configuration version. diff --git a/common/pkg/hooks/hooks_test.go b/common/pkg/hooks/hooks_test.go index 5db173f9d1..1f64c40419 100644 --- a/common/pkg/hooks/hooks_test.go +++ b/common/pkg/hooks/hooks_test.go @@ -10,7 +10,7 @@ import ( rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) // path is the path to an example hook executable. diff --git a/common/pkg/hooks/monitor.go b/common/pkg/hooks/monitor.go index c2039482ca..6bb06039f3 100644 --- a/common/pkg/hooks/monitor.go +++ b/common/pkg/hooks/monitor.go @@ -5,7 +5,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) // Monitor dynamically monitors hook directories for additions, diff --git a/common/pkg/hooks/read.go b/common/pkg/hooks/read.go index 92f3e534b6..890458f1de 100644 --- a/common/pkg/hooks/read.go +++ b/common/pkg/hooks/read.go @@ -11,7 +11,8 @@ import ( "github.com/sirupsen/logrus" old "go.podman.io/common/pkg/hooks/0.1.0" - current "go.podman.io/common/pkg/hooks/1.0.0" + prev "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) type reader func(content []byte) (*current.Hook, error) @@ -95,6 +96,7 @@ func ReadDir(path string, extensionStages []string, hooks map[string]*current.Ho func init() { Readers[current.Version] = current.Read + Readers[prev.Version] = prev.Read Readers[old.Version] = old.Read Readers[""] = old.Read } diff --git a/common/pkg/hooks/read_test.go b/common/pkg/hooks/read_test.go index bde79f41b8..fc35354bbb 100644 --- a/common/pkg/hooks/read_test.go +++ b/common/pkg/hooks/read_test.go @@ -9,7 +9,7 @@ import ( rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" - current "go.podman.io/common/pkg/hooks/1.0.0" + current "go.podman.io/common/pkg/hooks/1.1.0" ) func TestNoJSONSuffix(t *testing.T) {