diff --git a/pkg/cri/server/container_execsync.go b/pkg/cri/server/container_execsync.go index 3bf8d85c0..1cb344435 100644 --- a/pkg/cri/server/container_execsync.go +++ b/pkg/cri/server/container_execsync.go @@ -38,14 +38,55 @@ import ( cioutil "github.com/containerd/containerd/pkg/ioutil" ) +type cappedWriter struct { + w io.WriteCloser + remain int +} + +func (cw *cappedWriter) Write(p []byte) (int, error) { + if cw.remain <= 0 { + return len(p), nil + } + + end := cw.remain + if end > len(p) { + end = len(p) + } + written, err := cw.w.Write(p[0:end]) + cw.remain -= written + + if err != nil { + return written, err + } + return len(p), nil +} + +func (cw *cappedWriter) Close() error { + return cw.w.Close() +} + +func (cw *cappedWriter) isFull() bool { + return cw.remain <= 0 +} + // ExecSync executes a command in the container, and returns the stdout output. // If command exits with a non-zero exit code, an error is returned. func (c *criService) ExecSync(ctx context.Context, r *runtime.ExecSyncRequest) (*runtime.ExecSyncResponse, error) { + const maxStreamSize = 1024 * 1024 * 16 + var stdout, stderr bytes.Buffer + + // cappedWriter truncates the output. In that case, the size of + // the ExecSyncResponse will hit the CRI plugin's gRPC response limit. + // Thus the callers outside of the containerd process (e.g. Kubelet) never see + // the truncated output. + cout := &cappedWriter{w: cioutil.NewNopWriteCloser(&stdout), remain: maxStreamSize} + cerr := &cappedWriter{w: cioutil.NewNopWriteCloser(&stderr), remain: maxStreamSize} + exitCode, err := c.execInContainer(ctx, r.GetContainerId(), execOptions{ cmd: r.GetCmd(), - stdout: cioutil.NewNopWriteCloser(&stdout), - stderr: cioutil.NewNopWriteCloser(&stderr), + stdout: cout, + stderr: cerr, timeout: time.Duration(r.GetTimeout()) * time.Second, }) if err != nil { diff --git a/pkg/cri/server/container_execsync_test.go b/pkg/cri/server/container_execsync_test.go new file mode 100644 index 000000000..989909649 --- /dev/null +++ b/pkg/cri/server/container_execsync_test.go @@ -0,0 +1,52 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package server + +import ( + "bytes" + "testing" + + cioutil "github.com/containerd/containerd/pkg/ioutil" + "github.com/stretchr/testify/assert" +) + +func TestCWWrite(t *testing.T) { + var buf bytes.Buffer + cw := &cappedWriter{w: cioutil.NewNopWriteCloser(&buf), remain: 10} + + n, err := cw.Write([]byte("hello")) + assert.NoError(t, err) + assert.Equal(t, 5, n) + + n, err = cw.Write([]byte("helloworld")) + assert.NoError(t, err, "no errors even it hits the cap") + assert.Equal(t, 10, n, "no indication of partial write") + assert.True(t, cw.isFull()) + assert.Equal(t, []byte("hellohello"), buf.Bytes(), "the underlying writer is capped") + + _, err = cw.Write([]byte("world")) + assert.NoError(t, err) + assert.True(t, cw.isFull()) + assert.Equal(t, []byte("hellohello"), buf.Bytes(), "the underlying writer is capped") +} + +func TestCWClose(t *testing.T) { + var buf bytes.Buffer + cw := &cappedWriter{w: cioutil.NewNopWriteCloser(&buf), remain: 5} + err := cw.Close() + assert.NoError(t, err) +}