-
Notifications
You must be signed in to change notification settings - Fork 113
agent: add support for guest-hooks (v2) #393
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -536,6 +536,35 @@ func (a *agentGRPC) rollbackFailingContainerCreation(ctr *container) { | |
} | ||
} | ||
|
||
func (a *agentGRPC) finishCreateContainer(ctr *container, req *pb.CreateContainerRequest, config *configs.Config) (resp *gpb.Empty, err error) { | ||
containerPath := filepath.Join("/tmp/libcontainer", a.sandbox.id) | ||
factory, err := libcontainer.New(containerPath, libcontainer.Cgroupfs) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
ctr.container, err = factory.Create(req.ContainerId, config) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
ctr.config = *config | ||
|
||
ctr.initProcess, err = buildProcess(req.OCI.Process, req.ExecId) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
if err = a.execProcess(ctr, ctr.initProcess, true); err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
if err := a.updateSharedPidNs(ctr); err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
return emptyResp, a.postExecProcess(ctr, ctr.initProcess) | ||
} | ||
|
||
func (a *agentGRPC) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (resp *gpb.Empty, err error) { | ||
if err := a.createContainerChecks(req); err != nil { | ||
return emptyResp, err | ||
|
@@ -591,6 +620,25 @@ func (a *agentGRPC) CreateContainer(ctx context.Context, req *pb.CreateContainer | |
return emptyResp, err | ||
} | ||
|
||
if a.sandbox.guestHooksPresent { | ||
// Add any custom OCI hooks to the spec | ||
a.sandbox.addGuestHooks(ociSpec) | ||
|
||
// write the OCI spec to a file so that hooks can read it | ||
err = writeSpecToFile(ociSpec) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
// Change cwd because libcontainer assumes the bundle path is the cwd: | ||
// https://github.com/opencontainers/runc/blob/v1.0.0-rc5/libcontainer/specconv/spec_linux.go#L157 | ||
oldcwd, err := changeToBundlePath(ociSpec) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth noting that the code piece works because golang sets CLONE_FS when creating new M (https://github.com/golang/go/blob/2595fe7fb6f272f9204ca3ef0b0c55e66fb8d90f/src/runtime/os_linux.go#L131) so CWD is shared among all os threads. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth noting that the code piece works because golang sets CLONE_FS when creating new M (https://github.com/golang/go/blob/2595fe7fb6f272f9204ca3ef0b0c55e66fb8d90f/src/runtime/os_linux.go#L131) so CWD is shared among all os threads. We rely on this model because the goroutine might be rescheduled to another os thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems like an implementation detail. The runtime could very well store the cwd for each goroutine and then restore it when the goroutine is re-scheduled anywhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @flx42 I mentioned it because we do depend on the runtime implementation details here. If the Go runtime decides that CWD is an M local property, we are doomed because a goroutine can be rescheduled to another os thread since we do not lock os thread here, and then CWD is messed up in different threads. |
||
if err != nil { | ||
return emptyResp, err | ||
} | ||
defer os.Chdir(oldcwd) | ||
} | ||
|
||
// Convert the OCI specification into a libcontainer configuration. | ||
config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{ | ||
CgroupName: req.ContainerId, | ||
|
@@ -608,32 +656,7 @@ func (a *agentGRPC) CreateContainer(ctx context.Context, req *pb.CreateContainer | |
return emptyResp, err | ||
} | ||
|
||
containerPath := filepath.Join("/tmp/libcontainer", a.sandbox.id) | ||
factory, err := libcontainer.New(containerPath, libcontainer.Cgroupfs) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
ctr.container, err = factory.Create(req.ContainerId, config) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
ctr.config = *config | ||
|
||
ctr.initProcess, err = buildProcess(req.OCI.Process, req.ExecId) | ||
if err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
if err = a.execProcess(ctr, ctr.initProcess, true); err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
if err := a.updateSharedPidNs(ctr); err != nil { | ||
return emptyResp, err | ||
} | ||
|
||
return emptyResp, a.postExecProcess(ctr, ctr.initProcess) | ||
return a.finishCreateContainer(ctr, req, config) | ||
} | ||
|
||
func (a *agentGRPC) createContainerChecks(req *pb.CreateContainerRequest) (err error) { | ||
|
@@ -1193,6 +1216,12 @@ func (a *agentGRPC) CreateSandbox(ctx context.Context, req *pb.CreateSandboxRequ | |
a.sandbox.running = true | ||
a.sandbox.sandboxPidNs = req.SandboxPidns | ||
a.sandbox.storages = make(map[string]*sandboxStorage) | ||
a.sandbox.guestHooks = &specs.Hooks{} | ||
a.sandbox.guestHooksPresent = false | ||
|
||
if req.GuestHookPath != "" { | ||
a.sandbox.scanGuestHooks(req.GuestHookPath) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error is still silently ignored. I wonder how a user can find it out later on if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Through the logs: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I am leaning towards just logging this for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough! |
||
} | ||
|
||
if req.SandboxId != "" { | ||
a.sandbox.id = req.SandboxId | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// | ||
// Copyright (c) 2018 NVIDIA CORPORATION | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"io/ioutil" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
|
||
"github.com/opencontainers/runtime-spec/specs-go" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
// OCI config file | ||
const ( | ||
ociConfigFile string = "config.json" | ||
ociConfigFileMode os.FileMode = 0444 | ||
) | ||
|
||
// writeSpecToFile writes the container's OCI spec to "dirname(spec.Root.Path)/config.json" | ||
// This effectively makes the parent directory a valid OCI bundle. | ||
func writeSpecToFile(spec *specs.Spec) error { | ||
bundlePath := filepath.Dir(spec.Root.Path) | ||
configPath := filepath.Join(bundlePath, ociConfigFile) | ||
f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, ociConfigFileMode) | ||
if err != nil { | ||
return err | ||
} | ||
defer f.Close() | ||
|
||
return json.NewEncoder(f).Encode(spec) | ||
} | ||
|
||
// changeToBundlePath changes the cwd to the OCI bundle path defined as | ||
// dirname(spec.Root.Path) and returns the old cwd. | ||
func changeToBundlePath(spec *specs.Spec) (string, error) { | ||
cwd, err := os.Getwd() | ||
if err != nil { | ||
return cwd, err | ||
} | ||
|
||
if spec == nil || spec.Root == nil || spec.Root.Path == "" { | ||
return cwd, errors.New("invalid OCI spec") | ||
} | ||
|
||
bundlePath := filepath.Dir(spec.Root.Path) | ||
configPath := filepath.Join(bundlePath, ociConfigFile) | ||
|
||
// Verify that config.json is present at the root of the bundle path. | ||
if _, err := os.Stat(configPath); err != nil { | ||
return cwd, errors.New("invalid OCI bundle") | ||
} | ||
|
||
return cwd, os.Chdir(bundlePath) | ||
} | ||
|
||
func isValidHook(file os.FileInfo) (bool, error) { | ||
if file.IsDir() { | ||
return false, errors.New("is a directory") | ||
} | ||
|
||
mode := file.Mode() | ||
if (mode & os.ModeSymlink) != 0 { | ||
return false, errors.New("is a symbolic link") | ||
} | ||
|
||
perm := mode & os.ModePerm | ||
if (perm & 0111) == 0 { | ||
return false, errors.New("is not executable") | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
// findHooks searches guestHookPath for any OCI hooks for a given hookType | ||
func findHooks(guestHookPath, hookType string) (hooksFound []specs.Hook) { | ||
hooksPath := path.Join(guestHookPath, hookType) | ||
|
||
files, err := ioutil.ReadDir(hooksPath) | ||
if err != nil { | ||
agentLog.WithError(err).WithField("oci-hook-type", hookType).Info("Skipping hook type") | ||
return | ||
} | ||
|
||
for _, file := range files { | ||
name := file.Name() | ||
if ok, err := isValidHook(file); !ok { | ||
agentLog.WithError(err).WithField("oci-hook-name", name).Warn("Skipping hook") | ||
continue | ||
} | ||
|
||
agentLog.WithFields(logrus.Fields{ | ||
"oci-hook-name": name, | ||
"oci-hook-type": hookType, | ||
}).Info("Adding hook") | ||
hooksFound = append(hooksFound, specs.Hook{ | ||
Path: path.Join(hooksPath, name), | ||
Args: []string{name, hookType}, | ||
}) | ||
} | ||
|
||
agentLog.WithField("oci-hook-type", hookType).Infof("Added %d hooks", len(hooksFound)) | ||
|
||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be overkill, but I wonder if we want to run these three filesystem operations in separate goroutines and wait for them to finish to minimise the time cost?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems entirely overkill since every function call is likely to stop after the first ReadDir :), that is to say if
<path>/{prestart,poststart,poststop}
does not exist or is not a directory.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack ;)