diff --git a/daemon/containerio/cri_log_file.go b/daemon/containerio/cri_log_file.go new file mode 100644 index 000000000..5b4f8c3c6 --- /dev/null +++ b/daemon/containerio/cri_log_file.go @@ -0,0 +1,111 @@ +package containerio + +import ( + "bufio" + "bytes" + "io" + "os" + "time" + + "github.com/sirupsen/logrus" + "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" +) + +const ( + // delimiter used in cri logging format. + delimiter = ' ' + // eof is end-of-line. + eol = '\n' + // timestampFormat is the timestamp format used in cri logging format. + timestampFormat = time.RFC3339Nano + // pipeBufSize is the system PIPE_BUF size, on linux it is 4096 bytes. + pipeBufSize = 4096 + // bufSize is the size of the read buffer. + bufSize = pipeBufSize - len(timestampFormat) - len(Stdout) - 2 /*2 delimiter*/ - 1 /*eol*/ +) + +// StreamType is the type of the stream. +type StreamType string + +const ( + // Stdin stream type. + Stdin StreamType = "stdin" + // Stdout stream type. + Stdout StreamType = "stdout" + // Stderr stream type. + Stderr StreamType = "stderr" +) + +func init() { + Register(func() Backend { + return &criLogFile{} + }) +} + +type criLogFile struct { + file *os.File + pipeWriter *io.PipeWriter + pipeReader *io.PipeReader + closed bool +} + +func (c *criLogFile) Name() string { + return "cri-log-file" +} + +func (c *criLogFile) Init(opt *Option) error { + c.file = opt.criLogFile + c.pipeReader, c.pipeWriter = io.Pipe() + // TODO: redirect stderr. + go redirectLogs(c.file, c.pipeReader, Stdout) + return nil +} + +func redirectLogs(w io.WriteCloser, r io.ReadCloser, stream StreamType) { + defer r.Close() + defer w.Close() + streamBytes := []byte(stream) + delimiterBytes := []byte{delimiter} + partialBytes := []byte(runtime.LogTagPartial) + fullBytes := []byte(runtime.LogTagFull) + br := bufio.NewReaderSize(r, bufSize) + for { + lineBytes, isPrefix, err := br.ReadLine() + if err != nil { + if err == io.EOF { + logrus.Infof("finish redirecting log file") + } else { + logrus.Errorf("failed to redirect log file: %v", err) + } + return + } + tagBytes := fullBytes + if isPrefix { + tagBytes = partialBytes + } + timestampBytes := time.Now().AppendFormat(nil, time.RFC3339Nano) + data := bytes.Join([][]byte{timestampBytes, streamBytes, tagBytes, lineBytes}, delimiterBytes) + data = append(data, eol) + _, err = w.Write(data) + if err != nil { + logrus.Errorf("failed to write %q log to log file: %v", stream, err) + } + } +} + +func (c *criLogFile) Out() io.Writer { + return c.pipeWriter +} + +func (c *criLogFile) In() io.Reader { + // Log doesn't need stdin. + return nil +} + +func (c *criLogFile) Close() error { + if c.closed { + return nil + } + c.closed = true + return c.pipeWriter.Close() +} diff --git a/daemon/containerio/options.go b/daemon/containerio/options.go index 7cb3b8091..338f8a634 100644 --- a/daemon/containerio/options.go +++ b/daemon/containerio/options.go @@ -3,6 +3,7 @@ package containerio import ( "bytes" "net/http" + "os" "github.com/alibaba/pouch/cri/stream/remotecommand" ) @@ -17,6 +18,7 @@ type Option struct { stdinBackend string memBuffer *bytes.Buffer streams *remotecommand.Streams + criLogFile *os.File } // NewOption creates the Option instance. @@ -111,3 +113,14 @@ func WithStdinStream() func(*Option) { opt.stdinBackend = "streams" } } + +// WithCriLogFile specified the cri log file backend. +func WithCriLogFile(criLogFile *os.File) func(*Option) { + return func(opt *Option) { + if opt.backends == nil { + opt.backends = make(map[string]struct{}) + } + opt.backends["cri-log-file"] = struct{}{} + opt.criLogFile = criLogFile + } +} diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index 9533f063c..38f5cf872 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -886,6 +886,10 @@ func (mgr *ContainerManager) openAttachIO(id string, attach *AttachConfig) (*con options = append(options, containerio.WithStdinStream()) } } + + if attach.CriLogFile != nil { + options = append(options, containerio.WithCriLogFile(attach.CriLogFile)) + } } else { options = append(options, containerio.WithDiscard()) } @@ -932,6 +936,10 @@ func (mgr *ContainerManager) openIO(id string, attach *AttachConfig, exec bool) options = append(options, containerio.WithStdinStream()) } } + + if attach.CriLogFile != nil { + options = append(options, containerio.WithCriLogFile(attach.CriLogFile)) + } } else if !exec { options = append(options, containerio.WithRawFile()) diff --git a/daemon/mgr/container_types.go b/daemon/mgr/container_types.go index f04c736d8..8b01b9a2b 100644 --- a/daemon/mgr/container_types.go +++ b/daemon/mgr/container_types.go @@ -3,6 +3,7 @@ package mgr import ( "bytes" "net/http" + "os" "sync" "time" @@ -55,6 +56,9 @@ type AttachConfig struct { // Attach using streams. Streams *remotecommand.Streams + + // Attach to the container to get its log. + CriLogFile *os.File } // ContainerRemoveOption wraps the container remove interface params. diff --git a/daemon/mgr/cri.go b/daemon/mgr/cri.go index 899253872..fed2434f7 100644 --- a/daemon/mgr/cri.go +++ b/daemon/mgr/cri.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "time" apitypes "github.com/alibaba/pouch/apis/types" @@ -421,7 +422,28 @@ func (c *CriManager) CreateContainer(ctx context.Context, r *runtime.CreateConta return nil, fmt.Errorf("failed to create container for sandbox %q: %v", podSandboxID, err) } - return &runtime.CreateContainerResponse{ContainerId: createResp.ID}, nil + containerID := createResp.ID + + // Get container log. + if config.GetLogPath() != "" { + logPath := filepath.Join(sandboxConfig.GetLogDirectory(), config.GetLogPath()) + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640) + if err != nil { + return nil, fmt.Errorf("failed to create container for opening log file failed: %v", err) + } + // Attach to the container to get log. + attachConfig := &AttachConfig{ + Stdout: true, + Stderr: true, + CriLogFile: f, + } + err = c.ContainerMgr.Attach(context.Background(), containerID, attachConfig) + if err != nil { + return nil, fmt.Errorf("failed to attach to container %q to get its log: %v", containerID, err) + } + } + + return &runtime.CreateContainerResponse{ContainerId: containerID}, nil } // StartContainer starts the container. diff --git a/hack/cri-test/test-cri.sh b/hack/cri-test/test-cri.sh index b4cb89a01..47be8570f 100755 --- a/hack/cri-test/test-cri.sh +++ b/hack/cri-test/test-cri.sh @@ -23,7 +23,7 @@ POUCH_SOCK="/var/run/pouchcri.sock" # CRI_FOCUS focuses the test to run. # With the CRI manager completes its function, we may need to expand this field. -CRI_FOCUS=${CRI_FOCUS:-"PodSandbox|AppArmor|Privileged is true|basic operations on container|Runtime info|mount propagation|volume and device|RunAsUser|Networking|Streaming|NamespaceOption|SupplementalGroups"} +CRI_FOCUS=${CRI_FOCUS:-"PodSandbox|AppArmor|Privileged is true|Runtime info|Container|RunAsUser|Networking|Streaming|NamespaceOption|SupplementalGroups"} # CRI_SKIP skips the test to skip. CRI_SKIP=${CRI_SKIP:-"RunAsUserName|HostNetwork"}