Skip to content

Commit d5a05aa

Browse files
authored
Add LCOW logpath within uVM (#2511)
* 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. PR: move log file creation/management to (*Container).Start Signed-off-by: Hamza El-Saawy <[email protected]> * Fix `ambiguous import` due to `google.golang.org/genproto` Seems to be caused primarily by the `github.com/containerd/errdefs/pkg/errgrpc` import, but unclear why it is appearing now. Add replace directive to forcibly ignore older/pre-submodule `google.golang.org/genproto` package. See: googleapis/go-genproto#1015 Signed-off-by: Hamza El-Saawy <[email protected]> --------- Signed-off-by: Hamza El-Saawy <[email protected]>
1 parent 15a6afe commit d5a05aa

File tree

17 files changed

+2072
-98
lines changed

17 files changed

+2072
-98
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,9 @@ require (
116116
golang.org/x/net v0.43.0 // indirect
117117
golang.org/x/text v0.28.0 // indirect
118118
golang.org/x/tools v0.36.0 // indirect
119-
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
120119
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
121120
gopkg.in/yaml.v3 v3.0.1 // indirect
122121
sigs.k8s.io/yaml v1.4.0 // indirect
123122
)
123+
124+
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250428153025-10db94c68c34

go.sum

Lines changed: 1650 additions & 12 deletions
Large diffs are not rendered by default.

internal/guest/runtime/hcsv2/container.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ const (
4646
)
4747

4848
type Container struct {
49-
id string
50-
vsock transport.Transport
49+
id string
50+
51+
vsock transport.Transport
52+
logPath string // path to [logFile].
53+
logFile *os.File // file to redirect container's stdio to.
5154

5255
spec *oci.Spec
5356
ociBundlePath string
@@ -76,12 +79,42 @@ type Container struct {
7679
scratchDirPath string
7780
}
7881

79-
func (c *Container) Start(ctx context.Context, conSettings stdio.ConnectionSettings) (int, error) {
80-
log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::Start")
81-
stdioSet, err := stdio.Connect(c.vsock, conSettings)
82+
func (c *Container) Start(ctx context.Context, conSettings stdio.ConnectionSettings) (_ int, err error) {
83+
entity := log.G(ctx).WithField(logfields.ContainerID, c.id)
84+
entity.Info("opengcs::Container::Start")
85+
86+
// only use the logfile for the init process, since we don't want to tee stdio of execs
87+
t := c.vsock
88+
if c.logPath != "" {
89+
// don't use [os.Create] since that truncates an existing file, which is not desired
90+
if c.logFile, err = os.OpenFile(c.logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666); err != nil {
91+
return -1, fmt.Errorf("failed to open log file: %s: %w", c.logPath, err)
92+
}
93+
go func() {
94+
// initProcess is already created in [(*Host).CreateContainer], and is, therefore, "waitable"
95+
// wait in `writersWg`, which is closed after process is cleaned up (including io Relays)
96+
//
97+
// Note: [PipeRelay] and [TtyRelay] are not safe to call multiple times, so it is safer to wait
98+
// on the parent (init) process.
99+
c.initProcess.writersWg.Wait()
100+
101+
if lfErr := c.logFile.Close(); lfErr != nil {
102+
entity.WithFields(logrus.Fields{
103+
logrus.ErrorKey: lfErr,
104+
logfields.Path: c.logFile.Name(),
105+
}).Warn("failed to close log file")
106+
}
107+
c.logFile = nil
108+
}()
109+
110+
t = transport.NewMultiWriter(c.vsock, c.logFile)
111+
}
112+
113+
stdioSet, err := stdio.Connect(t, conSettings)
82114
if err != nil {
83115
return -1, err
84116
}
117+
85118
if c.initProcess.spec.Terminal {
86119
ttyr := c.container.Tty()
87120
ttyr.ReplaceConnectionSet(stdioSet)

internal/guest/runtime/hcsv2/uvm.go

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

2323
"github.com/Microsoft/cosesign1go/pkg/cosesign1"
2424
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
25-
"github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr"
2625
cgroups "github.com/containerd/cgroups/v3/cgroup1"
2726
cgroup1stats "github.com/containerd/cgroups/v3/cgroup1/stats"
2827
"github.com/mattn/go-shellwords"
@@ -31,6 +30,7 @@ import (
3130
"github.com/sirupsen/logrus"
3231
"golang.org/x/sys/unix"
3332

33+
"github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr"
3434
"github.com/Microsoft/hcsshim/internal/debug"
3535
"github.com/Microsoft/hcsshim/internal/guest/policy"
3636
"github.com/Microsoft/hcsshim/internal/guest/prot"
@@ -45,6 +45,7 @@ import (
4545
"github.com/Microsoft/hcsshim/internal/guest/storage/scsi"
4646
"github.com/Microsoft/hcsshim/internal/guest/transport"
4747
"github.com/Microsoft/hcsshim/internal/log"
48+
"github.com/Microsoft/hcsshim/internal/logfields"
4849
"github.com/Microsoft/hcsshim/internal/oci"
4950
"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
5051
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
@@ -355,6 +356,24 @@ func setupSandboxHugePageMountsPath(id string) error {
355356
return storage.MountRShared(mountPath)
356357
}
357358

359+
// setupSandboxLogDir creates the directory to house all redirected stdio logs from containers.
360+
//
361+
// Virtual pod aware.
362+
func setupSandboxLogDir(sandboxID, virtualSandboxID string) error {
363+
mountPath := specGuest.SandboxLogsDir(sandboxID, virtualSandboxID)
364+
if err := mkdirAllModePerm(mountPath); err != nil {
365+
id := sandboxID
366+
if virtualSandboxID != "" {
367+
id = virtualSandboxID
368+
}
369+
return errors.Wrapf(err, "failed to create sandbox logs dir in sandbox %v", id)
370+
}
371+
return nil
372+
}
373+
374+
// TODO: unify workload and standalone logic for non-sandbox features (e.g., block devices, huge pages, uVM mounts)
375+
// TODO(go1.24): use [os.Root] instead of `!strings.HasPrefix(<path>, <root>)`
376+
358377
func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VMHostedContainerSettingsV2) (_ *Container, err error) {
359378
criType, isCRI := settings.OCISpecification.Annotations[annotations.KubernetesContainerType]
360379

@@ -493,6 +512,9 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
493512
return nil, err
494513
}
495514
}
515+
if err = setupSandboxLogDir(id, virtualPodID); err != nil {
516+
return nil, err
517+
}
496518

497519
if err := policy.ExtendPolicyWithNetworkingMounts(id, h.securityPolicyEnforcer, settings.OCISpecification); err != nil {
498520
return nil, err
@@ -544,6 +566,17 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
544566
}
545567
}
546568

569+
// don't specialize tee logs (both files and mounts) just for workload containers
570+
// add log directory mount before enforcing (mount) policy
571+
if logDirMount := settings.OCISpecification.Annotations[annotations.LCOWTeeLogDirMount]; logDirMount != "" {
572+
settings.OCISpecification.Mounts = append(settings.OCISpecification.Mounts, specs.Mount{
573+
Destination: logDirMount,
574+
Type: "bind",
575+
Source: specGuest.SandboxLogsDir(sandboxID, virtualPodID),
576+
Options: []string{"bind"},
577+
})
578+
}
579+
547580
user, groups, umask, err := h.securityPolicyEnforcer.GetUserInfo(settings.OCISpecification.Process, settings.OCISpecification.Root.Path)
548581
if err != nil {
549582
return nil, err
@@ -580,6 +613,30 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
580613
c.vsock = h.devNullTransport
581614
}
582615

616+
// delay creating the directory to house the container's stdio until after we've verified
617+
// policy on log settings.
618+
// TODO: is using allowStdio appropriate here, since longs aren't leaving the uVM?
619+
if logPath := settings.OCISpecification.Annotations[annotations.LCOWTeeLogPath]; logPath != "" {
620+
if !allowStdio {
621+
return nil, errors.Errorf("teeing container stdio to log path %q denied due to policy not allowing stdio access", logPath)
622+
}
623+
624+
c.logPath = specGuest.SandboxLogPath(sandboxID, virtualPodID, logPath)
625+
// verify the logpath is still under the correct directory
626+
if !strings.HasPrefix(c.logPath, specGuest.SandboxLogsDir(sandboxID, virtualPodID)) {
627+
return nil, errors.Errorf("log path %v is not within sandbox's log dir", c.logPath)
628+
}
629+
630+
dir := filepath.Dir(c.logPath)
631+
log.G(ctx).WithFields(logrus.Fields{
632+
logfields.Path: dir,
633+
logfields.ContainerID: id,
634+
}).Debug("creating container log file parent directory in uVM")
635+
if err := mkdirAllModePerm(dir); err != nil {
636+
return nil, errors.Wrapf(err, "failed to create log file parent directory: %s", dir)
637+
}
638+
}
639+
583640
if envToKeep != nil {
584641
settings.OCISpecification.Process.Env = []string(envToKeep)
585642
}

internal/guest/spec/spec.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func SandboxRootDir(sandboxID string) string {
8080
}
8181

8282
// VirtualPodRootDir returns the virtual pod root directory inside UVM/host.
83-
// This is used when multiple pods share a UVM via virtualSandboxID
83+
// This is used when multiple pods share a UVM via virtualSandboxID.
8484
func VirtualPodRootDir(virtualSandboxID string) string {
8585
// Ensure virtualSandboxID is a relative path to prevent directory traversal
8686
sanitizedID := filepath.Clean(virtualSandboxID)
@@ -91,7 +91,7 @@ func VirtualPodRootDir(virtualSandboxID string) string {
9191
}
9292

9393
// VirtualPodAwareSandboxRootDir returns the appropriate root directory based on whether
94-
// the sandbox is part of a virtual pod or traditional single-pod setup
94+
// the sandbox is part of a virtual pod or traditional single-pod setup.
9595
func VirtualPodAwareSandboxRootDir(sandboxID, virtualSandboxID string) string {
9696
if virtualSandboxID != "" {
9797
return VirtualPodRootDir(virtualSandboxID)
@@ -109,7 +109,7 @@ func VirtualPodMountsDir(virtualSandboxID string) string {
109109
return filepath.Join(VirtualPodRootDir(virtualSandboxID), "sandboxMounts")
110110
}
111111

112-
// VirtualPodAwareSandboxMountsDir returns the appropriate mounts directory
112+
// VirtualPodAwareSandboxMountsDir returns the appropriate mounts directory.
113113
func VirtualPodAwareSandboxMountsDir(sandboxID, virtualSandboxID string) string {
114114
if virtualSandboxID != "" {
115115
return VirtualPodMountsDir(virtualSandboxID)
@@ -127,7 +127,7 @@ func VirtualPodTmpfsMountsDir(virtualSandboxID string) string {
127127
return filepath.Join(VirtualPodRootDir(virtualSandboxID), "sandboxTmpfsMounts")
128128
}
129129

130-
// VirtualPodAwareSandboxTmpfsMountsDir returns the appropriate tmpfs mounts directory
130+
// VirtualPodAwareSandboxTmpfsMountsDir returns the appropriate tmpfs mounts directory.
131131
func VirtualPodAwareSandboxTmpfsMountsDir(sandboxID, virtualSandboxID string) string {
132132
if virtualSandboxID != "" {
133133
return VirtualPodTmpfsMountsDir(virtualSandboxID)
@@ -140,27 +140,27 @@ func HugePagesMountsDir(sandboxID string) string {
140140
return filepath.Join(SandboxRootDir(sandboxID), "hugepages")
141141
}
142142

143-
// VirtualPodHugePagesMountsDir returns virtual pod hugepages mounts directory
143+
// VirtualPodHugePagesMountsDir returns virtual pod hugepages mounts directory.
144144
func VirtualPodHugePagesMountsDir(virtualSandboxID string) string {
145145
return filepath.Join(VirtualPodRootDir(virtualSandboxID), "hugepages")
146146
}
147147

148-
// VirtualPodAwareHugePagesMountsDir returns the appropriate hugepages directory
148+
// VirtualPodAwareHugePagesMountsDir returns the appropriate hugepages directory.
149149
func VirtualPodAwareHugePagesMountsDir(sandboxID, virtualSandboxID string) string {
150150
if virtualSandboxID != "" {
151151
return VirtualPodHugePagesMountsDir(virtualSandboxID)
152152
}
153153
return HugePagesMountsDir(sandboxID)
154154
}
155155

156-
// SandboxMountSource returns sandbox mount path inside UVM
156+
// SandboxMountSource returns sandbox mount path inside UVM.
157157
func SandboxMountSource(sandboxID, path string) string {
158158
mountsDir := SandboxMountsDir(sandboxID)
159159
subPath := strings.TrimPrefix(path, guestpath.SandboxMountPrefix)
160160
return filepath.Join(mountsDir, subPath)
161161
}
162162

163-
// VirtualPodAwareSandboxMountSource returns mount source path for virtual pod aware containers
163+
// VirtualPodAwareSandboxMountSource returns mount source path for virtual pod aware containers.
164164
func VirtualPodAwareSandboxMountSource(sandboxID, virtualSandboxID, path string) string {
165165
if virtualSandboxID != "" {
166166
mountsDir := VirtualPodMountsDir(virtualSandboxID)
@@ -170,14 +170,14 @@ func VirtualPodAwareSandboxMountSource(sandboxID, virtualSandboxID, path string)
170170
return SandboxMountSource(sandboxID, path)
171171
}
172172

173-
// SandboxTmpfsMountSource returns sandbox tmpfs mount path inside UVM
173+
// SandboxTmpfsMountSource returns sandbox tmpfs mount path inside UVM.
174174
func SandboxTmpfsMountSource(sandboxID, path string) string {
175175
tmpfsMountDir := SandboxTmpfsMountsDir(sandboxID)
176176
subPath := strings.TrimPrefix(path, guestpath.SandboxTmpfsMountPrefix)
177177
return filepath.Join(tmpfsMountDir, subPath)
178178
}
179179

180-
// VirtualPodAwareSandboxTmpfsMountSource returns tmpfs mount source path for virtual pod aware containers
180+
// VirtualPodAwareSandboxTmpfsMountSource returns tmpfs mount source path for virtual pod aware containers.
181181
func VirtualPodAwareSandboxTmpfsMountSource(sandboxID, virtualSandboxID, path string) string {
182182
if virtualSandboxID != "" {
183183
mountsDir := VirtualPodTmpfsMountsDir(virtualSandboxID)
@@ -187,14 +187,14 @@ func VirtualPodAwareSandboxTmpfsMountSource(sandboxID, virtualSandboxID, path st
187187
return SandboxTmpfsMountSource(sandboxID, path)
188188
}
189189

190-
// HugePagesMountSource returns hugepages mount path inside UVM
190+
// HugePagesMountSource returns hugepages mount path inside UVM.
191191
func HugePagesMountSource(sandboxID, path string) string {
192192
mountsDir := HugePagesMountsDir(sandboxID)
193193
subPath := strings.TrimPrefix(path, guestpath.HugePagesMountPrefix)
194194
return filepath.Join(mountsDir, subPath)
195195
}
196196

197-
// VirtualPodAwareHugePagesMountSource returns hugepages mount source for virtual pod aware containers
197+
// VirtualPodAwareHugePagesMountSource returns hugepages mount source for virtual pod aware containers.
198198
func VirtualPodAwareHugePagesMountSource(sandboxID, virtualSandboxID, path string) string {
199199
if virtualSandboxID != "" {
200200
mountsDir := VirtualPodHugePagesMountsDir(virtualSandboxID)
@@ -204,6 +204,20 @@ func VirtualPodAwareHugePagesMountSource(sandboxID, virtualSandboxID, path strin
204204
return HugePagesMountSource(sandboxID, path)
205205
}
206206

207+
// SandboxLogsDir returns the logs directory inside the UVM for forwarding container stdio to.
208+
//
209+
// Virtual pod aware.
210+
func SandboxLogsDir(sandboxID, virtualSandboxID string) string {
211+
return filepath.Join(VirtualPodAwareSandboxRootDir(sandboxID, virtualSandboxID), "logs")
212+
}
213+
214+
// SandboxLogPath returns the log path inside the UVM.
215+
//
216+
// Virtual pod aware.
217+
func SandboxLogPath(sandboxID, virtualSandboxID, path string) string {
218+
return filepath.Join(SandboxLogsDir(sandboxID, virtualSandboxID), path)
219+
}
220+
207221
// GetNetworkNamespaceID returns the `ToLower` of
208222
// `spec.Windows.Network.NetworkNamespace` or `""`.
209223
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
}

0 commit comments

Comments
 (0)