diff options
-rw-r--r-- | cmd/podman/machine/init.go | 19 | ||||
-rw-r--r-- | cmd/podman/machine/list.go | 2 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | libpod/boltdb_state.go | 248 | ||||
-rw-r--r-- | libpod/boltdb_state_internal.go | 22 | ||||
-rw-r--r-- | libpod/container_api.go | 97 | ||||
-rw-r--r-- | libpod/container_internal.go | 52 | ||||
-rw-r--r-- | libpod/define/errors.go | 4 | ||||
-rw-r--r-- | libpod/events.go | 3 | ||||
-rw-r--r-- | libpod/oci_conmon_linux.go | 28 | ||||
-rw-r--r-- | libpod/state.go | 9 | ||||
-rw-r--r-- | pkg/api/handlers/utils/containers.go | 1 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 22 | ||||
-rw-r--r-- | pkg/machine/e2e/list_test.go | 23 | ||||
-rw-r--r-- | pkg/specgen/volumes.go | 16 | ||||
-rw-r--r-- | test/e2e/image_scp_test.go | 23 | ||||
-rw-r--r-- | test/e2e/run_volume_test.go | 28 | ||||
-rw-r--r-- | test/e2e/system_connection_test.go | 22 | ||||
-rw-r--r-- | test/system/030-run.bats | 12 | ||||
-rw-r--r-- | test/system/150-login.bats | 2 | ||||
-rw-r--r-- | test/system/500-networking.bats | 8 | ||||
-rw-r--r-- | test/system/helpers.bash | 31 | ||||
-rw-r--r-- | vendor/github.com/stretchr/testify/assert/assertion_compare.go | 24 | ||||
-rw-r--r-- | vendor/modules.txt | 2 |
25 files changed, 537 insertions, 167 deletions
diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 612c36057..9d464ad37 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -109,7 +109,7 @@ func init() { flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution") } -func initMachine(_ *cobra.Command, args []string) error { +func initMachine(cmd *cobra.Command, args []string) error { var ( err error vm machine.VM @@ -147,17 +147,12 @@ func initMachine(_ *cobra.Command, args []string) error { fmt.Println("Machine init complete") if now { - err = vm.Start(initOpts.Name, machine.StartOptions{}) - if err == nil { - fmt.Printf("Machine %q started successfully\n", initOpts.Name) - newMachineEvent(events.Start, events.Event{Name: initOpts.Name}) - } - } else { - extra := "" - if initOpts.Name != defaultMachineName { - extra = " " + initOpts.Name - } - fmt.Printf("To start your machine run:\n\n\tpodman machine start%s\n\n", extra) + return start(cmd, args) + } + extra := "" + if initOpts.Name != defaultMachineName { + extra = " " + initOpts.Name } + fmt.Printf("To start your machine run:\n\n\tpodman machine start%s\n\n", extra) return err } diff --git a/cmd/podman/machine/list.go b/cmd/podman/machine/list.go index bb14d4a67..1ffb8690c 100644 --- a/cmd/podman/machine/list.go +++ b/cmd/podman/machine/list.go @@ -138,7 +138,7 @@ func outputTemplate(cmd *cobra.Command, responses []*ListReporter) error { switch { case cmd.Flags().Changed("format"): row = cmd.Flag("format").Value.String() - listFlag.noHeading = !report.HasTable(row) + printHeader = report.HasTable(row) row = report.NormalizeFormat(row) default: row = cmd.Flag("format").Value.String() @@ -57,7 +57,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.4 + github.com/stretchr/testify v1.7.5 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/ulikunitz/xz v0.5.10 @@ -1268,8 +1268,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/sylabs/sif/v2 v2.7.0/go.mod h1:TiyBWsgWeh5yBeQFNuQnvROwswqK7YJT8JA1L53bsXQ= github.com/sylabs/sif/v2 v2.7.1 h1:XXt9AP39sQfsMCGOGQ/XP9H47yqZOvAonalkaCaNIYM= diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go index c3db6152a..471f64b84 100644 --- a/libpod/boltdb_state.go +++ b/libpod/boltdb_state.go @@ -5,8 +5,10 @@ import ( "fmt" "net" "os" + "strconv" "strings" "sync" + "time" "github.com/containers/common/libnetwork/types" "github.com/containers/podman/v4/libpod/define" @@ -63,6 +65,13 @@ type BoltState struct { // initially created the database. This must match for any further instances // that access the database, to ensure that state mismatches with // containers/storage do not occur. +// - exitCodeBucket/exitCodeTimeStampBucket: (#14559) exit codes must be part +// of the database to resolve a previous race condition when one process waits +// for the exit file to be written and another process removes it along with +// the container during auto-removal. The same race would happen trying to +// read the exit code from the containers bucket. Hence, exit codes go into +// their own bucket. To avoid the rather expensive JSON (un)marshaling, we +// have two buckets: one for the exit codes, the other for the timestamps. // NewBoltState creates a new bolt-backed state database func NewBoltState(path string, runtime *Runtime) (State, error) { @@ -98,6 +107,8 @@ func NewBoltState(path string, runtime *Runtime) (State, error) { allVolsBkt, execBkt, runtimeConfigBkt, + exitCodeBkt, + exitCodeTimeStampBkt, } // Does the DB need an update? @@ -192,6 +203,45 @@ func (s *BoltState) Refresh() error { return err } + exitCodeBucket, err := getExitCodeBucket(tx) + if err != nil { + return err + } + + timeStampBucket, err := getExitCodeTimeStampBucket(tx) + if err != nil { + return err + } + + // Clear all exec exit codes + toRemoveExitCodes := []string{} + err = exitCodeBucket.ForEach(func(id, _ []byte) error { + toRemoveExitCodes = append(toRemoveExitCodes, string(id)) + return nil + }) + if err != nil { + return errors.Wrapf(err, "error reading exit codes bucket") + } + for _, id := range toRemoveExitCodes { + if err := exitCodeBucket.Delete([]byte(id)); err != nil { + return errors.Wrapf(err, "error removing exit code for ID %s", id) + } + } + + toRemoveTimeStamps := []string{} + err = timeStampBucket.ForEach(func(id, _ []byte) error { + toRemoveTimeStamps = append(toRemoveTimeStamps, string(id)) + return nil + }) + if err != nil { + return errors.Wrapf(err, "reading timestamps bucket") + } + for _, id := range toRemoveTimeStamps { + if err := timeStampBucket.Delete([]byte(id)); err != nil { + return errors.Wrapf(err, "removing timestamp for ID %s", id) + } + } + // Iterate through all IDs. Check if they are containers. // If they are, unmarshal their state, and then clear // PID, mountpoint, and state for all of them @@ -1341,6 +1391,204 @@ func (s *BoltState) GetContainerConfig(id string) (*ContainerConfig, error) { return config, nil } +// AddContainerExitCode adds the exit code for the specified container to the database. +func (s *BoltState) AddContainerExitCode(id string, exitCode int32) error { + if len(id) == 0 { + return define.ErrEmptyID + } + + if !s.valid { + return define.ErrDBClosed + } + + db, err := s.getDBCon() + if err != nil { + return err + } + defer s.deferredCloseDBCon(db) + + rawID := []byte(id) + rawExitCode := []byte(strconv.Itoa(int(exitCode))) + rawTimeStamp, err := time.Now().MarshalText() + if err != nil { + return fmt.Errorf("marshaling exit-code time stamp: %w", err) + } + + return db.Update(func(tx *bolt.Tx) error { + exitCodeBucket, err := getExitCodeBucket(tx) + if err != nil { + return err + } + timeStampBucket, err := getExitCodeTimeStampBucket(tx) + if err != nil { + return err + } + + if err := exitCodeBucket.Put(rawID, rawExitCode); err != nil { + return fmt.Errorf("adding exit code of container %s to DB: %w", id, err) + } + if err := timeStampBucket.Put(rawID, rawTimeStamp); err != nil { + if rmErr := exitCodeBucket.Delete(rawID); rmErr != nil { + logrus.Errorf("Removing exit code of container %s from DB: %v", id, rmErr) + } + return fmt.Errorf("adding exit-code time stamp of container %s to DB: %w", id, err) + } + + return nil + }) +} + +// GetContainerExitCode returns the exit code for the specified container. +func (s *BoltState) GetContainerExitCode(id string) (int32, error) { + if len(id) == 0 { + return -1, define.ErrEmptyID + } + + if !s.valid { + return -1, define.ErrDBClosed + } + + db, err := s.getDBCon() + if err != nil { + return -1, err + } + defer s.deferredCloseDBCon(db) + + rawID := []byte(id) + result := int32(-1) + return result, db.View(func(tx *bolt.Tx) error { + exitCodeBucket, err := getExitCodeBucket(tx) + if err != nil { + return err + } + + rawExitCode := exitCodeBucket.Get(rawID) + if rawExitCode == nil { + return fmt.Errorf("getting exit code of container %s from DB: %w", id, define.ErrNoSuchExitCode) + } + + exitCode, err := strconv.Atoi(string(rawExitCode)) + if err != nil { + return fmt.Errorf("converting raw exit code %v of container %s: %w", rawExitCode, id, err) + } + + result = int32(exitCode) + return nil + }) +} + +// GetContainerExitCodeTimeStamp returns the time stamp when the exit code of +// the specified container was added to the database. +func (s *BoltState) GetContainerExitCodeTimeStamp(id string) (*time.Time, error) { + if len(id) == 0 { + return nil, define.ErrEmptyID + } + + if !s.valid { + return nil, define.ErrDBClosed + } + + db, err := s.getDBCon() + if err != nil { + return nil, err + } + defer s.deferredCloseDBCon(db) + + rawID := []byte(id) + var result time.Time + return &result, db.View(func(tx *bolt.Tx) error { + timeStampBucket, err := getExitCodeTimeStampBucket(tx) + if err != nil { + return err + } + + rawTimeStamp := timeStampBucket.Get(rawID) + if rawTimeStamp == nil { + return fmt.Errorf("getting exit-code time stamp of container %s from DB: %w", id, define.ErrNoSuchExitCode) + } + + if err := result.UnmarshalText(rawTimeStamp); err != nil { + return fmt.Errorf("converting raw time stamp %v of container %s from DB: %w", rawTimeStamp, id, err) + } + + return nil + }) +} + +// PruneExitCodes removes exit codes older than 5 minutes. +func (s *BoltState) PruneContainerExitCodes() error { + if !s.valid { + return define.ErrDBClosed + } + + db, err := s.getDBCon() + if err != nil { + return err + } + defer s.deferredCloseDBCon(db) + + toRemoveIDs := []string{} + + threshold := time.Minute * 5 + err = db.View(func(tx *bolt.Tx) error { + timeStampBucket, err := getExitCodeTimeStampBucket(tx) + if err != nil { + return err + } + + return timeStampBucket.ForEach(func(rawID, rawTimeStamp []byte) error { + var timeStamp time.Time + if err := timeStamp.UnmarshalText(rawTimeStamp); err != nil { + return fmt.Errorf("converting raw time stamp %v of container %s from DB: %w", rawTimeStamp, string(rawID), err) + } + if time.Since(timeStamp) > threshold { + toRemoveIDs = append(toRemoveIDs, string(rawID)) + } + return nil + }) + }) + if err != nil { + return errors.Wrapf(err, "reading exit codes to prune") + } + + if len(toRemoveIDs) > 0 { + err = db.Update(func(tx *bolt.Tx) error { + exitCodeBucket, err := getExitCodeBucket(tx) + if err != nil { + return err + } + timeStampBucket, err := getExitCodeTimeStampBucket(tx) + if err != nil { + return err + } + + var finalErr error + for _, id := range toRemoveIDs { + rawID := []byte(id) + if err := exitCodeBucket.Delete(rawID); err != nil { + if finalErr != nil { + logrus.Error(finalErr) + } + finalErr = fmt.Errorf("removing exit code of container %s from DB: %w", id, err) + } + if err := timeStampBucket.Delete(rawID); err != nil { + if finalErr != nil { + logrus.Error(finalErr) + } + finalErr = fmt.Errorf("removing exit code timestamp of container %s from DB: %w", id, err) + } + } + + return finalErr + }) + if err != nil { + return errors.Wrapf(err, "pruning exit codes") + } + } + + return nil +} + // AddExecSession adds an exec session to the state. func (s *BoltState) AddExecSession(ctr *Container, session *ExecSession) error { if !s.valid { diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index d6f035af9..edba78d6d 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -29,6 +29,9 @@ const ( aliasesName = "aliases" runtimeConfigName = "runtime-config" + exitCodeName = "exit-code" + exitCodeTimeStampName = "exit-code-time-stamp" + configName = "config" stateName = "state" dependenciesName = "dependencies" @@ -65,6 +68,9 @@ var ( volDependenciesBkt = []byte(volCtrDependencies) networksBkt = []byte(networksName) + exitCodeBkt = []byte(exitCodeName) + exitCodeTimeStampBkt = []byte(exitCodeTimeStampName) + configKey = []byte(configName) stateKey = []byte(stateName) netNSKey = []byte(netNSName) @@ -362,6 +368,22 @@ func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) { return bkt, nil } +func getExitCodeBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + bkt := tx.Bucket(exitCodeBkt) + if bkt == nil { + return nil, errors.Wrapf(define.ErrDBBadConfig, "exit-code container bucket not found in DB") + } + return bkt, nil +} + +func getExitCodeTimeStampBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + bkt := tx.Bucket(exitCodeTimeStampBkt) + if bkt == nil { + return nil, errors.Wrapf(define.ErrDBBadConfig, "exit-code time stamp bucket not found in DB") + } + return bkt, nil +} + func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig, ctrsBkt *bolt.Bucket) error { ctrBkt := ctrsBkt.Bucket(id) if ctrBkt == nil { diff --git a/libpod/container_api.go b/libpod/container_api.go index b064d3528..c14fe95b0 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -2,6 +2,7 @@ package libpod import ( "context" + "fmt" "io" "io/ioutil" "net/http" @@ -490,41 +491,84 @@ func (c *Container) RemoveArtifact(name string) error { // Wait blocks until the container exits and returns its exit code. func (c *Container) Wait(ctx context.Context) (int32, error) { - return c.WaitWithInterval(ctx, DefaultWaitInterval) + return c.WaitForExit(ctx, DefaultWaitInterval) } -// WaitWithInterval blocks until the container to exit and returns its exit -// code. The argument is the interval at which checks the container's status. -func (c *Container) WaitWithInterval(ctx context.Context, waitTimeout time.Duration) (int32, error) { +// WaitForExit blocks until the container exits and returns its exit code. The +// argument is the interval at which checks the container's status. +func (c *Container) WaitForExit(ctx context.Context, pollInterval time.Duration) (int32, error) { if !c.valid { return -1, define.ErrCtrRemoved } - exitFile, err := c.exitFilePath() - if err != nil { - return -1, err - } - chWait := make(chan error, 1) + id := c.ID() + var conmonTimer time.Timer + conmonTimerSet := false - go func() { - <-ctx.Done() - chWait <- define.ErrCanceled - }() + getExitCode := func() (bool, int32, error) { + containerRemoved := false + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + } - for { - // ignore errors here (with exception of cancellation), it is only used to avoid waiting - // too long. - _, e := WaitForFile(exitFile, chWait, waitTimeout) - if e == define.ErrCanceled { - return -1, define.ErrCanceled + if err := c.syncContainer(); err != nil { + if !errors.Is(err, define.ErrNoSuchCtr) { + return false, -1, err + } + containerRemoved = true + } + + // If conmon is not alive anymore set a timer to make sure + // we're returning even if conmon has forcefully been killed. + if !conmonTimerSet && !containerRemoved { + conmonAlive, err := c.ociRuntime.CheckConmonRunning(c) + switch { + case errors.Is(err, define.ErrNoSuchCtr): + containerRemoved = true + case err != nil: + return false, -1, err + case !conmonAlive: + timerDuration := time.Second * 20 + conmonTimer = *time.NewTimer(timerDuration) + conmonTimerSet = true + } + } + + if !containerRemoved { + // If conmon is dead for more than $timerDuration or if the + // container has exited properly, try to look up the exit code. + select { + case <-conmonTimer.C: + logrus.Debugf("Exceeded conmon timeout waiting for container %s to exit", id) + default: + if !c.ensureState(define.ContainerStateExited, define.ContainerStateConfigured) { + return false, -1, nil + } + } } - stopped, code, err := c.isStopped() + exitCode, err := c.runtime.state.GetContainerExitCode(id) + if err != nil { + return true, -1, err + } + + return true, exitCode, nil + } + + for { + hasExited, exitCode, err := getExitCode() + if hasExited { + return exitCode, err + } if err != nil { return -1, err } - if stopped { - return code, nil + select { + case <-ctx.Done(): + return -1, fmt.Errorf("waiting for exit code of container %s canceled", id) + default: + time.Sleep(pollInterval) } } } @@ -551,11 +595,12 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou wantedStates := make(map[define.ContainerStatus]bool, len(conditions)) for _, condition := range conditions { - if condition == define.ContainerStateStopped || condition == define.ContainerStateExited { + switch condition { + case define.ContainerStateExited, define.ContainerStateStopped: waitForExit = true - continue + default: + wantedStates[condition] = true } - wantedStates[condition] = true } trySend := func(code int32, err error) { @@ -572,7 +617,7 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou go func() { defer wg.Done() - code, err := c.WaitWithInterval(ctx, waitTimeout) + code, err := c.WaitForExit(ctx, waitTimeout) trySend(code, err) }() } diff --git a/libpod/container_internal.go b/libpod/container_internal.go index ce48987f6..ae61298f3 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -219,7 +219,7 @@ func (c *Container) handleExitFile(exitFile string, fi os.FileInfo) error { // Write an event for the container's death c.newContainerExitedEvent(c.state.ExitCode) - return nil + return c.runtime.state.AddContainerExitCode(c.ID(), c.state.ExitCode) } func (c *Container) shouldRestart() bool { @@ -784,20 +784,6 @@ func (c *Container) getArtifactPath(name string) string { return filepath.Join(c.config.StaticDir, artifactsDir, name) } -// Used with Wait() to determine if a container has exited -func (c *Container) isStopped() (bool, int32, error) { - if !c.batched { - c.lock.Lock() - defer c.lock.Unlock() - } - err := c.syncContainer() - if err != nil { - return true, -1, err - } - - return !c.ensureState(define.ContainerStateRunning, define.ContainerStatePaused, define.ContainerStateStopping), c.state.ExitCode, nil -} - // save container state to the database func (c *Container) save() error { if err := c.runtime.state.SaveContainer(c); err != nil { @@ -1282,13 +1268,6 @@ func (c *Container) stop(timeout uint) error { } } - // Check if conmon is still alive. - // If it is not, we won't be getting an exit file. - conmonAlive, err := c.ociRuntime.CheckConmonRunning(c) - if err != nil { - return err - } - // Set the container state to "stopping" and unlock the container // before handing it over to conmon to unblock other commands. #8501 // demonstrates nicely that a high stop timeout will block even simple @@ -1341,21 +1320,18 @@ func (c *Container) stop(timeout uint) error { } c.newContainerEvent(events.Stop) - - c.state.PID = 0 - c.state.ConmonPID = 0 c.state.StoppedByUser = true + conmonAlive, err := c.ociRuntime.CheckConmonRunning(c) + if err != nil { + return err + } if !conmonAlive { - // Conmon is dead, so we can't expect an exit code. - c.state.ExitCode = -1 - c.state.FinishedTime = time.Now() - c.state.State = define.ContainerStateStopped - if err := c.save(); err != nil { - logrus.Errorf("Saving container %s status: %v", c.ID(), err) + if err := c.checkExitFile(); err != nil { + return err } - return errors.Wrapf(define.ErrConmonDead, "container %s conmon process missing, cannot retrieve exit code", c.ID()) + return c.save() } if err := c.save(); err != nil { @@ -1939,6 +1915,18 @@ func (c *Container) cleanup(ctx context.Context) error { } } + // Prune the exit codes of other container during clean up. + // Since Podman is no daemon, we have to clean them up somewhere. + // Cleanup seems like a good place as it's not performance + // critical. + if err := c.runtime.state.PruneContainerExitCodes(); err != nil { + if lastError == nil { + lastError = err + } else { + logrus.Errorf("Pruning container exit codes: %v", err) + } + } + return lastError } diff --git a/libpod/define/errors.go b/libpod/define/errors.go index f5a7c73e5..9757a85b1 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -24,6 +24,10 @@ var ( // not exist. ErrNoSuchExecSession = errors.New("no such exec session") + // ErrNoSuchExitCode indicates that the requested container exit code + // does not exist. + ErrNoSuchExitCode = errors.New("no such exit code") + // ErrDepExists indicates that the current object has dependencies and // cannot be removed before them. ErrDepExists = errors.New("dependency exists") diff --git a/libpod/events.go b/libpod/events.go index f09d8402a..021b3b53c 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -151,6 +151,9 @@ func (r *Runtime) GetEvents(ctx context.Context, filters []string) ([]*events.Ev // GetLastContainerEvent takes a container name or ID and an event status and returns // the last occurrence of the container event func (r *Runtime) GetLastContainerEvent(ctx context.Context, nameOrID string, containerEvent events.Status) (*events.Event, error) { + // FIXME: events should be read in reverse order! + // https://github.com/containers/podman/issues/14579 + // check to make sure the event.Status is valid if _, err := events.StringToStatus(containerEvent.String()); err != nil { return nil, err diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index d34ff7cea..7a9ae7ee5 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -267,11 +267,6 @@ func (r *ConmonOCIRuntime) CreateContainer(ctr *Container, restoreOptions *Conta // status, but will instead only check for the existence of the conmon exit file // and update state to stopped if it exists. func (r *ConmonOCIRuntime) UpdateContainerStatus(ctr *Container) error { - exitFile, err := r.ExitFilePath(ctr) - if err != nil { - return err - } - runtimeDir, err := util.GetRuntimeDir() if err != nil { return err @@ -343,22 +338,10 @@ func (r *ConmonOCIRuntime) UpdateContainerStatus(ctr *Container) error { // Only grab exit status if we were not already stopped // If we were, it should already be in the database if ctr.state.State == define.ContainerStateStopped && oldState != define.ContainerStateStopped { - var fi os.FileInfo - chWait := make(chan error) - defer close(chWait) - - _, err := WaitForFile(exitFile, chWait, time.Second*5) - if err == nil { - fi, err = os.Stat(exitFile) + if _, err := ctr.Wait(context.Background()); err != nil { + logrus.Errorf("Waiting for container %s to exit: %v", ctr.ID(), err) } - if err != nil { - ctr.state.ExitCode = -1 - ctr.state.FinishedTime = time.Now() - logrus.Errorf("No exit file for container %s found: %v", ctr.ID(), err) - return nil - } - - return ctr.handleExitFile(exitFile, fi) + return nil } // Handle ContainerStateStopping - keep it unless the container @@ -1169,7 +1152,6 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co }).Debugf("running conmon: %s", r.conmonPath) cmd := exec.Command(r.conmonPath, args...) - cmd.Dir = ctr.bundlePath() cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, } @@ -1357,8 +1339,6 @@ func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, p logDriverArg = define.NoLogging case define.PassthroughLogging: logDriverArg = define.PassthroughLogging - case define.JSONLogging: - fallthrough //lint:ignore ST1015 the default case has to be here default: //nolint:stylecheck,gocritic // No case here should happen except JSONLogging, but keep this here in case the options are extended @@ -1368,6 +1348,8 @@ func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, p // to get here, either a user would specify `--log-driver ""`, or this came from another place in libpod // since the former case is obscure, and the latter case isn't an error, let's silently fallthrough fallthrough + case define.JSONLogging: + fallthrough case define.KubernetesLogging: logDriverArg = fmt.Sprintf("%s:%s", define.KubernetesLogging, logPath) } diff --git a/libpod/state.go b/libpod/state.go index 471023769..4fbd3c302 100644 --- a/libpod/state.go +++ b/libpod/state.go @@ -111,6 +111,15 @@ type State interface { // Return a container config from the database by full ID GetContainerConfig(id string) (*ContainerConfig, error) + // Add the exit code for the specified container to the database. + AddContainerExitCode(id string, exitCode int32) error + + // Return the exit code for the specified container. + GetContainerExitCode(id string) (int32, error) + + // Remove exit codes older than 5 minutes. + PruneContainerExitCodes() error + // Add creates a reference to an exec session in the database. // The container the exec session is attached to will be recorded. // The container state will not be modified. diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index 8588b49ba..1795f6ce1 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -191,7 +191,6 @@ func waitDockerCondition(ctx context.Context, containerName string, interval tim var notRunningStates = []define.ContainerStatus{ define.ContainerStateCreated, define.ContainerStateRemoving, - define.ContainerStateStopped, define.ContainerStateExited, define.ContainerStateConfigured, } diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 1934533cf..281e448f6 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -16,7 +16,6 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/libpod/logs" "github.com/containers/podman/v4/pkg/checkpoint" "github.com/containers/podman/v4/pkg/domain/entities" @@ -939,6 +938,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri } return reports, errors.Wrapf(err, "unable to start container %s", ctr.ID()) } + exitCode = ic.GetContainerExitCode(ctx, ctr) reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), @@ -1099,25 +1099,11 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta func (ic *ContainerEngine) GetContainerExitCode(ctx context.Context, ctr *libpod.Container) int { exitCode, err := ctr.Wait(ctx) - if err == nil { - return int(exitCode) - } - if errors.Cause(err) != define.ErrNoSuchCtr { - logrus.Errorf("Could not retrieve exit code: %v", err) + if err != nil { + logrus.Errorf("Waiting for container %s: %v", ctr.ID(), err) return define.ExecErrorCodeNotFound } - // Make 4 attempt with 0.25s backoff between each for 1 second total - var event *events.Event - for i := 0; i < 4; i++ { - event, err = ic.Libpod.GetLastContainerEvent(ctx, ctr.ID(), events.Exited) - if err != nil { - time.Sleep(250 * time.Millisecond) - continue - } - return event.ContainerExitCode - } - logrus.Errorf("Could not retrieve exit code from event: %v", err) - return define.ExecErrorCodeNotFound + return int(exitCode) } func (ic *ContainerEngine) ContainerLogs(ctx context.Context, containers []string, options entities.ContainerLogsOptions) error { diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go index 1c8c6ac81..e2121e7bf 100644 --- a/pkg/machine/e2e/list_test.go +++ b/pkg/machine/e2e/list_test.go @@ -29,7 +29,7 @@ var _ = Describe("podman machine list", func() { firstList, err := mb.setCmd(list).run() Expect(err).NotTo(HaveOccurred()) Expect(firstList).Should(Exit(0)) - Expect(len(firstList.outputToStringSlice())).To(Equal(1)) // just the header + Expect(firstList.outputToStringSlice()).To(HaveLen(1)) // just the header i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() @@ -39,7 +39,7 @@ var _ = Describe("podman machine list", func() { secondList, err := mb.setCmd(list).run() Expect(err).NotTo(HaveOccurred()) Expect(secondList).To(Exit(0)) - Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // one machine and the header + Expect(secondList.outputToStringSlice()).To(HaveLen(2)) // one machine and the header }) It("list machines with quiet or noheading", func() { @@ -51,12 +51,12 @@ var _ = Describe("podman machine list", func() { firstList, err := mb.setCmd(list.withQuiet()).run() Expect(err).NotTo(HaveOccurred()) Expect(firstList).Should(Exit(0)) - Expect(len(firstList.outputToStringSlice())).To(Equal(0)) // No header with quiet + Expect(firstList.outputToStringSlice()).To(HaveLen(0)) // No header with quiet noheaderSession, err := mb.setCmd(list.withNoHeading()).run() // noheader Expect(err).NotTo(HaveOccurred()) Expect(noheaderSession).Should(Exit(0)) - Expect(len(noheaderSession.outputToStringSlice())).To(Equal(0)) + Expect(noheaderSession.outputToStringSlice()).To(HaveLen(0)) i := new(initMachine) session, err := mb.setName(name1).setCmd(i.withImagePath(mb.imagePath)).run() @@ -70,7 +70,7 @@ var _ = Describe("podman machine list", func() { secondList, err := mb.setCmd(list.withQuiet()).run() Expect(err).NotTo(HaveOccurred()) Expect(secondList).To(Exit(0)) - Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // two machines, no header + Expect(secondList.outputToStringSlice()).To(HaveLen(2)) // two machines, no header listNames := secondList.outputToStringSlice() stripAsterisk(listNames) @@ -116,10 +116,10 @@ var _ = Describe("podman machine list", func() { // go format list := new(listMachine) - listSession, err := mb.setCmd(list.withFormat("{{.Name}}").withNoHeading()).run() + listSession, err := mb.setCmd(list.withFormat("{{.Name}}")).run() Expect(err).NotTo(HaveOccurred()) Expect(listSession).To(Exit(0)) - Expect(len(listSession.outputToStringSlice())).To(Equal(1)) + Expect(listSession.outputToStringSlice()).To(HaveLen(1)) listNames := listSession.outputToStringSlice() stripAsterisk(listNames) @@ -135,6 +135,15 @@ var _ = Describe("podman machine list", func() { var listResponse []*machine.ListReporter err = jsoniter.Unmarshal(listSession.Bytes(), &listResponse) Expect(err).To(BeNil()) + + // table format includes the header + list = new(listMachine) + listSession3, err3 := mb.setCmd(list.withFormat("table {{.Name}}")).run() + Expect(err3).NotTo(HaveOccurred()) + Expect(listSession3).To(Exit(0)) + listNames3 := listSession3.outputToStringSlice() + Expect(listNames3).To(HaveLen(2)) + Expect(listNames3).To(ContainSubstring("NAME")) }) }) diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go index a7a1022b0..f272a5c11 100644 --- a/pkg/specgen/volumes.go +++ b/pkg/specgen/volumes.go @@ -1,6 +1,7 @@ package specgen import ( + "path/filepath" "strings" "github.com/containers/common/pkg/parse" @@ -56,7 +57,6 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na overlayVolumes := make(map[string]*OverlayVolume) volumeFormatErr := errors.Errorf("incorrect volume format, should be [host-dir:]ctr-dir[:option]") - for _, vol := range volumeFlag { var ( options []string @@ -71,6 +71,20 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na } src = splitVol[0] + + // Support relative paths beginning with ./ + if strings.HasPrefix(src, "./") { + path, err := filepath.EvalSymlinks(src) + if err != nil { + return nil, nil, nil, err + } + src, err = filepath.Abs(path) + if err != nil { + return nil, nil, nil, err + } + splitVol[0] = src + } + if len(splitVol) == 1 { // This is an anonymous named volume. Only thing given // is destination. diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go index 2ad3cc75e..53681f05b 100644 --- a/test/e2e/image_scp_test.go +++ b/test/e2e/image_scp_test.go @@ -22,12 +22,10 @@ var _ = Describe("podman image scp", func() { ) BeforeEach(func() { - ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF") conf, err := ioutil.TempFile("", "containersconf") - if err != nil { - panic(err) - } + Expect(err).ToNot(HaveOccurred()) + os.Setenv("CONTAINERS_CONF", conf.Name()) tempdir, err = CreateTempDirInTempDir() if err != nil { @@ -57,7 +55,7 @@ var _ = Describe("podman image scp", func() { } scp := podmanTest.Podman([]string{"image", "scp", "FOOBAR"}) scp.WaitWithDefaultTimeout() - Expect(scp).To(ExitWithError()) + Expect(scp).Should(ExitWithError()) }) It("podman image scp with proper connection", func() { @@ -67,27 +65,28 @@ var _ = Describe("podman image scp", func() { cmd := []string{"system", "connection", "add", "--default", "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", } session := podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() - Expect(session).To(Exit(0)) + Expect(session).Should(Exit(0)) cfg, err := config.ReadCustomConfig() Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine).To(HaveField("ActiveService", "QA")) + Expect(cfg.Engine).Should(HaveField("ActiveService", "QA")) Expect(cfg.Engine.ServiceDestinations).To(HaveKeyWithValue("QA", config.Destination{ - URI: "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + URI: "ssh://root@podman.test:2222/run/podman/podman.sock", }, )) scp := podmanTest.Podman([]string{"image", "scp", ALPINE, "QA::"}) - scp.Wait(45) + scp.WaitWithDefaultTimeout() // exit with error because we cannot make an actual ssh connection // This tests that the input we are given is validated and prepared correctly - // The error given should either be a missing image (due to testing suite complications) or a i/o timeout on ssh - Expect(scp).To(ExitWithError()) + // The error given should either be a missing image (due to testing suite complications) or a no such host timeout on ssh + Expect(scp).Should(ExitWithError()) + Expect(scp.ErrorToString()).Should(ContainSubstring("no such host")) }) diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index f31e62e42..edb657695 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -953,4 +953,32 @@ USER testuser`, fedoraMinimal) Expect(volMount).Should(Exit(0)) Expect(volMount.OutputToString()).To(Equal("1000:1000")) }) + + It("podman run -v with a relative dir", func() { + mountPath := filepath.Join(podmanTest.TempDir, "vol") + err = os.Mkdir(mountPath, 0755) + Expect(err).ToNot(HaveOccurred()) + defer func() { + err := os.RemoveAll(mountPath) + Expect(err).ToNot(HaveOccurred()) + }() + + f, err := os.CreateTemp(mountPath, "podman") + Expect(err).ToNot(HaveOccurred()) + + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + + err = os.Chdir(mountPath) + Expect(err).ToNot(HaveOccurred()) + defer func() { + err := os.Chdir(cwd) + Expect(err).ToNot(HaveOccurred()) + }() + + run := podmanTest.Podman([]string{"run", "-it", "--security-opt", "label=disable", "-v", "./:" + dest, ALPINE, "ls", dest}) + run.WaitWithDefaultTimeout() + Expect(run).Should(Exit(0)) + Expect(run.OutputToString()).Should(ContainSubstring(strings.TrimLeft("/vol/", f.Name()))) + }) }) diff --git a/test/e2e/system_connection_test.go b/test/e2e/system_connection_test.go index 8f755e692..baa31424b 100644 --- a/test/e2e/system_connection_test.go +++ b/test/e2e/system_connection_test.go @@ -47,9 +47,7 @@ var _ = Describe("podman system connection", func() { } f := CurrentGinkgoTestDescription() - _, _ = GinkgoWriter.Write( - []byte( - fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()))) + processTestResult(f) }) Context("without running API service", func() { @@ -58,7 +56,7 @@ var _ = Describe("podman system connection", func() { "--default", "--identity", "~/.ssh/id_rsa", "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", } session := podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() @@ -67,10 +65,10 @@ var _ = Describe("podman system connection", func() { cfg, err := config.ReadCustomConfig() Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg).To(HaveActiveService("QA")) + Expect(cfg).Should(HaveActiveService("QA")) Expect(cfg).Should(VerifyService( "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", "~/.ssh/id_rsa", )) @@ -82,7 +80,7 @@ var _ = Describe("podman system connection", func() { session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(config.ReadCustomConfig()).To(HaveActiveService("QE")) + Expect(config.ReadCustomConfig()).Should(HaveActiveService("QE")) }) It("add UDS", func() { @@ -141,7 +139,7 @@ var _ = Describe("podman system connection", func() { "--default", "--identity", "~/.ssh/id_rsa", "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", }) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -155,8 +153,8 @@ var _ = Describe("podman system connection", func() { cfg, err := config.ReadCustomConfig() Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(BeEmpty()) - Expect(cfg.Engine.ServiceDestinations).To(BeEmpty()) + Expect(cfg.Engine.ActiveService).Should(BeEmpty()) + Expect(cfg.Engine.ServiceDestinations).Should(BeEmpty()) } }) @@ -165,7 +163,7 @@ var _ = Describe("podman system connection", func() { "--default", "--identity", "~/.ssh/id_rsa", "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", }) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) @@ -187,7 +185,7 @@ var _ = Describe("podman system connection", func() { "--default", "--identity", "~/.ssh/id_rsa", name, - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "ssh://root@podman.test:2222/run/podman/podman.sock", } session := podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 117d791d6..56cf4f266 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -376,17 +376,7 @@ json-file | f while read driver do_check; do msg=$(random_string 15) run_podman run --name myctr --log-driver $driver $IMAGE echo $msg - - # Simple output check - # Special case: 'json-file' emits a warning, the rest do not - # ...but with podman-remote the warning is on the server only - if [[ $do_check == 'f' ]] && ! is_remote; then # 'f' for 'fallback' - is "${lines[0]}" ".* level=error msg=\"json-file logging specified but not supported. Choosing k8s-file logging instead\"" \ - "Fallback warning emitted" - is "${lines[1]}" "$msg" "basic output sanity check (driver=$driver)" - else - is "$output" "$msg" "basic output sanity check (driver=$driver)" - fi + is "$output" "$msg" "basic output sanity check (driver=$driver)" # Simply confirm that podman preserved our argument as-is run_podman inspect --format '{{.HostConfig.LogConfig.Type}}' myctr diff --git a/test/system/150-login.bats b/test/system/150-login.bats index 33b8438bf..dc902d5fe 100644 --- a/test/system/150-login.bats +++ b/test/system/150-login.bats @@ -314,7 +314,7 @@ function _test_skopeo_credential_sharing() { fi # Make sure socket is closed - if { exec 3<> /dev/tcp/127.0.0.1/${PODMAN_LOGIN_REGISTRY_PORT}; } &>/dev/null; then + if ! port_is_free $PODMAN_LOGIN_REGISTRY_PORT; then die "Socket still seems open" fi } diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index fb785177c..0d724985e 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -676,12 +676,12 @@ EOF @test "podman run port forward range" { for netmode in bridge slirp4netns:port_handler=slirp4netns slirp4netns:port_handler=rootlesskit; do - local port=$(random_free_port) - local end_port=$(( $port + 2 )) - local range="$port-$end_port:$port-$end_port" + local range=$(random_free_port_range 3) + local port="${test%-*}" + local end_port="${test#-*}" local random=$(random_string) - run_podman run --network $netmode -p "$range" -d $IMAGE sleep inf + run_podman run --network $netmode -p "$range:$range" -d $IMAGE sleep inf cid="$output" for port in $(seq $port $end_port); do run_podman exec -d $cid nc -l -p $port -e /bin/cat diff --git a/test/system/helpers.bash b/test/system/helpers.bash index 74b5ddc4b..273e8d2f5 100644 --- a/test/system/helpers.bash +++ b/test/system/helpers.bash @@ -284,7 +284,7 @@ function random_free_port() { local port for port in $(shuf -i ${range}); do - if ! { exec {unused_fd}<> /dev/tcp/127.0.0.1/$port; } &>/dev/null; then + if port_is_free $port; then echo $port return fi @@ -293,6 +293,35 @@ function random_free_port() { die "Could not find open port in range $range" } +function random_free_port_range() { + local size=${1?Usage: random_free_port_range SIZE (as in, number of ports)} + + local maxtries=10 + while [[ $maxtries -gt 0 ]]; do + local firstport=$(random_free_port) + local all_ports_free=1 + for i in $(seq 2 $size); do + if ! port_is_free $((firstport + $i)); then + all_ports_free= + break + fi + done + if [[ -n "$all_ports_free" ]]; then + echo "$firstport-$((firstport + $size - 1))" + return + fi + + maxtries=$((maxtries - 1)) + done + + die "Could not find free port range with size $size" +} + +function port_is_free() { + local port=${1?Usage: port_is_free PORT} + ! { exec {unused_fd}<> /dev/tcp/127.0.0.1/$port; } &>/dev/null +} + ################### # wait_for_port # Returns once port is available on host ################### diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 3bb22a971..95d8e59da 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -1,6 +1,7 @@ package assert import ( + "bytes" "fmt" "reflect" "time" @@ -32,7 +33,8 @@ var ( stringType = reflect.TypeOf("") - timeType = reflect.TypeOf(time.Time{}) + timeType = reflect.TypeOf(time.Time{}) + bytesType = reflect.TypeOf([]byte{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -323,6 +325,26 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) } + case reflect.Slice: + { + // We only care about the []byte type. + if !canConvert(obj1Value, bytesType) { + break + } + + // []byte can be compared! + bytesObj1, ok := obj1.([]byte) + if !ok { + bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte) + + } + bytesObj2, ok := obj2.([]byte) + if !ok { + bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) + } + + return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true + } } return compareEqual, false diff --git a/vendor/modules.txt b/vendor/modules.txt index 187197d33..e72779e09 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -645,7 +645,7 @@ github.com/spf13/cobra github.com/spf13/pflag # github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 github.com/stefanberger/go-pkcs11uri -# github.com/stretchr/testify v1.7.4 +# github.com/stretchr/testify v1.7.5 ## explicit github.com/stretchr/testify/assert github.com/stretchr/testify/require |