Add support for oci-hooks to libkpod

Add new directory /etc/crio/hooks.d, where packagers can drop a json config
file to specify a hook.

The json must specify a valid executable to run.
The json must also specify which stage(s) to run the hook:
prestart, poststart, poststop
The json must specify under which criteria the hook should be launched
If the container HasBindMounts
If the container cmd matches a list of regular expressions
If the containers annotations matches a list of regular expressions.
If any of these match the the hook will be launched.

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
This commit is contained in:
Daniel J Walsh 2017-08-12 10:29:22 +00:00
parent 8538c4067a
commit 139d0841e8
13 changed files with 365 additions and 1 deletions

View file

@ -135,6 +135,12 @@ type RuntimeConfig struct {
// handle cgroups for containers.
CgroupManager string `toml:"cgroup_manager"`
// HooksDirPath location of oci hooks config files
HooksDirPath string `toml:"hooks_dir_path"`
// Hooks List of hooks to run with container
Hooks map[string]HookParams
// PidsLimit is the number of processes each container is restricted to
// by the cgroup process number controller.
PidsLimit int64 `toml:"pids_limit"`
@ -267,6 +273,7 @@ func DefaultConfig() *Config {
CgroupManager: cgroupManager,
PidsLimit: DefaultPidsLimit,
ContainerExitsDir: containerExitsDir,
HooksDirPath: DefaultHooksDirPath,
},
ImageConfig: ImageConfig{
DefaultTransport: defaultTransport,

View file

@ -36,6 +36,7 @@ type ContainerServer struct {
ctrIDIndex *truncindex.TruncIndex
podNameIndex *registrar.Registrar
podIDIndex *truncindex.TruncIndex
hooks map[string]HookParams
imageContext *types.SystemContext
stateLock sync.Locker
@ -48,6 +49,11 @@ func (c *ContainerServer) Runtime() *oci.Runtime {
return c.runtime
}
// Hooks returns the oci hooks for the ContainerServer
func (c *ContainerServer) Hooks() map[string]HookParams {
return c.hooks
}
// Store returns the Store for the ContainerServer
func (c *ContainerServer) Store() cstorage.Store {
return c.store
@ -131,6 +137,21 @@ func New(config *Config) (*ContainerServer, error) {
lock = new(sync.Mutex)
}
hooks := make(map[string]HookParams)
// If hooks directory is set in config use it
if config.HooksDirPath != "" {
if err := readHooks(config.HooksDirPath, hooks); err != nil {
return nil, err
}
// If user overrode default hooks, this means it is in a test, so don't
// use OverrideHooksDirPath
if config.HooksDirPath == DefaultHooksDirPath {
if err := readHooks(OverrideHooksDirPath, hooks); err != nil {
return nil, err
}
}
}
return &ContainerServer{
runtime: runtime,
store: store,
@ -141,6 +162,7 @@ func New(config *Config) (*ContainerServer, error) {
podNameIndex: registrar.NewRegistrar(),
podIDIndex: truncindex.NewTruncIndex([]string{}),
imageContext: &types.SystemContext{SignaturePolicyPath: config.SignaturePolicyPath},
hooks: hooks,
stateLock: lock,
state: &containerServerState{
containers: oci.NewMemoryStore(),

98
libkpod/hooks.go Normal file
View file

@ -0,0 +1,98 @@
package libkpod
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// DefaultHooksDirPath Default directory containing hooks config files
DefaultHooksDirPath = "/usr/share/containers/oci/hooks.d"
// OverrideHooksDirPath Directory where admin can override the default configuration
OverrideHooksDirPath = "/etc/containers/oci/hooks.d"
)
// HookParams is the structure returned from read the hooks configuration
type HookParams struct {
Hook string `json:"hook"`
Stage []string `json:"stage"`
Cmds []string `json:"cmd"`
Annotations []string `json:"annotation"`
HasBindMounts bool `json:"hasbindmounts"`
}
// readHook reads hooks json files, verifies it and returns the json config
func readHook(hookPath string) (HookParams, error) {
var hook HookParams
raw, err := ioutil.ReadFile(hookPath)
if err != nil {
return hook, errors.Wrapf(err, "error Reading hook %q", hookPath)
}
if err := json.Unmarshal(raw, &hook); err != nil {
return hook, errors.Wrapf(err, "error Unmarshalling JSON for %q", hookPath)
}
if _, err := os.Stat(hook.Hook); err != nil {
return hook, errors.Wrapf(err, "unable to stat hook %q in hook config %q", hook.Hook, hookPath)
}
validStage := map[string]bool{"prestart": true, "poststart": true, "poststop": true}
for _, cmd := range hook.Cmds {
if _, err = regexp.Compile(cmd); err != nil {
return hook, errors.Wrapf(err, "invalid cmd regular expression %q defined in hook config %q", cmd, hookPath)
}
}
for _, cmd := range hook.Annotations {
if _, err = regexp.Compile(cmd); err != nil {
return hook, errors.Wrapf(err, "invalid cmd regular expression %q defined in hook config %q", cmd, hookPath)
}
}
for _, stage := range hook.Stage {
if !validStage[stage] {
return hook, errors.Wrapf(err, "unknown stage %q defined in hook config %q", stage, hookPath)
}
}
return hook, nil
}
// readHooks reads hooks json files in directory to setup OCI Hooks
// adding hooks to the passedin hooks map.
func readHooks(hooksPath string, hooks map[string]HookParams) error {
if _, err := os.Stat(hooksPath); err != nil {
if os.IsNotExist(err) {
logrus.Warnf("hooks path: %q does not exist", hooksPath)
return nil
}
return errors.Wrapf(err, "unable to stat hooks path %q", hooksPath)
}
files, err := ioutil.ReadDir(hooksPath)
if err != nil {
return err
}
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") {
continue
}
hook, err := readHook(filepath.Join(hooksPath, file.Name()))
if err != nil {
return err
}
for key, h := range hooks {
// hook.Hook can only be defined in one hook file, unless it has the
// same name in the override path.
if hook.Hook == h.Hook && key != file.Name() {
return errors.Wrapf(syscall.EINVAL, "duplicate path, hook %q from %q already defined in %q", hook.Hook, hooksPath, key)
}
}
hooks[file.Name()] = hook
}
return nil
}