Skip to content

Commit 83fc8a3

Browse files
committed
Add LCOW logpath within uVM
A common scenario for pods is to write container logs (`stdout` and `stderr`) to a file on the host (specified by the `LogPath` and `LogDirectory` CRI fields), and then mount that directory to another (e.g., a monitoring or observability container) in the pod for consumption. For LCOW, this means logs must traverse from the uVM to the host, and then back into the uVM via (plan9) share, which both: - adds unnecessary overhead; and - exposes data to the host. Address this by adding annotations to mimic `LogPath` and `LogDirectory` functionality, but within the uVM. Since the uVM rootfs is not directly backed by a VHD (i.e., directories are either RO or `tmpfs` backed), create a dedicated directory in the sandbox scratch for logs, and allow that path to be mounted within a container. Note: containerd expects to be the end-all sink for container logs, use `io.MultiWriter` to keep default behavior and write logs to the specified path while also exposing them to the host. Therefore, annotations use `TeeLog` (instead of just `Log`) to make behavior explicit. Add `LCOWTeeLogPath` annotation to tee container `stdin` and `stderr` to a specified file within the uVM, relative to a dedicated log directory in the sandbox scratch. Add `LCOWTeeLogDirMount` annotation to mount the above log directory to the specified path within the container. Add a dedicated `trapsort.MultiWriter` type that writes to both an underlying `transport.Connection` and the provided `io.Writer`. Move `logConnection` from `stdio` to `transport`, to keep the different `Connection` implementations together. Add functional tests. Streamline `test/internal/util` by making `CleanName` operation on the test name directly (`testing.TB.Name()`), since that is the majority of the usage. Add `CleanString` to handle the original functionality. Signed-off-by: Hamza El-Saawy <[email protected]>
1 parent 296144f commit 83fc8a3

File tree

14 files changed

+392
-77
lines changed

14 files changed

+392
-77
lines changed

internal/guest/runtime/hcsv2/container.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ const (
4545
)
4646

