From 6f528ce599d4b37de27619079f634ca8acf63ef1 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Sun, 12 Sep 2021 23:00:36 +0300 Subject: [PATCH] plugins/logger: add sample protocol logger plugin. Add a dummy plugin that simply logs incoming requests/events without practically altering anything (apart from labeling containers). --- Makefile | 14 +- v2alpha1/plugins/logger/logging.go | 137 +++++++++++++ v2alpha1/plugins/logger/nri-logger.go | 264 ++++++++++++++++++++++++++ 3 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 v2alpha1/plugins/logger/logging.go create mode 100644 v2alpha1/plugins/logger/nri-logger.go diff --git a/Makefile b/Makefile index 2a2f001e..3e388f4b 100644 --- a/Makefile +++ b/Makefile @@ -22,18 +22,28 @@ TTRPC_MODULES = $(foreach mod,$(PROTO_MODULES),--gogottrpc_opt=M$(mod)) TTRPC_OPTIONS = $(TTRPC_INCLUDE) $(TTRPC_MODULES) --gogottrpc_opt=paths=source_relative TTRPC_COMPILE = protoc $(TTRPC_OPTIONS) +GO_CMD := go +GO_BUILD := $(GO_CMD) build + +PLUGINS := bin/logger + all: build -build: protos +build: protos binaries go build -v $(shell go list ./...) protos: $(PROTO_GOFILES) +binaries: $(PLUGINS) + %.pb.go: %.proto @echo "Generating $@..."; \ PATH=$(PATH):$(shell go env GOPATH)/bin; \ $(TTRPC_COMPILE) -I$(dir $<) --gogottrpc_out=plugins=ttrpc:$(dir $<) $< +bin/logger: $(wildcard v2alpha1/plugins/logger/*.go) + @echo "Building $@..."; \ + $(GO_BUILD) -o $@ ./$(dir $<) + install-ttrpc-plugin: go install github.com/containerd/ttrpc/cmd/protoc-gen-gogottrpc - diff --git a/v2alpha1/plugins/logger/logging.go b/v2alpha1/plugins/logger/logging.go new file mode 100644 index 00000000..6156e61e --- /dev/null +++ b/v2alpha1/plugins/logger/logging.go @@ -0,0 +1,137 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" +) + +// Logging is an interface for creating loggers and writing log messages. +type Logging interface { + Get(string) Logger + message(string, ...interface{}) + block(string, string, ...interface{}) + SetWriter(io.Writer) +} + +// Logger is an interface for logging. +type Logger interface { + Debug(string, ...interface{}) + Info(string, ...interface{}) + Warn(string, ...interface{}) + Error(string, ...interface{}) + Fatal(string, ...interface{}) + Panic(string, ...interface{}) + DebugBlock(string, string, ...interface{}) + InfoBlock(string, string, ...interface{}) + WarnBlock(string, string, ...interface{}) + ErrorBlock(string, string, ...interface{}) + SetWriter(io.Writer) +} + +type logging struct { + w io.Writer +} + +type logger struct { + log *logging + prefix string +} + +func LogWriter(w io.Writer) Logging { + return &logging{ + w: w, + } +} + +func (l *logging) Get(prefix string) Logger { + return &logger{ + log: l, + prefix: "[" + prefix + "] ", + } +} + +func (l *logging) message(format string, args ...interface{}) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf(format+"\n", args...)) + l.w.Write(buf.Bytes()) +} + +func (l *logging) block(prefix, format string, args ...interface{}) { + buf := bytes.Buffer{} + for _, line := range strings.Split(fmt.Sprintf(format, args...), "\n") { + buf.WriteString(prefix) + buf.WriteString(line) + buf.WriteString("\n") + } + l.w.Write(buf.Bytes()) +} + +func (l *logging) SetWriter(w io.Writer) { + l.w = w +} + +func (l *logger) Debug(format string, args ...interface{}) { + l.log.message("D: "+l.prefix+format, args...) +} + +func (l *logger) Info(format string, args ...interface{}) { + l.log.message("I: "+l.prefix+format, args...) +} + +func (l *logger) Warn(format string, args ...interface{}) { + l.log.message("W: "+l.prefix+format, args...) +} + +func (l *logger) Error(format string, args ...interface{}) { + l.log.message("E: "+l.prefix+format, args...) +} + +func (l *logger) Fatal(format string, args ...interface{}) { + l.log.message("Fatal Error: "+l.prefix+format, args...) + os.Exit(1) +} + +func (l *logger) Panic(format string, args ...interface{}) { + l.log.message("PANIC: "+l.prefix+format, args...) + panic(fmt.Sprintf(l.prefix+format, args...)) +} + +func (l *logger) DebugBlock(prefix, format string, args ...interface{}) { + l.log.block("D: "+l.prefix+prefix, format, args...) +} + +func (l *logger) InfoBlock(prefix, format string, args ...interface{}) { + l.log.block("I: "+l.prefix+prefix, format, args...) +} + +func (l *logger) WarnBlock(prefix, format string, args ...interface{}) { + l.log.block("W: "+l.prefix+prefix, format, args...) +} + +func (l *logger) ErrorBlock(prefix, format string, args ...interface{}) { + l.log.block("E: "+l.prefix+prefix, format, args...) +} + +func (l *logger) SetWriter(w io.Writer) { + l.log.SetWriter(w) +} + diff --git a/v2alpha1/plugins/logger/nri-logger.go b/v2alpha1/plugins/logger/nri-logger.go new file mode 100644 index 00000000..8fe99333 --- /dev/null +++ b/v2alpha1/plugins/logger/nri-logger.go @@ -0,0 +1,264 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an Sub"AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "os" + "sort" + "strings" + "sigs.k8s.io/yaml" + + "github.com/pkg/errors" + + "github.com/containerd/nri/v2alpha1/pkg/api" + "github.com/containerd/nri/v2alpha1/pkg/stub" +) + +type config struct { + LogFile string `json:"logFile"` + Events []string `json:"events"` +} + +type plugin struct { + stub stub.Stub + Logger +} + +func (p *plugin) Configure(nriCfg string) (stub.SubscribeMask, error) { + if nriCfg == "" { + return optSubscribe.Mask(), nil + } + + cfg := config{} + err := yaml.Unmarshal([]byte(nriCfg), &cfg) + if err != nil { + return 0, errors.Wrap(err, "failed to parse provided configuration") + } + + mask := stub.SubscribeMask(0) + for _, name := range cfg.Events { + m, ok := eventMask[strings.ToLower(name)] + if !ok { + return 0, errors.Errorf("invalid event name %q in configuration", name) + } + mask |= m + } + + return mask, nil +} + +func (p *plugin) Synchronize(pods []*api.PodSandbox, containers []*api.Container) ([]*api.ContainerAdjustment, error) { + p.dump("Synchronize", "pods", pods, "containers", containers) + return nil, nil +} + +func (p *plugin) Shutdown() { + p.dump("Shutdown") +} + +func (p *plugin) RunPodSandbox(pod *api.PodSandbox) { + p.dump("RunPodSandbox", "pod", pod) +} + +func (p *plugin) StopPodSandbox(pod *api.PodSandbox) { + p.dump("StopPodSandbox", "pod", pod) +} + +func (p *plugin) RemovePodSandbox(pod *api.PodSandbox) { + p.dump("RemovePodSandbox", "pod", pod) +} + +func (p *plugin) CreateContainer(pod *api.PodSandbox, container *api.Container) (*api.ContainerCreateAdjustment, []*api.ContainerAdjustment, error) { + p.dump("CreateContainer", "pod", pod, "container", container) + return nil, nil, nil +} + +func (p *plugin) PostCreateContainer(pod *api.PodSandbox, container *api.Container) { + p.dump("PostCreateContainer", "pod", pod, "container", container) +} + +func (p *plugin) StartContainer(pod *api.PodSandbox, container *api.Container) { + p.dump("StartContainer", "pod", pod, "container", container) +} + +func (p *plugin) PostStartContainer(pod *api.PodSandbox, container *api.Container) { + p.dump("PostStartContainer", "pod", pod, "container", container) +} + +func (p *plugin) UpdateContainer(pod *api.PodSandbox, container *api.Container) ([]*api.ContainerAdjustment, error) { + p.dump("UpdateContainer", "pod", pod, "container", container) + return nil, nil +} + +func (p *plugin) PostUpdateContainer(pod *api.PodSandbox, container *api.Container) { + p.dump("PostUpdateContainer", "pod", pod, "container", container) +} + +func (p *plugin) StopContainer(pod *api.PodSandbox, container *api.Container) ([]*api.ContainerAdjustment, error) { + p.dump("StopContainer", "pod", pod, "container", container) + return nil, nil +} + +func (p *plugin) RemoveContainer(pod *api.PodSandbox, container *api.Container) { + p.dump("RemoveContainer", "pod", pod, "container", container) +} + +// dump a message. +func (p *plugin) dump(msgType string, args ...interface{}) { + if len(args) & 0x1 == 1 { + p.Fatal("invalid dump: no argument for tag %q", args[len(args)-1]) + } + + for { + tag := args[0] + val := args[1] + msg, err := yaml.Marshal(val) + if err != nil { + p.Warn("failed to marshal object %q: %v", tag, err) + continue + } + + p.Info("%s: %s:", msgType, tag) + prefix := msgType+": " + p.InfoBlock(prefix, "%s", msg) + + args = args[2:] + if len(args) == 0 { + break + } + } +} + +func (p *plugin) onClose() { + os.Exit(0) +} + +type nameOption string +type idOption string +type subscribeOption stub.SubscribeMask + +func (o *nameOption) String() string { + return string(*o) +} + +func (o *nameOption) Set(value string) error { + *o = nameOption(value) + return nil +} + +func (o *idOption) String() string { + return string(*o) +} + +func (o *idOption) Set(value string) error { + *o = idOption(value) + return nil +} + +func (o *subscribeOption) Mask() stub.SubscribeMask { + return stub.SubscribeMask(*o) +} + +func (o *subscribeOption) String() string { + var events []string + for name, mask := range eventMask { + if name == "pods" || name == "containers" || name == "all" { + continue + } + if o.Mask() & mask != 0 { + events = append(events, name) + } + } + sort.Slice(events, func(i, j int) bool { + return eventMask[events[i]] < eventMask[events[j]] + }) + + return strings.Join(events, ",") +} + +func (o *subscribeOption) Set(value string) error { + var mask stub.SubscribeMask + for _, name := range strings.Split(strings.ToLower(value), ",") { + m, ok := eventMask[name] + if !ok { + return errors.Errorf("invalid event name %q", name) + } + mask |= m + } + *o = subscribeOption(mask) + return nil +} + +var eventMask = map[string]stub.SubscribeMask{ + "runpodsandbox": stub.RunPodSandbox, + "stoppodsandbox": stub.StopPodSandbox, + "removepodsandbox": stub.RemovePodSandbox, + "createcontainer": stub.CreateContainer, + "postcreatecontainer": stub.PostCreateContainer, + "startcontainer": stub.StartContainer, + "poststartcontainer": stub.PostStartContainer, + "updatecontainer": stub.UpdateContainer, + "postupdatecontainer": stub.PostUpdateContainer, + "stopcontainer": stub.StopContainer, + "removecontainer": stub.RemoveContainer, + "all": stub.AllEvents, + + "pods": stub.RunPodSandbox | stub.StopPodSandbox | stub.RemovePodSandbox, + "containers": stub.CreateContainer | stub.PostCreateContainer | + stub.StartContainer | stub.PostStartContainer | + stub.UpdateContainer | stub.PostUpdateContainer | + stub.StopContainer | + stub.RemoveContainer, +} + +var ( + optSubscribe subscribeOption + optName nameOption + optID idOption +) + +func main() { + flag.Var(&optSubscribe, "events", "comma-separated list of events to subscribe for") + flag.Var(&optName, "name", "plugin name to register") + flag.Var(&optID, "id", "plugin ID (two-letter sorting order) to register") + flag.Parse() + + p := &plugin{ + Logger: LogWriter(os.Stdout).Get("nri-logger"), + } + + opts := []stub.Option{ stub.WithOnClose(p.onClose) } + if optName.String() != "" { + opts = append(opts, stub.WithPluginName(optName.String())) + } + if optID.String() != "" { + opts = append(opts, stub.WithPluginID(optID.String())) + } + + s, err := stub.New(p, opts...) + if err != nil { + p.Fatal("failed to create plugin stub: %v", err) + } + p.stub = s + + err = p.stub.Run(context.Background()) + if err != nil { + p.Error("Plugin exited with error %v", err) + } +}