Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions pkg/plugin/common/filesystem_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// package common contains common functions and types used by all Retina plugins.
package common

import (
"fmt"

"github.com/microsoft/retina/pkg/log"
"go.uber.org/zap"
"golang.org/x/sys/unix"
)

// FileSystemChecker allows mocking Statfs for testing.
type FileSystemChecker interface {
Statfs(path string, buf *unix.Statfs_t) error
}

// UnixFileSystemChecker performs real syscalls.
type UnixFileSystemChecker struct{}

func (u *UnixFileSystemChecker) Statfs(path string, buf *unix.Statfs_t) error {
if err := unix.Statfs(path, buf); err != nil {
return fmt.Errorf("Statfs failed for %s: %w", path, err)
}
return nil
}

type fsInfo struct {
name string
paths []string
magic int64
required bool
}

// CheckMountedFilesystems checks required kernel filesystems.
func CheckMountedFilesystems(l *log.ZapLogger) error {
return CheckMountedFilesystemsWithChecker(l, &UnixFileSystemChecker{})
}

// CheckMountedFilesystemsWithChecker is testable version.
func CheckMountedFilesystemsWithChecker(l *log.ZapLogger, checker FileSystemChecker) error {
filesystems := []fsInfo{
{
name: "bpf",
paths: []string{"/sys/fs/bpf"},
magic: unix.BPF_FS_MAGIC,
required: false, // bpffs is less critical
},
{
name: "debugfs",
paths: []string{"/sys/kernel/debug"},
magic: unix.DEBUGFS_MAGIC,
required: true,
},
{
name: "tracefs",
paths: []string{"/sys/kernel/tracing", "/sys/kernel/debug/tracing"},
magic: unix.TRACEFS_MAGIC,
required: true,
},
}

missingRequired := false

for _, fs := range filesystems {
found := false

for _, path := range fs.paths {
var stat unix.Statfs_t
err := checker.Statfs(path, &stat)
if err != nil {
l.Debug("filesystem check error", zap.String("fs", fs.name), zap.String("path", path), zap.Error(err))
continue
}

if stat.Type == fs.magic {
l.Debug("filesystem mounted", zap.String("fs", fs.name), zap.String("path", path))
found = true
break
}
}

if !found {
l.Error("filesystem NOT mounted", zap.String("fs", fs.name), zap.Strings("paths", fs.paths))
if fs.required {
missingRequired = true
}
}
}

if missingRequired {
return fmt.Errorf("required filesystem missing: %w", unix.ENOENT)
}
return nil
}
130 changes: 130 additions & 0 deletions pkg/plugin/common/filesystem_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package common //nolint:revive // ignore var-naming for clarity

import (
"errors"
"testing"

"github.com/microsoft/retina/pkg/log"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"golang.org/x/sys/unix"
)

type fakeFSChecker struct {
// map[path] => (magic, error)
results map[string]struct {
magic int64
err error
}
}

var (
ErrPathNotFound = errors.New("path not found in fakeFSChecker results")
ErrNotMounted = errors.New("not mounted")
ErrNoBpf = errors.New("no bpf")
)

func (f *fakeFSChecker) Statfs(path string, buf *unix.Statfs_t) error {
if r, ok := f.results[path]; ok {
if r.err != nil {
return r.err
}
buf.Type = r.magic
return nil
}
return ErrPathNotFound
}

type ZapLogger struct {
*zap.SugaredLogger
}

func TestCheckMountedFilesystems(t *testing.T) {
zapLogger := zaptest.NewLogger(t)

logger := &log.ZapLogger{
Logger: zapLogger,
}

tests := []struct {
name string
results map[string]struct {
magic int64
err error
}
expectErr bool
}{
{
name: "all required filesystems mounted",
results: map[string]struct {
magic int64
err error
}{
"/sys/kernel/debug": {unix.DEBUGFS_MAGIC, nil},
"/sys/kernel/tracing": {unix.TRACEFS_MAGIC, nil},
"/sys/fs/bpf": {unix.BPF_FS_MAGIC, nil},
},
expectErr: false,
},
{
name: "missing required debugfs",
results: map[string]struct {
magic int64
err error
}{
"/sys/kernel/tracing": {unix.TRACEFS_MAGIC, nil},
},
expectErr: true,
},
{
name: "tracefs mounted via second path",
results: map[string]struct {
magic int64
err error
}{
"/sys/kernel/debug": {unix.DEBUGFS_MAGIC, nil},
"/sys/kernel/tracing": {0, ErrNotMounted},
"/sys/kernel/debug/tracing": {unix.TRACEFS_MAGIC, nil}, // fallback works
},
expectErr: false,
},
{
name: "non-required bpf missing but required ones present",
results: map[string]struct {
magic int64
err error
}{
"/sys/kernel/debug": {unix.DEBUGFS_MAGIC, nil},
"/sys/kernel/tracing": {unix.TRACEFS_MAGIC, nil},
},
expectErr: false,
},
{
name: "both required filesystems missing",
results: map[string]struct {
magic int64
err error
}{
"/sys/fs/bpf": {0, ErrNoBpf},
},
expectErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checker := &fakeFSChecker{
results: tt.results,
}
err := CheckMountedFilesystemsWithChecker(logger, checker)
if tt.expectErr && err == nil {
t.Errorf("expected error but got nil")
}
if !tt.expectErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
18 changes: 16 additions & 2 deletions pkg/plugin/dns/dns_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package dns

import (
"context"
"fmt"
"net"
"os"

Expand Down Expand Up @@ -47,8 +48,21 @@ func (d *dns) Compile(ctx context.Context) error {
}

func (d *dns) Init() error {
// Create tracer. In this case no parameters are passed.
err := host.Init(host.Config{})
// Check and mount filesystems before calling host.Init to avoid os.Exit()
if err := common.CheckMountedFilesystems(d.l); err != nil {
d.l.Error("Required filesystems not available for DNS plugin", zap.Error(err))
// Return error to let retina decide whether to continue without DNS plugin
// or fail the entire agent initialization
return fmt.Errorf("required filesystems not available: %w", err)
}

// Filesystems are available, safe to call host.Init()
if err := host.Init(host.Config{}); err != nil {
d.l.Error("Host initialization failed", zap.Error(err))
return fmt.Errorf("host initialization failed: %w", err)
}

// Create tracer
tracer, err := tracer.NewTracer()
if err != nil {
d.l.Error("Failed to create tracer", zap.Error(err))
Expand Down
9 changes: 9 additions & 0 deletions pkg/plugin/tcpretrans/tcpretrans_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
kcfg "github.com/microsoft/retina/pkg/config"
"github.com/microsoft/retina/pkg/enricher"
"github.com/microsoft/retina/pkg/log"
"github.com/microsoft/retina/pkg/plugin/common"
"github.com/microsoft/retina/pkg/plugin/registry"
"github.com/microsoft/retina/pkg/utils"
"go.uber.org/zap"
Expand Down Expand Up @@ -53,6 +54,14 @@ func (t *tcpretrans) Init() error {
t.l.Warn("tcpretrans will not init because pod level is disabled")
return nil
}

if err := common.CheckMountedFilesystems(t.l); err != nil {
t.l.Error("Required filesystems not available for tcpretrans plugin", zap.Error(err))
// Return error to let retina decide whether to continue without tcpretrans plugin
// or fail the entire agent initialization
return fmt.Errorf("required filesystems not available: %w", err)
}

// Create tracer. In this case no parameters are passed.
if err := host.Init(host.Config{}); err != nil {
t.l.Error("failed to init host", zap.Error(err))
Expand Down
Loading