Skip to content

Commit

Permalink
[feat] Standalone control commands (slimtoolkit#596)
Browse files Browse the repository at this point in the history
To control sensor execution when running in the standalone mode.
This PR ships the machinery and the very first command:

```
/opt/_slim/sensor control stop-target-app
```

The command writes to the commands FIFO file and then waits for the
StopMonitorDone event to show up in the events file.

Signed-off-by: Ivan Velichko <iximiuz@gmail.com>
  • Loading branch information
iximiuz authored Oct 23, 2023
1 parent d110795 commit ddadba6
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 22 deletions.
68 changes: 50 additions & 18 deletions pkg/app/sensor/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/docker-slim/docker-slim/pkg/app/sensor/execution"
"github.com/docker-slim/docker-slim/pkg/app/sensor/monitor"
"github.com/docker-slim/docker-slim/pkg/app/sensor/standalone"
"github.com/docker-slim/docker-slim/pkg/app/sensor/standalone/control"
"github.com/docker-slim/docker-slim/pkg/appbom"
"github.com/docker-slim/docker-slim/pkg/ipc/event"
"github.com/docker-slim/docker-slim/pkg/mondel"
Expand Down Expand Up @@ -75,7 +76,6 @@ const (
artifactsDirFlagUsage = "output director for all sensor artifacts"
artifactsDirFlagDefault = app.DefaultArtifactsDirPath

enableMondelFlag = "mondel"
enableMondelFlagUsage = "enable monitor data event logging"
enableMondelFlagDefault = false
)
Expand All @@ -92,11 +92,15 @@ var (
lifecycleHookCommand *string = flag.String("lifecycle-hook", lifecycleHookCommandFlagDefault, lifecycleHookCommandFlagUsage)
stopSignal *string = flag.String("stop-signal", stopSignalFlagDefault, stopSignalFlagUsage)
stopGracePeriod *time.Duration = flag.Duration("stop-grace-period", stopGracePeriodFlagDefault, stopGracePeriodFlagUsage)
enableMondel *bool = flag.Bool(enableMondelFlag, enableMondelFlagDefault, enableMondelFlagUsage)
enableMondel *bool = flag.Bool("mondel", enableMondelFlagDefault, enableMondelFlagUsage)

errUnknownMode = errors.New("unknown sensor mode")
)

func eventsFilePath() string {
return filepath.Join(*artifactsDir, "events.json")
}

func init() {
flag.BoolVar(getAppBom, "b", getAppBomFlagDefault, getAppBomFlagUsage)
flag.BoolVar(enableDebug, "d", enableDebugFlagDefault, enableDebugFlagUsage)
Expand All @@ -112,20 +116,6 @@ func init() {
flag.BoolVar(enableMondel, "n", enableMondelFlagDefault, enableMondelFlagUsage)
}

func dumpAppBom() {
info := appbom.Get()
if info == nil {
return
}

var out bytes.Buffer
encoder := json.NewEncoder(&out)
encoder.SetEscapeHTML(false)
encoder.SetIndent(" ", " ")
_ = encoder.Encode(info)
fmt.Printf("%s\n", out.String())
}

// Run starts the sensor app
func Run() {
flag.Parse()
Expand All @@ -137,6 +127,13 @@ func Run() {

errutil.FailOn(configureLogger(*enableDebug, *logLevel, *logFormat, *logFile))

ctx := context.Background()

if len(os.Args) > 1 && os.Args[1] == "control" {
runControlCommand(ctx)
return
}

activeCaps, maxCaps, err := sysenv.Capabilities(0)
errutil.WarnOn(err)
log.Infof("sensor: ver=%v", version.Current())
Expand All @@ -158,12 +155,11 @@ func Run() {
}
artifactor := artifact.NewProcessor(*artifactsDir, artifactsExtra)

ctx := context.Background()
exe, err := newExecution(
ctx,
*sensorMode,
*commandsFile,
filepath.Join(*artifactsDir, "events.json"),
eventsFilePath(),
*lifecycleHookCommand,
)
if err != nil {
Expand Down Expand Up @@ -286,3 +282,39 @@ func newSensor(
exe.PubEvent(event.StartMonitorFailed, errUnknownMode.Error())
return nil, errUnknownMode
}

func dumpAppBom() {
info := appbom.Get()
if info == nil {
return
}

var out bytes.Buffer
encoder := json.NewEncoder(&out)
encoder.SetEscapeHTML(false)
encoder.SetIndent(" ", " ")
_ = encoder.Encode(info)
fmt.Printf("%s\n", out.String())
}

// sensor control <stop-target-app|change-log-level|...>
func runControlCommand(ctx context.Context) {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "Missing control command")
os.Exit(1)
}

cmd := control.Command(os.Args[2])

switch cmd {
case control.StopTargetAppCommand:
if err := control.ExecuteStopTargetAppCommand(ctx, *commandsFile, eventsFilePath()); err != nil {
fmt.Fprintln(os.Stderr, "Error stopping target app:", err)
os.Exit(1)
}

default:
fmt.Fprintln(os.Stderr, "Unknown control command:", cmd)
os.Exit(1)
}
}
19 changes: 19 additions & 0 deletions pkg/app/sensor/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
log "github.com/sirupsen/logrus"

"github.com/docker-slim/docker-slim/pkg/app/sensor/standalone/control"
"github.com/docker-slim/docker-slim/pkg/ipc/event"
"github.com/docker-slim/docker-slim/pkg/report"
testsensor "github.com/docker-slim/docker-slim/pkg/test/e2e/sensor"
Expand Down Expand Up @@ -557,3 +558,21 @@ func TestArchiveArtifacts_SensorFailure_NoRoot(t *testing.T) {
// It's a fairly common failure scenario.
t.Skip("Implement me!")
}

func TestControlCommands_StopTargetApp(t *testing.T) {
runID := newTestRun(t)
ctx := context.Background()

sensor := testsensor.NewSensorOrFail(t, ctx, t.TempDir(), runID, imageSimpleService)
defer sensor.Cleanup(t, ctx)

sensor.StartStandaloneOrFail(t, ctx, nil)

go testutil.Delayed(ctx, 5*time.Second, func() {
sensor.ExecuteControlCommandOrFail(t, ctx, control.StopTargetAppCommand)
})

sensor.WaitOrFail(t, ctx)

sensor.AssertSensorLogsContain(t, ctx, sensorFullLifecycleSequence...)
}
5 changes: 4 additions & 1 deletion pkg/app/sensor/execution/standalone.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

log "github.com/sirupsen/logrus"

"github.com/docker-slim/docker-slim/pkg/app/sensor/standalone/control"
"github.com/docker-slim/docker-slim/pkg/ipc/command"
"github.com/docker-slim/docker-slim/pkg/ipc/event"
"github.com/docker-slim/docker-slim/pkg/util/fsutil"
Expand Down Expand Up @@ -54,9 +55,11 @@ func NewStandalone(
)
}

commandCh := make(chan command.Message, 1)
commandCh := make(chan command.Message, 10)
commandCh <- &cmd

go control.HandleControlCommandQueue(ctx, commandFileName, commandCh)

return &standaloneExe{
hookExecutor: hookExecutor{
ctx: ctx,
Expand Down
7 changes: 7 additions & 0 deletions pkg/app/sensor/standalone/control/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package control

type Command string

const (
StopTargetAppCommand Command = "stop-target-app"
)
83 changes: 83 additions & 0 deletions pkg/app/sensor/standalone/control/queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package control

import (
"bufio"
"context"
"io"
"os"
"syscall"
"time"

log "github.com/sirupsen/logrus"

"github.com/docker-slim/docker-slim/pkg/ipc/command"
)

func HandleControlCommandQueue(ctx context.Context, commandsFile string, commandCh chan command.Message) {
fifoPath := getFIFOPath(commandsFile)
if !createFIFOIfNeeded(fifoPath) {
return
}
go func() {
<-ctx.Done()
os.Remove(fifoPath)
}()

processCommandsFromFIFO(ctx, fifoPath, commandCh)
}

func getFIFOPath(commandsFile string) string {
return commandsFile + ".fifo"
}

func createFIFOIfNeeded(fifoPath string) bool {
if _, err := os.Stat(fifoPath); os.IsNotExist(err) {
if err = syscall.Mkfifo(fifoPath, 0600); err != nil {
log.Warnf("sensor: control commands not activated - cannot create %s FIFO file: %s", fifoPath, err)
return false
}
log.Info("sensor: control commands activated")
}
return true
}

func processCommandsFromFIFO(ctx context.Context, fifoPath string, commandCh chan command.Message) {
for ctx.Err() == nil {
fifo, err := os.Open(fifoPath)
if err != nil {
log.Debugf("sensor: control commands - cannot open %s FIFO file: %s", fifoPath, err)
time.Sleep(1 * time.Second)
continue
}

readAndHandleCommands(fifo, commandCh)
fifo.Close()
}
}

func readAndHandleCommands(fifo *os.File, commandCh chan command.Message) {
reader := bufio.NewReader(fifo)
for {
line, err := reader.ReadBytes('\n')
if len(line) > 0 {
handleCommand(line, commandCh)
}

if err == io.EOF {
return
}
if err != nil {
log.Warnf("sensor: error reading control command: %s", err)
time.Sleep(1 * time.Second)
}
}
}

func handleCommand(line []byte, commandCh chan command.Message) {
msg, err := command.Decode(line)
if err == nil {
commandCh <- msg
} else {
log.Warnf("sensor: cannot decode control command %#q: %s", line, err)
}
}
75 changes: 75 additions & 0 deletions pkg/app/sensor/standalone/control/stop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package control

import (
"bufio"
"context"
"fmt"
"os"
"strings"
"time"

"github.com/docker-slim/docker-slim/pkg/ipc/command"
"github.com/docker-slim/docker-slim/pkg/ipc/event"
"github.com/docker-slim/docker-slim/pkg/util/fsutil"
)

func ExecuteStopTargetAppCommand(
ctx context.Context,
commandsFile string,
eventsFile string,
) error {
msg, err := command.Encode(&command.StopMonitor{})
if err != nil {
return fmt.Errorf("cannot encode stop command: %w", err)
}

if err := fsutil.AppendToFile(getFIFOPath(commandsFile), msg, false); err != nil {
return fmt.Errorf("cannot append stop command to FIFO file: %w", err)
}

if err := waitForEvent(ctx, eventsFile, event.StopMonitorDone); err != nil {
return fmt.Errorf("waiting for %v event: %w", event.StopMonitorDone, err)
}

return nil
}

func waitForEvent(ctx context.Context, eventsFile string, target event.Type) error {
for ctx.Err() == nil {
found, err := findEvent(eventsFile, target)
if err != nil {
return err
}

if found {
return nil
}

time.Sleep(1 * time.Second)
}

return ctx.Err()
}

func findEvent(eventsFile string, target event.Type) (bool, error) {
file, err := os.Open(eventsFile)
if err != nil {
return false, err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// A bit hacky - we probably need to parse the event struct properly.
if strings.Contains(line, string(target)) {
return true, nil
}
}

if scanner.Err() != nil {
return false, scanner.Err()
}

return false, nil
}
Loading

0 comments on commit ddadba6

Please sign in to comment.