diff --git a/client.go b/client.go new file mode 100644 index 00000000..046d47dc --- /dev/null +++ b/client.go @@ -0,0 +1,135 @@ +package nri + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/oci" + "github.com/containerd/nri/types" + "github.com/pkg/errors" +) + +const ( + // DefaultBinaryPath for nri plugins + DefaultBinaryPath = "/opt/nri/bin" + // DefaultConfPath for the global nri configuration + DefaultConfPath = "/etc/nri/conf.json" +) + +// New nri client +func New() (*Client, error) { + conf, err := loadConfig(DefaultConfPath) + if err != nil { + return nil, err + } + if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), DefaultBinaryPath)); err != nil { + return nil, err + } + return &Client{ + conf: conf, + }, nil +} + +// Client for calling nri plugins +type Client struct { + conf *types.ConfigList +} + +// Invoke the ConfList of nri plugins +func (c *Client) Invoke(ctx context.Context, task containerd.Task, state types.State) ([]*types.Result, error) { + spec, err := task.Spec(ctx) + if err != nil { + return nil, err + } + rs, err := createSpec(spec) + if err != nil { + return nil, err + } + var results []*types.Result + r := &types.Request{ + Version: c.conf.Version, + ID: task.ID(), + Pid: int(task.Pid()), + State: state, + Spec: rs, + } + for _, p := range c.conf.Plugins { + r.Conf = p.Conf + result, err := c.invokePlugin(ctx, p.Type, r) + if err != nil { + return nil, err + } + results = append(results, result) + } + return results, nil +} + +func createSpec(spec *oci.Spec) (*types.Spec, error) { + s := types.Spec{ + Namespaces: make(map[string]string), + Annotations: spec.Annotations, + } + switch { + case spec.Linux != nil: + s.CgroupsPath = spec.Linux.CgroupsPath + data, err := json.Marshal(spec.Linux.Resources) + if err != nil { + return nil, err + } + s.Resources = json.RawMessage(data) + for _, ns := range spec.Linux.Namespaces { + s.Namespaces[string(ns.Type)] = ns.Path + } + case spec.Windows != nil: + data, err := json.Marshal(spec.Windows.Resources) + if err != nil { + return nil, err + } + s.Resources = json.RawMessage(data) + } + return &s, nil +} + +func (c *Client) invokePlugin(ctx context.Context, name string, r *types.Request) (*types.Result, error) { + payload, err := json.Marshal(r) + if err != nil { + return nil, err + } + cmd := exec.CommandContext(ctx, name, "invoke") + cmd.Stdin = bytes.NewBuffer(payload) + cmd.Stderr = os.Stderr + + out, err := cmd.Output() + if err != nil { + return nil, errors.Wrapf(err, "%s: %s", name, out) + } + var result types.Result + if err := json.Unmarshal(out, &result); err != nil { + return nil, err + } + return &result, nil +} + +func loadConfig(path string) (*types.ConfigList, error) { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return &types.ConfigList{ + Version: "0.1", + }, nil + } + return nil, err + } + var c types.ConfigList + err = json.NewDecoder(f).Decode(&c) + f.Close() + if err != nil { + return nil, err + } + return &c, nil +} diff --git a/skel/skel.go b/skel/skel.go new file mode 100644 index 00000000..b9f9ec6d --- /dev/null +++ b/skel/skel.go @@ -0,0 +1,41 @@ +package skel + +import ( + "context" + "encoding/json" + "os" + + "github.com/containerd/nri/types" + "github.com/pkg/errors" +) + +// Plugin for modifications of resources +type Plugin interface { + // Type or plugin name + Type() string + // Invoke the plugin + Invoke(context.Context, *types.Request) (*types.Result, error) +} + +// Run the plugin from a main() function +func Run(ctx context.Context, plugin Plugin) error { + var ( + enc = json.NewEncoder(os.Stdout) + out interface{} + ) + var request types.Request + if err := json.NewDecoder(os.Stdin).Decode(&request); err != nil { + return err + } + switch os.Args[1] { + case "invoke": + result, err := plugin.Invoke(ctx, &request) + if err != nil { + return err + } + out = result + default: + return errors.New("undefined arg") + } + return enc.Encode(out) +} diff --git a/types/types.go b/types/types.go new file mode 100644 index 00000000..238a5919 --- /dev/null +++ b/types/types.go @@ -0,0 +1,105 @@ +package types + +import "encoding/json" + +// Plugin type and configuration +type Plugin struct { + // Type of plugin + Type string `json:"type"` + // Conf for the specific plugin + Conf json.RawMessage `json:"conf,omitempty"` +} + +// ConfigList for the global configuration of NRI +// +// Normally located at /etc/nri/conf.json +type ConfigList struct { + // Verion of the list + Version string `json:"version"` + // Plugins + Plugins []*Plugin `json:"plugins"` +} + +// Spec for the container being processed +type Spec struct { + // Resources struct from the OCI specification + // + // Can be WindowsResources or LinuxResources + Resources json.RawMessage `json:"resources"` + // Namespaces for the container + Namespaces map[string]string `json:"namespaces,omitempty"` + // CgroupsPath for the container + CgroupsPath string `json:"cgroupsPath,omitempty"` + // Annotations passed down to the OCI runtime specification + Annotations map[string]string `json:"annotations,omitempty"` +} + +// State of the request +type State string + +const ( + // Create the initial resource for the container + Create State = "create" + // Delete any resources for the container + Delete State = "delete" + // Update the resources for the container + Update State = "update" + // Pause action of the container + Pause State = "pause" + // Resume action for the container + Resume State = "resume" +) + +// Request for a plugin invocation +type Request struct { + // Conf specific for the plugin + Conf json.RawMessage `json:"conf"` + + // Version of the plugin + Version string `json:"version"` + // State action for the request + State State `json:"state"` + // ID for the container + ID string `json:"id"` + // SandboxID for the sandbox that the request belongs to + // + // If ID and SandboxID are the same, this is a request for the sandbox + // SandboxID is empty for a non sandboxed container + SandboxID string `json:"sandboxID"` + // Pid of the container + // + // -1 if there is no pid + Pid int `json:"pid,omitempty"` + // Spec generated from the OCI runtime specification + Spec *Spec `json:"spec"` +} + +// IsSandbox returns true if the request is for a sandbox +func (r *Request) IsSandbox() bool { + return r.ID == r.SandboxID +} + +// NewResult returns a result from the original request +func (r *Request) NewResult() *Result { + return &Result{ + ID: r.ID, + State: r.State, + Pid: r.Pid, + Version: r.Version, + CgroupsPath: r.Spec.CgroupsPath, + } +} + +// Result of the plugin invocation +type Result struct { + // Version of the plugin + Version string `json:"version"` + // State of the invocation + State State `json:"state"` + // ID of the container + ID string `json:"id"` + // Pid of the container + Pid int `json:"pid"` + // CgroupsPath of the container + CgroupsPath string `json:"cgroupsPath"` +}