Repository: opencontainers/runc
File: libcontainer/state_linux.go (Go)

Code before:

func (b *stoppedState) status() Status {
	return Stopped
}

func (b *stoppedState) transition(s containerState) error {
	switch s.(type) {
	case *runningState, *restoredState:
		b.c.state = s
		return nil
	case *stoppedState:
		return nil
	}
	return newStateTransitionError(b, s)
}

func (b *stoppedState) destroy() error {
	// Normally, when a container init is gone, all other processes in its
	// cgroup are killed by the kernel. This is not the case for a shared
	// PID namespace container, which may have some leftover processes.
	if !b.c.config.Namespaces.IsPrivate(configs.NEWPID) {
		// Get container pids on a best-effort basis.
		if pids, _ := b.c.cgroupManager.GetAllPids(); len(pids) > 0 {
			return fmt.Errorf("container is in stopped state (init is gone), but its cgroup still has %d processes; use runc delete -f or runc kill", len(pids))
		}
	}
	return destroy(b.c)
}

// runningState represents a container that is currently running.
type runningState struct {
	c *Container
}

func (r *runningState) status() Status {
	return Running
}

func (r *runningState) transition(s containerState) error {
	switch s.(type) {
	case *stoppedState:
		if r.c.hasInit() {
			return ErrRunning
		}
		r.c.state = s
		return nil
	case *pausedState:
		r.c.state = s
		return nil
	case *runningState:
		return nil

Review comment (bug):
So, I understand the reasoning behind this -- by some definition the container is still "alive" because the pid1 is not special for non-pidns containers. And you cannot `runc delete` a container that is is still alive, you need to `runc kill` it or use `runc delete -f`. So there is _an_ argument that this is more consistent.

However, runc considers the container to be stopped because we bind the lifecycle of the container to the pid1 (as evidenced by `runc state` and the fact that this code was added for `stoppedState`). From that perspective, it's more consistent that any container which is "stopped" (according to `runc state`) should be able to be deleted with `runc delete` (no `-f`) except in weird circumstances.

IMHO, I think the latter is more consistent, primarily because (in my mind) killing the init process of a container means you've conceptually killed the container by definition (regardless of whether you have a separate pidns) and thus any remaining processes in the container are leftover garbage that `runc delete` should be happy to remove. This also has the benefit of not affecting compatibility with pre-1.2 runc. I agree the compatibility concern isn't enormous, because of what an edge-case host pidns containers are, but the difference between this and the shared cgroup stuff is the shared cgroup stuff was categorically broken and thus unusable -- this is a straight-forward change of behaviour, not removal of a broken mis-feature.

I think we should drop this part.

Code after:
		return err
	}
	s.Status = specs.StateStopped

	return hooks.Run(configs.Poststop, s)
}

// stoppedState represents a container is a stopped/destroyed state.
type stoppedState struct {
	c *Container
}

func (b *stoppedState) status() Status {
	return Stopped
}

func (b *stoppedState) transition(s containerState) error {
	switch s.(type) {
	case *runningState, *restoredState:
		b.c.state = s
		return nil
	case *stoppedState:
		return nil
	}
	return newStateTransitionError(b, s)
}

func (b *stoppedState) destroy() error {
	return destroy(b.c)
}

// runningState represents a container that is currently running.
type runningState struct {
	c *Container
}

func (r *runningState) status() Status {
	return Running
}

func (r *runningState) transition(s containerState) error {
	switch s.(type) {
	case *stoppedState:
		if r.c.hasInit() {
			return ErrRunning
		}
		r.c.state = s
		return nil
	case *pausedState:
		r.c.state = s
		return nil