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
70 changes: 70 additions & 0 deletions pkg/shim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# shim

This package provides a reusable Windows containerd shim bootstrap implementation. It owns the shim entrypoints, ttrpc hosting, and event publishing, and then delegates to the shim-specific service implementations supplied by the consumer package.

## Responsibilities

- Implements the shim entrypoints: `start`, `serve`, and `delete`.
- Hosts the ttrpc server and registers runtime services.
- Publishes task events back to containerd.
- Manages logging and diagnostics (ETW, named-pipe logs).
- Avoids boilerplate duplication across Windows shim binaries.

## Usage

A shim binary provides a concrete `Shim` implementation and then calls `shim.Run(...)` from `main`. This package wires the CLI entrypoints and forwards requests into the shim-specific ttrpc service registrations.

Below is a minimal example of using this package in a shim binary:

```
//go:build windows

package main

import (
"github.com/Microsoft/hcsshim/pkg/shim"
"github.com/containerd/ttrpc"
"github.com/urfave/cli"
)

type exampleShim struct{}

var _ shim.Shim = &exampleShim{}

func (s *exampleShim) Name() string {
return name
}

func (s *exampleShim) RegisterServices(ctx *cli.Context, server *ttrpc.Server, events shim.Publisher) error {
// Register shim-specific ttrpc services here.
return nil
}

func (s *exampleShim) ETW() *shim.ETWConfig {
// Return nil to disable ETW. Provide a config to enable.
return nil
}

func (s *exampleShim) Done() <-chan struct{} {
// Return a channel that closes to signal a shutdown request.
return nil
}

func main() {
shim.Run(&exampleShim{})
}
```

## CLI entrypoints

The shim binary exposes the following commands (wired in `shim.Run`):

- `start`: invoked by containerd to spawn a new shim process.
- `serve`: internal entrypoint that creates the ttrpc server and listens on a named pipe.
- `delete`: cleanup path when containerd cannot reach the shim over ttrpc.

## Logging and events

- ETW logging is enabled by returning a non-nil `ETWConfig` from `Shim.ETW`.
- Named-pipe logging is used when `Options_NPIPE` is selected in shim options.
- The `Publisher` interface sends task events back to containerd.
127 changes: 127 additions & 0 deletions pkg/shim/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//go:build windows

package shim

import (
"context"
"fmt"
"os"
"path/filepath"
"time"

"github.com/Microsoft/hcsshim/internal/hcs"
"github.com/Microsoft/hcsshim/internal/memory"
"github.com/Microsoft/hcsshim/internal/oc"

"github.com/containerd/containerd/api/runtime/task/v2"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)

func getDeleteCommand(_ Shim) cli.Command {
var deleteCommand = cli.Command{
Name: "delete",
Usage: `
This command allows containerd to delete any container resources created, mounted, and/or run by a shim when containerd can no longer communicate over rpc. This happens if a shim is SIGKILL'd with a running container. These resources will need to be cleaned up when containerd loses the connection to a shim. This is also used when containerd boots and reconnects to shims. If a bundle is still on disk but containerd cannot connect to a shim, the delete command is invoked.

The delete command will be executed in the container's bundle as its cwd.
`,
SkipArgReorder: true,
Action: func(cCtx *cli.Context) (err error) {
// We cant write anything to stdout for this cmd other than the
// task.DeleteResponse by protocol. We can write to stderr which will be
// logged as a warning in containerd.

ctx, span := oc.StartSpan(context.Background(), "delete")
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()

// Get the shim context values.
shimCtx := parseContext(cCtx)

bundleFlag := cCtx.GlobalString("bundle")
if bundleFlag == "" {
return errors.New("bundle is required")
}

// hcsshim shim writes panic logs in the bundle directory in a file named "panic.log"
// log those messages (if any) on stderr so that it shows up in containerd's log.
// This should be done as the first thing so that we don't miss any panic logs even if
// something goes wrong during delete op.
// The file can be very large so read only first 1MB of data.
readLimit := int64(memory.MiB) // 1MB
logBytes, err := limitedRead(filepath.Join(bundleFlag, "panic.log"), readLimit)
if err == nil && len(logBytes) > 0 {
if int64(len(logBytes)) == readLimit {
logrus.Warnf("shim panic log file %s is larger than 1MB, logging only first 1MB", filepath.Join(bundleFlag, "panic.log"))
}
logrus.WithField("log", string(logBytes)).Warn("found shim panic logs during delete")
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
logrus.WithError(err).Warn("failed to open shim panic log")
}

// Attempt to find the hcssystem for this bundle and terminate it.
if sys, _ := hcs.OpenComputeSystem(ctx, shimCtx.id); sys != nil {
defer sys.Close()
if err := sys.Terminate(ctx); err != nil {
fmt.Fprintf(os.Stderr, "failed to terminate '%s': %v", shimCtx.id, err)
} else {
ch := make(chan error, 1)
go func() { ch <- sys.Wait() }()
t := time.NewTimer(time.Second * 30)
select {
case <-t.C:
sys.Close()
return fmt.Errorf("timed out waiting for '%s' to terminate", shimCtx.id)
case err := <-ch:
t.Stop()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to wait for '%s' to terminate: %v", shimCtx.id, err)
}
}
}
}

if data, err := proto.Marshal(&task.DeleteResponse{
ExitedAt: timestamppb.New(time.Now()),
ExitStatus: 255,
}); err != nil {
return err
} else {
if _, err := os.Stdout.Write(data); err != nil {
return err
}
}
return nil
},
}

