diff options
-rw-r--r-- | .cirrus.yml | 5 | ||||
-rw-r--r-- | cmd/podman/common/specgen.go | 12 | ||||
-rwxr-xr-x | contrib/cirrus/logformatter | 23 | ||||
-rwxr-xr-x | contrib/cirrus/runner.sh | 14 | ||||
-rw-r--r-- | docs/source/markdown/podman-create.1.md | 18 | ||||
-rw-r--r-- | docs/source/markdown/podman-run.1.md | 16 | ||||
-rw-r--r-- | libpod/container_config.go | 2 | ||||
-rw-r--r-- | libpod/oci_conmon_linux.go | 13 | ||||
-rw-r--r-- | libpod/options.go | 28 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_attach.go | 33 | ||||
-rw-r--r-- | pkg/api/handlers/compat/exec.go | 31 | ||||
-rw-r--r-- | pkg/api/server/idle/tracker.go | 10 | ||||
-rw-r--r-- | pkg/api/server/server.go | 62 | ||||
-rw-r--r-- | pkg/specgen/generate/container_create.go | 3 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 3 | ||||
-rw-r--r-- | pkg/util/utils_linux.go | 9 | ||||
-rw-r--r-- | test/e2e/logs_test.go | 16 |
17 files changed, 185 insertions, 113 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 75f095fe0..b23ec1a90 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -87,6 +87,7 @@ ext_svc_check_task: automation_task: alias: 'automation' name: "Check Automation" + skip: &branch "$CIRRUS_PR == '' || $CIRRUS_TAG != ''" # Don't run for branches container: *smallcontainer env: TEST_FLAVOR: automation @@ -231,6 +232,7 @@ validate_task: bindings_task: name: "Test Bindings" alias: bindings + skip: *branch depends_on: - build gce_instance: *standardvm @@ -419,6 +421,7 @@ local_integration_test_task: &local_integration_test_task # Integration-test task name convention: # <int.|sys.> <podman|remote> <Distro NV> <root|rootless> name: &std_name_fmt "$TEST_FLAVOR $PODBIN_NAME $DISTRO_NV $PRIV_NAME $TEST_ENVIRON" + skip: *branch alias: local_integration_test depends_on: - unit_test @@ -456,6 +459,7 @@ remote_integration_test_task: container_integration_test_task: name: *std_name_fmt alias: container_integration_test + skip: *branch depends_on: - unit_test matrix: &fedora_vm_axis @@ -484,6 +488,7 @@ container_integration_test_task: rootless_integration_test_task: name: *std_name_fmt alias: rootless_integration_test + skip: *branch depends_on: - unit_test matrix: *fedora_vm_axis diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 84ae70b6a..f427830c6 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -575,11 +575,17 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string if len(split) < 2 { return errors.Errorf("invalid log option %q", o) } - switch { - case split[0] == "driver": + switch strings.ToLower(split[0]) { + case "driver": s.LogConfiguration.Driver = split[1] - case split[0] == "path": + case "path": s.LogConfiguration.Path = split[1] + case "max-size": + logSize, err := units.FromHumanSize(split[1]) + if err != nil { + return errors.Wrapf(err, "%s is not a valid option", o) + } + s.LogConfiguration.Size = logSize default: logOpts[split[0]] = split[1] } diff --git a/contrib/cirrus/logformatter b/contrib/cirrus/logformatter index 2ab8aa117..0cbd1f34b 100755 --- a/contrib/cirrus/logformatter +++ b/contrib/cirrus/logformatter @@ -517,25 +517,20 @@ END_SYNOPSIS } # eg "test fedora", "special_testing_rootless" + # WARNING: As of 2020-10-05, $CIRRUS_TASK_NAME reflects the same + # descriptive content as our $subtest_name argument (confirm via + # cross-checking runner.sh:logformatter() vs cirrus.yml:&std_name_fmt). + # If this ever becomes untrue, just add _tr("Subtest", $subtest_name). my $test_name = _env_replace("{CIRRUS_TASK_NAME}"); - if (my $rcli = $ENV{RCLI}) { - $test_name .= " [remote]" if $rcli eq 'true'; - } - else { - $test_name .= " [no RCLI; cannot determine remote/local]"; - } + # (Special-case cleanup: Cirrus\ quotes\ spaces; remove for readability). + $test_name =~ s/\\\s+/ /g; $s .= _tr("Test name", $test_name); - # Subtest, e.g. system_test - $s .= _tr("Subtest", $subtest_name); - # Link to further Cirrus results, e.g. other runs. # Build is mostly boring, it's usually TASK that we want to see. - $s .= _tr("Cirrus Build ID", "<small>" . _a("{CIRRUS_BUILD_ID}", "https://cirrus-ci.com/build/{CIRRUS_BUILD_ID}") . "</small>"); - $s .= _tr("Cirrus <b>Task</b> ID", _a("{CIRRUS_TASK_ID}", "https://cirrus-ci.com/task/{CIRRUS_TASK_ID}")); - - # "none", "rootless" - $s .= _tr("Special mode", _env_replace("{SPECIALMODE}")); + $s .= _tr("Cirrus", sprintf("<small>Build %s</small> / <b>Task %s</b>", + _a("{CIRRUS_BUILD_ID}", "https://cirrus-ci.com/build/{CIRRUS_BUILD_ID}"), + _a("{CIRRUS_TASK_ID}", "https://cirrus-ci.com/task/{CIRRUS_TASK_ID}"))); $s .= "</table>\n"; return $s; diff --git a/contrib/cirrus/runner.sh b/contrib/cirrus/runner.sh index 17bb05019..13fab447c 100755 --- a/contrib/cirrus/runner.sh +++ b/contrib/cirrus/runner.sh @@ -102,7 +102,7 @@ altbuild() { esac } -integration_outputfilter() { +logformatter() { # Use similar format as human-friendly task name from .cirrus.yml # shellcheck disable=SC2154 output_name="$TEST_FLAVOR-$PODBIN_NAME-$DISTRO_NV-$PRIV_NAME-$TEST_ENVIRON" @@ -134,10 +134,11 @@ dotest() { # does not return fi - output_filter="cat" # no filter - if [[ "$testsuite" == "integration" ]]; then - output_filter=integration_outputfilter - fi + # 'logformatter' script makes test logs readable; only works for some tests + case "$testsuite" in + integration|system) output_filter=logformatter ;; + *) output_filter="cat" ;; + esac # containers/automation sets this to 0 for it's dbg() function # but the e2e integration tests are also sensitive to it. @@ -210,7 +211,8 @@ case "$TEST_FLAVOR" in bindings) # shellcheck disable=SC2155 export PATH=$PATH:$GOSRC/hack - cd pkg/bindings/test && ginkgo -trace -noColor -debug -r + # Subshell needed for .cirrus.yml to find logformatter output in cwd + (cd pkg/bindings/test && ginkgo -trace -noColor -debug -r) |& logformatter ;; endpoint) make test-binaries diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 28c340d2f..3303ae572 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -438,16 +438,22 @@ Not implemented Logging driver for the container. Currently available options are *k8s-file*, *journald*, and *none*, with *json-file* aliased to *k8s-file* for scripting compatibility. -**--log-opt**=*path* +**--log-opt**=*name*=*value* -Logging driver specific options. Used to set the path to the container log file. For example: +Set custom logging configuration. The following *name*s are supported: -`--log-opt path=/var/log/container/mycontainer.json` +- **path**: specify a path to the log file +(e.g. **--log-opt path=/var/log/container/mycontainer.json**); -**--log-opt**=*tag* +- **max-size**: specify a max size of the log file +(e.g. **--log-opt max-size=10mb**); -Set custom logging configuration. Presently supports the `tag` option -which specified a custom log tag for the container. For example: +- **tag**: specify a custom log tag for the container +(e.g. **--log-opt tag="{{.ImageName}}"**. + +It supports the same keys as **podman inspect --format**. + +This option is currently supported only by the **journald** log driver. `--log-opt tag="{{.ImageName}}"` diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index a27b1b175..d9bcc49ae 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -452,12 +452,18 @@ Logging driver for the container. Currently available options are **k8s-file**, **--log-opt**=*name*=*value* +Logging driver specific options. + Set custom logging configuration. The following *name*s are supported: -- **path**: specify a path to the log file (e.g. -**--log-opt path=/var/log/container/mycontainer.json**); -- **tag**: specify a custom log tag for the container (e.g. -**--log-opt tag="{{.ImageName}}"**. It supports the same -keys as **podman inspect --format**. + +**path**: specify a path to the log file + (e.g. **--log-opt path=/var/log/container/mycontainer.json**); + +**max-size**: specify a max size of the log file + (e.g. **--log-opt max-size=10mb**); + +**tag**: specify a custom log tag for the container + (e.g. **--log-opt tag="{{.ImageName}}"**. This option is currently supported only by the **journald** log driver. diff --git a/libpod/container_config.go b/libpod/container_config.go index 3fc058d52..fc93140dd 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -287,6 +287,8 @@ type ContainerMiscConfig struct { LogPath string `json:"logPath"` // LogTag is the tag used for logging LogTag string `json:"logTag"` + // LogSize is the tag used for logging + LogSize int64 `json:"logSize"` // LogDriver driver for logs LogDriver string `json:"logDriver"` // File containing the conmon PID diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index b13d25e22..7fb374e0d 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -1350,10 +1350,6 @@ func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, p } args = append(args, "-l", logDriverArg) - if r.logSizeMax >= 0 { - args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax)) - } - logLevel := logrus.GetLevel() args = append(args, "--log-level", logLevel.String()) @@ -1361,6 +1357,15 @@ func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, p logrus.Debugf("%s messages will be logged to syslog", r.conmonPath) args = append(args, "--syslog") } + + size := r.logSizeMax + if ctr.config.LogSize > 0 { + size = ctr.config.LogSize + } + if size > 0 { + args = append(args, "--log-size-max", fmt.Sprintf("%v", size)) + } + if ociLogPath != "" { args = append(args, "--runtime-arg", "--log-format=json", "--runtime-arg", "--log", fmt.Sprintf("--runtime-arg=%s", ociLogPath)) } diff --git a/libpod/options.go b/libpod/options.go index f7190d0e3..1ffb78da9 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -328,20 +328,6 @@ func WithNoStore() RuntimeOption { } } -// WithMaxLogSize sets the maximum size of container logs. -// Positive sizes are limits in bytes, -1 is unlimited. -func WithMaxLogSize(limit int64) RuntimeOption { - return func(rt *Runtime) error { - if rt.valid { - return define.ErrRuntimeFinalized - } - - rt.config.Containers.LogSizeMax = limit - - return nil - } -} - // WithNoPivotRoot sets the runtime to use MS_MOVE instead of PIVOT_ROOT when // starting containers. func WithNoPivotRoot() RuntimeOption { @@ -543,6 +529,20 @@ func WithRuntimeFlags(runtimeFlags []string) RuntimeOption { // Container Creation Options +// WithMaxLogSize sets the maximum size of container logs. +// Positive sizes are limits in bytes, -1 is unlimited. +func WithMaxLogSize(limit int64) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrRuntimeFinalized + } + + ctr.config.LogSize = limit + + return nil + } +} + // WithShmDir sets the directory that should be mounted on /dev/shm. func WithShmDir(dir string) CtrCreateOption { return func(ctr *Container) error { diff --git a/pkg/api/handlers/compat/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index 4a1196c89..a4013469b 100644 --- a/pkg/api/handlers/compat/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -92,30 +92,29 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - idleTracker := r.Context().Value("idletracker").(*idle.Tracker) - hijackChan := make(chan bool, 1) + logErr := func(e error) { + logrus.Error(errors.Wrapf(e, "error attaching to container %s", ctr.ID())) + } // Perform HTTP attach. // HTTPAttach will handle everything about the connection from here on // (including closing it and writing errors to it). - if err := ctr.HTTPAttach(r, w, streams, detachKeys, nil, query.Stream, query.Logs, hijackChan); err != nil { - hijackComplete := <-hijackChan + hijackChan := make(chan bool, 1) + err = ctr.HTTPAttach(r, w, streams, detachKeys, nil, query.Stream, query.Logs, hijackChan) - // We can't really do anything about errors anymore. HTTPAttach - // should be writing them to the connection. - logrus.Errorf("Error attaching to container %s: %v", ctr.ID(), err) + if <-hijackChan { + // If connection was Hijacked, we have to signal it's being closed + t := r.Context().Value("idletracker").(*idle.Tracker) + defer t.Close() - if hijackComplete { - // We do need to tell the idle tracker that the - // connection has been closed, though. We can guarantee - // that is true after HTTPAttach exits. - idleTracker.Close() - } else { - // A hijack was not successfully completed. We need to - // report the error normally. - utils.InternalServerError(w, err) + if err != nil { + // Cannot report error to client as a 500 as the Upgrade set status to 101 + logErr(err) } + } else { + // If the Hijack failed we are going to assume we can still inform client of failure + utils.InternalServerError(w, err) + logErr(err) } - logrus.Debugf("Attach for container %s completed successfully", ctr.ID()) } diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go index df51293c2..cc5db8efe 100644 --- a/pkg/api/handlers/compat/exec.go +++ b/pkg/api/handlers/compat/exec.go @@ -174,25 +174,26 @@ func ExecStartHandler(w http.ResponseWriter, r *http.Request) { return } - idleTracker := r.Context().Value("idletracker").(*idle.Tracker) - hijackChan := make(chan bool, 1) + logErr := func(e error) { + logrus.Error(errors.Wrapf(e, "error attaching to container %s exec session %s", sessionCtr.ID(), sessionID)) + } - if err := sessionCtr.ExecHTTPStartAndAttach(sessionID, r, w, nil, nil, nil, hijackChan); err != nil { - hijackComplete := <-hijackChan + hijackChan := make(chan bool, 1) + err = sessionCtr.ExecHTTPStartAndAttach(sessionID, r, w, nil, nil, nil, hijackChan) - logrus.Errorf("Error attaching to container %s exec session %s: %v", sessionCtr.ID(), sessionID, err) + if <-hijackChan { + // If connection was Hijacked, we have to signal it's being closed + t := r.Context().Value("idletracker").(*idle.Tracker) + defer t.Close() - if hijackComplete { - // We do need to tell the idle tracker that the - // connection has been closed, though. We can guarantee - // that is true after HTTPAttach exits. - idleTracker.Close() - } else { - // A hijack was not successfully completed. We need to - // report the error normally. - utils.InternalServerError(w, err) + if err != nil { + // Cannot report error to client as a 500 as the Upgrade set status to 101 + logErr(err) } + } else { + // If the Hijack failed we are going to assume we can still inform client of failure + utils.InternalServerError(w, err) + logErr(err) } - logrus.Debugf("Attach for container %s exec session %s completed successfully", sessionCtr.ID(), sessionID) } diff --git a/pkg/api/server/idle/tracker.go b/pkg/api/server/idle/tracker.go index 1b378c492..50e41b7bf 100644 --- a/pkg/api/server/idle/tracker.go +++ b/pkg/api/server/idle/tracker.go @@ -61,10 +61,14 @@ func (t *Tracker) ConnState(conn net.Conn, state http.ConnState) { oldActive := t.ActiveConnections() // Either the server or a hijacking handler has closed the http connection to a client - if _, found := t.managed[conn]; found { - delete(t.managed, conn) - } else { + if conn == nil { t.hijacked-- // guarded by t.mux above + } else { + if _, found := t.managed[conn]; found { + delete(t.managed, conn) + } else { + logrus.Warnf("IdleTracker %p: StateClosed transition by un-managed connection", conn) + } } // Transitioned from any "active" connection to no connections diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 09a9f6370..355a46fb7 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -8,9 +8,9 @@ import ( "net/http" "os" "os/signal" - "runtime" goRuntime "runtime" "strings" + "sync" "syscall" "time" @@ -42,13 +42,19 @@ const ( UnlimitedServiceDuration = 0 * time.Second ) +// shutdownOnce ensures Shutdown() may safely be called from several go routines +var shutdownOnce sync.Once + // NewServer will create and configure a new API server with all defaults func NewServer(runtime *libpod.Runtime) (*APIServer, error) { return newServer(runtime, DefaultServiceDuration, nil) } // NewServerWithSettings will create and configure a new API server using provided settings -func NewServerWithSettings(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) (*APIServer, error) { +func NewServerWithSettings(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) ( + *APIServer, + error, +) { return newServer(runtime, duration, listener) } @@ -180,7 +186,7 @@ func (s *APIServer) Serve() error { go func() { <-s.idleTracker.Done() - logrus.Debugf("API Server idle for %v", s.idleTracker.Duration) + logrus.Debugf("API Server idle for %s", s.idleTracker.Duration.Round(time.Second).String()) _ = s.Shutdown() }() @@ -224,31 +230,43 @@ func (s *APIServer) Serve() error { // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { if s.idleTracker.Duration == UnlimitedServiceDuration { - logrus.Debug("APIServer.Shutdown ignored as Duration is UnlimitedService.") + logrus.Debug("APIServer.Shutdown ignored as Duration is UnlimitedService") return nil } - // Gracefully shutdown server(s), duration of wait same as idle window - // TODO: Should we really wait the idle window for shutdown? - ctx, cancel := context.WithTimeout(context.Background(), s.idleTracker.Duration) - defer cancel() + shutdownOnce.Do(func() { + if logrus.IsLevelEnabled(logrus.DebugLevel) { + _, file, line, _ := goRuntime.Caller(1) + logrus.Debugf("APIServer.Shutdown by %s:%d, %d/%d connection(s)", + file, line, s.idleTracker.ActiveConnections(), s.idleTracker.TotalConnections()) - if logrus.IsLevelEnabled(logrus.DebugLevel) { - _, file, line, _ := runtime.Caller(1) - logrus.Debugf("APIServer.Shutdown by %s:%d, %d/%d connection(s)", - file, line, s.idleTracker.ActiveConnections(), s.idleTracker.TotalConnections()) - if err := s.pprof.Shutdown(ctx); err != nil { - logrus.Warn("Failed to cleanly shutdown pprof Server: " + err.Error()) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), s.idleTracker.Duration) + go func() { + defer cancel() + if err := s.pprof.Shutdown(ctx); err != nil { + logrus.Warn( + errors.Wrapf(err, "failed to cleanly shutdown pprof Server")) + } + }() + <-ctx.Done() + }() } - } - go func() { - err := s.Server.Shutdown(ctx) - if err != nil && err != context.Canceled && err != http.ErrServerClosed { - logrus.Error("Failed to cleanly shutdown APIServer: " + err.Error()) - } - }() - <-ctx.Done() + // Gracefully shutdown server(s), duration of wait same as idle window + ctx, cancel := context.WithTimeout(context.Background(), s.idleTracker.Duration) + go func() { + defer cancel() + + err := s.Server.Shutdown(ctx) + if err != nil && err != context.Canceled && err != http.ErrServerClosed { + logrus.Error( + errors.Wrapf(err, "failed to cleanly shutdown APIServer")) + } + }() + <-ctx.Done() + }) + return nil } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 147450703..105e36bc6 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -260,6 +260,9 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.LogConfiguration.Path) > 0 { options = append(options, libpod.WithLogPath(s.LogConfiguration.Path)) } + if s.LogConfiguration.Size > 0 { + options = append(options, libpod.WithMaxLogSize(s.LogConfiguration.Size)) + } if len(s.LogConfiguration.Options) > 0 && s.LogConfiguration.Options["tag"] != "" { // Note: I'm really guessing here. options = append(options, libpod.WithLogTag(s.LogConfiguration.Options["tag"])) diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index b8f37ec7a..fa4af7b2b 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -19,6 +19,9 @@ type LogConfig struct { // Only available if LogDriver is set to "json-file" or "k8s-file". // Optional. Path string `json:"path,omitempty"` + // Size is the maximimup size of the log file + // Optional. + Size int64 `json:"size,omitempty"` // A set of options to accompany the log driver. // Optional. Options map[string]string `json:"options,omitempty"` diff --git a/pkg/util/utils_linux.go b/pkg/util/utils_linux.go index 29b16f765..e4957f442 100644 --- a/pkg/util/utils_linux.go +++ b/pkg/util/utils_linux.go @@ -60,11 +60,12 @@ func CheckRootlessUIDRange(uid int) error { if err != nil { return err } + total := 0 for _, u := range uids { - // add 1 since we also map in the user's own UID - if uid > u.Size+1 { - return errors.Errorf("requested user's UID %d is too large for the rootless user namespace", uid) - } + total += u.Size + } + if uid > total { + return errors.Errorf("requested user's UID %d is too large for the rootless user namespace", uid) } return nil } diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go index 67ab71d20..9b3163856 100644 --- a/test/e2e/logs_test.go +++ b/test/e2e/logs_test.go @@ -321,4 +321,20 @@ var _ = Describe("Podman logs", func() { results.WaitWithDefaultTimeout() Expect(results).To(Exit(0)) }) + + It("using container with container log-size", func() { + logc := podmanTest.Podman([]string{"run", "--log-opt=max-size=10k", "-d", ALPINE, "sh", "-c", "echo podman podman podman"}) + logc.WaitWithDefaultTimeout() + Expect(logc).To(Exit(0)) + cid := logc.OutputToString() + + wait := podmanTest.Podman([]string{"wait", cid}) + wait.WaitWithDefaultTimeout() + Expect(wait).To(Exit(0)) + + results := podmanTest.Podman([]string{"logs", cid}) + results.WaitWithDefaultTimeout() + Expect(results).To(Exit(0)) + Expect(results.OutputToString()).To(Equal("podman podman podman")) + }) }) |