4747
type Container struct {
48-
id string
49-
vsock transport.Transport
48+
id string
49+
50+
vsock transport.Transport
51+
logFile *os.File
5052

5153
spec *oci.Spec
5254
ociBundlePath string
@@ -77,10 +79,17 @@ type Container struct {
7779

7880
func (c *Container) Start(ctx context.Context, conSettings stdio.ConnectionSettings) (int, error) {
7981
log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::Start")
80-
stdioSet, err := stdio.Connect(c.vsock, conSettings)
82+
83+
// only use the logfile for the init process, since we don't want to tee stdio of execs
84+
t := c.vsock
85+
if c.logFile != nil {
86+
t = transport.NewMultiWriter(c.vsock, c.logFile)
87+
}
88+
stdioSet, err := stdio.Connect(t, conSettings)
8189
if err != nil {
8290
return -1, err
8391
}
92+
8493
if c.initProcess.spec.Terminal {
8594
ttyr := c.container.Tty()
8695
ttyr.ReplaceConnectionSet(stdioSet)
@@ -180,12 +189,24 @@ func (c *Container) GetAllProcessPids(ctx context.Context) ([]int, error) {
180189

181190
// Kill sends 'signal' to the container process.
182191
func (c *Container) Kill(ctx context.Context, signal syscall.Signal) error {
183-
log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::Kill")
192+
entity := log.G(ctx).WithField(logfields.ContainerID, c.id)
193+
entity.Info("opengcs::Container::Kill")
184194
err := c.container.Kill(signal)
185195
if err != nil {
186196
return err
187197
}
198+
188199
c.setExitType(signal)
200+
if c.logFile != nil {
201+
if err = c.logFile.Close(); err != nil {
202+
entity.WithFields(logrus.Fields{
203+
logrus.ErrorKey: err,
204+
logfields.Path: c.logFile.Name(),
205+
}).Warn("failed to close log file")
206+
}
207+
c.logFile = nil
208+
}
209+
189210
return nil
190211
}
191212

internal/guest/runtime/hcsv2/uvm.go

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ import (
2222

2323
"github.com/Microsoft/cosesign1go/pkg/cosesign1"
2424
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
25+
cgroup1stats "github.com/containerd/cgroups/v3/cgroup1/stats"
26+
"github.com/mattn/go-shellwords"
27+
"github.com/opencontainers/runtime-spec/specs-go"
28+
"github.com/pkg/errors"
29+
"github.com/sirupsen/logrus"
30+
"golang.org/x/sys/unix"
31+
2532
"github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr"
2633
"github.com/Microsoft/hcsshim/internal/debug"
2734
"github.com/Microsoft/hcsshim/internal/guest/policy"
@@ -37,18 +44,13 @@ import (
3744
"github.com/Microsoft/hcsshim/internal/guest/storage/scsi"
3845
"github.com/Microsoft/hcsshim/internal/guest/transport"
3946
"github.com/Microsoft/hcsshim/internal/log"
47+
"github.com/Microsoft/hcsshim/internal/logfields"
4048
"github.com/Microsoft/hcsshim/internal/oci"
4149
"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
4250
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
4351
"github.com/Microsoft/hcsshim/internal/verity"
4452
"github.com/Microsoft/hcsshim/pkg/annotations"
4553
"github.com/Microsoft/hcsshim/pkg/securitypolicy"
46-
cgroup1stats "github.com/containerd/cgroups/v3/cgroup1/stats"
47-
"github.com/mattn/go-shellwords"
48-
"github.com/opencontainers/runtime-spec/specs-go"
49-
"github.com/pkg/errors"
50-
"github.com/sirupsen/logrus"
51-
"golang.org/x/sys/unix"
5254
)
5355

5456
// UVMContainerID is the ContainerID that will be sent on any prot.MessageBase
@@ -299,6 +301,17 @@ func setupSandboxHugePageMountsPath(id string) error {
299301
return storage.MountRShared(mountPath)
300302
}
301303

304+
func setupSandboxLogDir(id string) error {
305+
mountPath := specGuest.SandboxLogsDir(id)
306+
if err := mkdirAllModePerm(mountPath); err != nil {
307+
return errors.Wrapf(err, "failed to create sandbox logs dir in sandbox %v", id)
308+
}
309+
return nil
310+
}
311+
312+
// TODO: unify workload and standalone logic for non-sandbox features (e.g., block devices, huge pages, uVM mounts)
313+
// TODO(go1.24): use [os.Root] instead of `!strings.HasPrefix(<path>, <root>)`
314+
302315
func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VMHostedContainerSettingsV2) (_ *Container, err error) {
303316
criType, isCRI := settings.OCISpecification.Annotations[annotations.KubernetesContainerType]
304317
c := &Container{
@@ -354,6 +367,9 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
354367
if err = setupSandboxHugePageMountsPath(id); err != nil {
355368
return nil, err
356369
}
370+
if err = setupSandboxLogDir(id); err != nil {
371+
return nil, err
372+
}
357373

358374
if err := policy.ExtendPolicyWithNetworkingMounts(id, h.securityPolicyEnforcer, settings.OCISpecification); err != nil {
359375
return nil, err
@@ -405,6 +421,17 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
405421
}
406422
}
407423

424+
// don't specialize tee logs (both files and mounts) just for workload containers
425+
// add log directory mount before enforcing (mount) policy
426+
if logDirMount := settings.OCISpecification.Annotations[annotations.LCOWTeeLogDirMount]; logDirMount != "" {
427+
settings.OCISpecification.Mounts = append(settings.OCISpecification.Mounts, specs.Mount{
428+
Destination: logDirMount,
429+
Type: "bind",
430+
Source: specGuest.SandboxLogsDir(sandboxID),
431+
Options: []string{"bind"},
432+
})
433+
}
434+
408435
user, groups, umask, err := h.securityPolicyEnforcer.GetUserInfo(settings.OCISpecification.Process, settings.OCISpecification.Root.Path)
409436
if err != nil {
410437
return nil, err
@@ -441,6 +468,31 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
441468
c.vsock = h.devNullTransport
442469
}
443470

471+
if logPath := settings.OCISpecification.Annotations[annotations.LCOWTeeLogPath]; logPath != "" {
472+
if !allowStdio {
473+
return nil, errors.Errorf("teeing container stdio to log path %q denied due to policy not allowing stdio access", logPath)
474+
}
475+
476+
logPath = specGuest.SandboxLogPath(sandboxID, logPath)
477+
// verify the logpath is still under the correct directory
478+
if !strings.HasPrefix(logPath, specGuest.SandboxLogsDir(sandboxID)) {
479+
return nil, errors.Errorf("log path %v is not within sandbox's log dir", logPath)
480+
}
481+
482+
log.G(ctx).WithFields(logrus.Fields{
483+
logfields.Path: logPath,
484+
logfields.ContainerID: id,
485+
}).Debug("creating container log file in uVM")
486+
dir := filepath.Dir(logPath)
487+
if err := mkdirAllModePerm(dir); err != nil {
488+
return nil, errors.Wrapf(err, "failed to create log file parent directory: %s", dir)
489+
}
490+
// don't use [os.Create] since that truncates an existing file, which is not desired
491+
if c.logFile, err = os.OpenFile(logPath, os.O_RDWR|os.O_CREATE, 0666); err != nil {
492+
return nil, errors.Wrapf(err, "failed to create log file: %s", logPath)
493+
}
494+
}
495+
444496
if envToKeep != nil {
445497
settings.OCISpecification.Process.Env = []string(envToKeep)
446498
}

internal/guest/spec/spec.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,30 @@ func HugePagesMountsDir(sandboxID string) string {
8989
return filepath.Join(SandboxRootDir(sandboxID), "hugepages")
9090
}
9191

92-
// SandboxMountSource returns sandbox mount path inside UVM
92+
// SandboxMountSource returns sandbox mount path inside UVM.
9393
func SandboxMountSource(sandboxID, path string) string {
9494
mountsDir := SandboxMountsDir(sandboxID)
9595
subPath := strings.TrimPrefix(path, guestpath.SandboxMountPrefix)
9696
return filepath.Join(mountsDir, subPath)
9797
}
9898

99-
// HugePagesMountSource returns hugepages mount path inside UVM
99+
// HugePagesMountSource returns hugepages mount path inside UVM.
100100
func HugePagesMountSource(sandboxID, path string) string {
101101
mountsDir := HugePagesMountsDir(sandboxID)
102102
subPath := strings.TrimPrefix(path, guestpath.HugePagesMountPrefix)
103103
return filepath.Join(mountsDir, subPath)
104104
}
105105

106+
// SandboxLogsDir returns the logs directory inside the UVM for forwarding container stdio to.
107+
func SandboxLogsDir(sandboxID string) string {
108+
return filepath.Join(SandboxRootDir(sandboxID), "logs")
109+
}
110+
111+
// SandboxLogPath returns the log path inside the UVM.
112+
func SandboxLogPath(sandboxID, path string) string {
113+
return filepath.Join(SandboxLogsDir(sandboxID), path)
114+
}
115+
106116
// GetNetworkNamespaceID returns the `ToLower` of
107117
// `spec.Windows.Network.NetworkNamespace` or `""`.
108118
func GetNetworkNamespaceID(spec *oci.Spec) string {

internal/guest/stdio/connection.go

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
package stdio
55

66
import (
7-
"os"
7+
"github.com/pkg/errors"
88

99
"github.com/Microsoft/hcsshim/internal/guest/transport"
10-
"github.com/pkg/errors"
11-
"github.com/sirupsen/logrus"
1210
)
1311

1412
// ConnectionSettings describe the stdin, stdout, stderr ports to connect the
@@ -19,49 +17,6 @@ type ConnectionSettings struct {
1917
StdErr *uint32
2018
}
2119

22-
type logConnection struct {
23-
con transport.Connection
24-
port uint32
25-
}
26-
27-
func (lc *logConnection) Read(b []byte) (int, error) {
28-
return lc.con.Read(b)
29-
}
30-
31-
func (lc *logConnection) Write(b []byte) (int, error) {
32-
return lc.con.Write(b)
33-
}
34-
35-
func (lc *logConnection) Close() error {
36-
logrus.WithFields(logrus.Fields{
37-
"port": lc.port,
38-
}).Debug("opengcs::logConnection::Close - closing connection")
39-
40-
return lc.con.Close()
41-
}
42-
43-
func (lc *logConnection) CloseRead() error {
44-
logrus.WithFields(logrus.Fields{
45-
"port": lc.port,
46-
}).Debug("opengcs::logConnection::Close - closing read connection")
47-
48-
return lc.con.CloseRead()
49-
}
50-
51-
func (lc *logConnection) CloseWrite() error {
52-
logrus.WithFields(logrus.Fields{
53-
"port": lc.port,
54-
}).Debug("opengcs::logConnection::Close - closing write connection")
55-
56-
return lc.con.CloseWrite()
57-
}
58-
59-
func (lc *logConnection) File() (*os.File, error) {
60-
return lc.con.File()
61-
}
62-
63-
var _ = (transport.Connection)(&logConnection{})
64-
6520
// Connect returns new transport.Connection instances, one for each stdio pipe
6621
// to be used. If CreateStd*Pipe for a given pipe is false, the given Connection
6722
// is set to nil.
@@ -77,30 +32,21 @@ func Connect(tport transport.Transport, settings ConnectionSettings) (_ *Connect
7732
if err != nil {
7833
return nil, errors.Wrap(err, "failed creating stdin Connection")
7934
}
80-
connSet.In = &logConnection{
81-
con: c,
82-
port: *settings.StdIn,
83-
}
35+
connSet.In = transport.NewLogConnection(c, *settings.StdIn)
8436
}
8537
if settings.StdOut != nil {
8638
c, err := tport.Dial(*settings.StdOut)
8739
if err != nil {
8840
return nil, errors.Wrap(err, "failed creating stdout Connection")
8941
}
90-
connSet.Out = &logConnection{
91-
con: c,
92-
port: *settings.StdOut,
93-
}
42+
connSet.Out = transport.NewLogConnection(c, *settings.StdOut)
9443
}
9544
if settings.StdErr != nil {
9645
c, err := tport.Dial(*settings.StdErr)
9746
if err != nil {
9847
return nil, errors.Wrap(err, "failed creating stderr Connection")
9948
}
100-
connSet.Err = &logConnection{
101-
con: c,
102-
port: *settings.StdErr,
103-
}
49+
connSet.Err = transport.NewLogConnection(c, *settings.StdErr)
10450
}
10551
return connSet, nil
10652
}

internal/guest/transport/log.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//go:build linux
2+
3+
package transport
4+
5+
import (
6+
"github.com/sirupsen/logrus"
7+
)
8+
9+
// logConnection wraps the underlying [Connection] and logs the Close*() operations with
10+
// the connection's port number.
11+
type logConnection struct {
12+
Connection
13+
entry *logrus.Entry
14+
}
15+
16+
var _ Connection = &logConnection{}
17+
18+
func NewLogConnection(c Connection, port uint32) Connection {
19+
return &logConnection{c, logrus.WithField("port", port)}
20+
}
21+
22+
func (c *logConnection) Close() error {
23+
c.entry.Debug("opengcs::logConnection::Close - closing connection")
24+
25+
return c.Connection.Close()
26+
}
27+
28+
func (c *logConnection) CloseRead() error {
29+
c.entry.Debug("opengcs::logConnection::Close - closing read connection")
30+
31+
return c.Connection.CloseRead()
32+
}
33+
34+
func (c *logConnection) CloseWrite() error {
35+
c.entry.Debug("opengcs::logConnection::Close - closing write connection")
36+
37+
return c.Connection.CloseWrite()
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build linux
2+
3+
package transport
4+
5+
import (
6+
"fmt"
7+
"io"
8+
)
9+
10+
// multiWriter writes to both the underlying connection and the [io.Writer], w, via [io.multiWriter].
11+
type multiWriter struct {
12+
t Transport
13+
w io.Writer
14+
}
15+
16+
var _ Transport = &multiWriter{}
17+
18+
func NewMultiWriter(t Transport, w io.Writer) Transport {
19+
return &multiWriter{t, w}
20+
}
21+
22+
// Dial accepts a vsock socket port number as configuration, and
23+
// returns an unconnected VsockConnection struct.
24+
func (t *multiWriter) Dial(port uint32) (Connection, error) {
25+
if t == nil || t.w == nil || t.t == nil {
26+
return nil, fmt.Errorf("invalid transpot")
27+
}
28+
29+
conn, err := t.t.Dial(port)
30+
if err != nil {
31+
return nil, fmt.Errorf("multiwriter base transport dial: %w", err)
32+
}
33+
return &multiWriterConnection{conn, io.MultiWriter(conn, t.w)}, nil
34+
}
35+
36+
type multiWriterConnection struct {
37+
Connection
38+
multi io.Writer
39+
}
40+
41+
var _ Connection = &multiWriterConnection{}
42+
43+
func (c *multiWriterConnection) Write(buf []byte) (int, error) {
44+
return c.multi.Write(buf)
45+
}

internal/guest/transport/transport.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"os"
66
)
77

8+
// TODO: specialized [Transport] and [Connection] for [io.Reader]/[io.Writer] instead of both,
9+
// so either stdin or stdout/stderr can be specialized without affecting the other.
10+
// i.e., don't use [multiWriter] for stdin.
11+
812
// Transport is the interface defining a method of transporting data in a
913
// connection-like way.
1014
// Examples of a Transport implementation could be:

0 commit comments

Comments
 (0)