return deleteCommand
}

// limitedRead reads at max `readLimitBytes` bytes from the file at path `filePath`. If the file has
// more than `readLimitBytes` bytes of data then first `readLimitBytes` will be returned.
// Read at most readLimitBytes so delete does not flood logs.
func limitedRead(filePath string, readLimitBytes int64) ([]byte, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, errors.Wrapf(err, "limited read failed to open file: %s", filePath)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return []byte{}, errors.Wrapf(err, "limited read failed during file stat: %s", filePath)
}
if fi.Size() < readLimitBytes {
readLimitBytes = fi.Size()
}
buf := make([]byte, readLimitBytes)
_, err = f.Read(buf)
if err != nil {
return []byte{}, errors.Wrapf(err, "limited read failed during file read: %s", filePath)
}
return buf, nil
}
41 changes: 41 additions & 0 deletions pkg/shim/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//go:build windows

package shim

import (
"os"
"path/filepath"
"testing"
)

func TestLimitedRead(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "panic.log")
content := []byte("hello")
if err := os.WriteFile(filePath, content, 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}

buf, err := limitedRead(filePath, 2)
if err != nil {
t.Fatalf("limitedRead: %v", err)
}
if string(buf) != "he" {
t.Fatalf("expected 'he', got %q", string(buf))
}

buf, err = limitedRead(filePath, 10)
if err != nil {
t.Fatalf("limitedRead: %v", err)
}
if string(buf) != "hello" {
t.Fatalf("expected 'hello', got %q", string(buf))
}
}

func TestLimitedReadMissingFile(t *testing.T) {
_, err := limitedRead(filepath.Join(t.TempDir(), "missing.log"), 10)
if err == nil {
t.Fatalf("expected error for missing file")
}
}
56 changes: 56 additions & 0 deletions pkg/shim/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//go:build windows

package shim

import (
"context"
"fmt"

"github.com/Microsoft/hcsshim/internal/oc"

"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/shim"
"go.opencensus.io/trace"
)

// Publisher is an interface for publishing events to a remote event bus.
type Publisher interface {
PublishEvent(ctx context.Context, topic string, event interface{}) (err error)
}

type eventPublisher struct {
namespace string
remotePublisher *shim.RemoteEventsPublisher
}

var _ Publisher = &eventPublisher{}

func newEventPublisher(address, namespace string) (*eventPublisher, error) {
p, err := shim.NewPublisher(address)
if err != nil {
return nil, err
}
return &eventPublisher{
namespace: namespace,
remotePublisher: p,
}, nil
}

func (e *eventPublisher) close() error {
return e.remotePublisher.Close()
}

func (e *eventPublisher) PublishEvent(ctx context.Context, topic string, event interface{}) (err error) {
if e == nil || e.remotePublisher == nil {
// No-op when events are not configured.
return nil
}
ctx, span := oc.StartSpan(ctx, "publishEvent")
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()
span.AddAttributes(
trace.StringAttribute("topic", topic),
trace.StringAttribute("event", fmt.Sprintf("%+v", event)))

return e.remotePublisher.Publish(namespaces.WithNamespace(ctx, e.namespace), topic, event)
}
15 changes: 15 additions & 0 deletions pkg/shim/events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build windows

package shim

import (
"context"
"testing"
)

func TestPublishEventNilPublisher(t *testing.T) {
var p *eventPublisher
if err := p.PublishEvent(context.Background(), "topic", "event"); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}
Loading