diff --git a/cmd/crio/wipe.go b/cmd/crio/wipe.go index 74f23d47ec0..4875f6356d1 100644 --- a/cmd/crio/wipe.go +++ b/cmd/crio/wipe.go @@ -67,6 +67,15 @@ func crioWipe(c *cli.Context) error { return handleCleanShutdown(config, store) } + // If crio is configured to wipe internally (and `--force` wasn't set) + // the `crio wipe` command has nothing left to do, + // as the remaining work will be done on server startup. + if config.InternalWipe && !c.IsSet("force") { + return nil + } + + logrus.Infof("Internal wipe not set, meaning crio wipe will wipe. In the future, all wipes after reboot will happen when starting the crio server.") + // if we should not wipe, exit with no error if !shouldWipeContainers { // we should not wipe images without wiping containers diff --git a/completions/bash/crio b/completions/bash/crio index 663c9f26237..b5fac2405d9 100755 --- a/completions/bash/crio +++ b/completions/bash/crio @@ -49,6 +49,7 @@ h --image-volumes --infra-ctr-cpuset --insecure-registry +--internal-wipe --irqbalance-config-file --listen --log diff --git a/completions/fish/crio.fish b/completions/fish/crio.fish index 70e6f7ee746..a2daa8daf17 100644 --- a/completions/fish/crio.fish +++ b/completions/fish/crio.fish @@ -87,6 +87,7 @@ complete -c crio -n '__fish_crio_no_subcommand' -f -l insecure-registry -r -d 'E be enabled for testing purposes**. For increased security, users should add their CA to their system\'s list of trusted CAs instead of using \'--insecure-registry\'.' +complete -c crio -n '__fish_crio_no_subcommand' -f -l internal-wipe -d 'Whether CRI-O should wipe containers after a reboot and images after an upgrade when the server starts. If set to false, one must run `crio wipe` to wipe the containers and images in these situations.' complete -c crio -n '__fish_crio_no_subcommand' -f -l irqbalance-config-file -r -d 'The irqbalance service config file which is used by CRI-O.' complete -c crio -n '__fish_crio_no_subcommand' -l listen -r -d 'Path to the CRI-O socket' complete -c crio -n '__fish_crio_no_subcommand' -l log -r -d 'Set the log file path where internal debug information is written' diff --git a/completions/zsh/_crio b/completions/zsh/_crio index 9ad7e402f66..4900244e71f 100644 --- a/completions/zsh/_crio +++ b/completions/zsh/_crio @@ -7,7 +7,7 @@ it later with **--config**. Global options will modify the output.' 'version:dis _describe 'commands' cmds local -a opts - opts=('--absent-mount-sources-to-reject' '--additional-devices' '--apparmor-profile' '--big-files-temporary-dir' '--bind-mount-prefix' '--cgroup-manager' '--clean-shutdown-file' '--cni-config-dir' '--cni-default-network' '--cni-plugin-dir' '--config' '--config-dir' '--conmon' '--conmon-cgroup' '--conmon-env' '--container-attach-socket-dir' '--container-exits-dir' '--ctr-stop-timeout' '--decryption-keys-path' '--default-capabilities' '--default-env' '--default-mounts-file' '--default-runtime' '--default-sysctls' '--default-transport' '--default-ulimits' '--drop-infra-ctr' '--enable-metrics' '--enable-profile-unix-socket' '--gid-mappings' '--global-auth-file' '--grpc-max-recv-msg-size' '--grpc-max-send-msg-size' '--hooks-dir' '--image-volumes' '--infra-ctr-cpuset' '--insecure-registry' '--irqbalance-config-file' '--listen' '--log' '--log-dir' '--log-filter' '--log-format' '--log-journald' '--log-level' '--log-size-max' '--metrics-port' '--metrics-socket' '--namespaces-dir' '--no-pivot' '--pause-command' '--pause-image' '--pause-image-auth-file' '--pids-limit' '--pinns-path' '--profile' '--profile-port' '--read-only' '--registries-conf' '--registries-conf-dir' '--registry' '--root' '--runroot' '--runtimes' '--seccomp-profile' '--seccomp-use-default-when-empty' '--selinux' '--separate-pull-cgroup' '--signature-policy' '--storage-driver' '--storage-opt' '--stream-address' '--stream-enable-tls' '--stream-idle-timeout' '--stream-port' '--stream-tls-ca' '--stream-tls-cert' '--stream-tls-key' '--uid-mappings' '--version-file' '--version-file-persist' '--help' '--version') + opts=('--absent-mount-sources-to-reject' '--additional-devices' '--apparmor-profile' '--big-files-temporary-dir' '--bind-mount-prefix' '--cgroup-manager' '--clean-shutdown-file' '--cni-config-dir' '--cni-default-network' '--cni-plugin-dir' '--config' '--config-dir' '--conmon' '--conmon-cgroup' '--conmon-env' '--container-attach-socket-dir' '--container-exits-dir' '--ctr-stop-timeout' '--decryption-keys-path' '--default-capabilities' '--default-env' '--default-mounts-file' '--default-runtime' '--default-sysctls' '--default-transport' '--default-ulimits' '--drop-infra-ctr' '--enable-metrics' '--enable-profile-unix-socket' '--gid-mappings' '--global-auth-file' '--grpc-max-recv-msg-size' '--grpc-max-send-msg-size' '--hooks-dir' '--image-volumes' '--infra-ctr-cpuset' '--insecure-registry' '--internal-wipe' '--irqbalance-config-file' '--listen' '--log' '--log-dir' '--log-filter' '--log-format' '--log-journald' '--log-level' '--log-size-max' '--metrics-port' '--metrics-socket' '--namespaces-dir' '--no-pivot' '--pause-command' '--pause-image' '--pause-image-auth-file' '--pids-limit' '--pinns-path' '--profile' '--profile-port' '--read-only' '--registries-conf' '--registries-conf-dir' '--registry' '--root' '--runroot' '--runtimes' '--seccomp-profile' '--seccomp-use-default-when-empty' '--selinux' '--separate-pull-cgroup' '--signature-policy' '--storage-driver' '--storage-opt' '--stream-address' '--stream-enable-tls' '--stream-idle-timeout' '--stream-port' '--stream-tls-ca' '--stream-tls-cert' '--stream-tls-key' '--uid-mappings' '--version-file' '--version-file-persist' '--help' '--version') _describe 'global options' opts return diff --git a/docs/crio.8.md b/docs/crio.8.md index eaae664cea8..198ccde8afd 100644 --- a/docs/crio.8.md +++ b/docs/crio.8.md @@ -49,6 +49,7 @@ crio [--image-volumes]=[value] [--infra-ctr-cpuset]=[value] [--insecure-registry]=[value] +[--internal-wipe] [--irqbalance-config-file]=[value] [--listen]=[value] [--log-dir]=[value] @@ -235,6 +236,8 @@ crio [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] their CA to their system's list of trusted CAs instead of using '--insecure-registry'. (default: []) +**--internal-wipe**: Whether CRI-O should wipe containers after a reboot and images after an upgrade when the server starts. If set to false, one must run `crio wipe` to wipe the containers and images in these situations. + **--irqbalance-config-file**="": The irqbalance service config file which is used by CRI-O. (default: /etc/sysconfig/irqbalance) **--listen**="": Path to the CRI-O socket (default: /var/run/crio/crio.sock) diff --git a/docs/crio.conf.5.md b/docs/crio.conf.5.md index 52053529975..2806fb57359 100644 --- a/docs/crio.conf.5.md +++ b/docs/crio.conf.5.md @@ -54,6 +54,10 @@ CRI-O reads its storage defaults from the containers-storage.conf(5) file locate It is used to check if crio wipe should wipe images, which should only happen when CRI-O has been upgraded +**internal_wipe**=false + Whether CRI-O should wipe containers after a reboot and images after an upgrade when the server starts. + If set to false, one must run `crio wipe` to wipe the containers and images in these situations. + **clean_shutdown_file**="/var/lib/crio/clean.shutdown" Location for CRI-O to lay down the clean shutdown file. It is used to check whether crio had time to sync before shutting down. diff --git a/internal/criocli/criocli.go b/internal/criocli/criocli.go index 8f8447137c7..3d7b1a8f021 100644 --- a/internal/criocli/criocli.go +++ b/internal/criocli/criocli.go @@ -293,6 +293,9 @@ func mergeConfig(config *libconfig.Config, ctx *cli.Context) error { if ctx.IsSet("absent-mount-sources-to-reject") { config.AbsentMountSourcesToReject = StringSliceTrySplit(ctx, "absent-mount-sources-to-reject") } + if ctx.IsSet("internal-wipe") { + config.InternalWipe = ctx.Bool("internal-wipe") + } if ctx.IsSet("enable-metrics") { config.EnableMetrics = ctx.Bool("enable-metrics") } @@ -839,6 +842,12 @@ func getCrioFlags(defConf *libconfig.Config) []cli.Flag { EnvVars: []string{"CONTAINER_VERSION_FILE_PERSIST"}, TakesFile: true, }, + &cli.BoolFlag{ + Name: "internal-wipe", + Usage: "Whether CRI-O should wipe containers after a reboot and images after an upgrade when the server starts. If set to false, one must run `crio wipe` to wipe the containers and images in these situations.", + Value: defConf.InternalWipe, + EnvVars: []string{"CONTAINER_INTERNAL_WIPE"}, + }, &cli.StringFlag{ Name: "infra-ctr-cpuset", Usage: "CPU set to run infra containers, if not specified CRI-O will use all online CPUs to run infra containers (default: '').", diff --git a/internal/lib/remove.go b/internal/lib/remove.go index 533415a247c..acbf252dfe5 100644 --- a/internal/lib/remove.go +++ b/internal/lib/remove.go @@ -23,8 +23,7 @@ func (c *ContainerServer) Remove(ctx context.Context, container string, force bo return "", errors.Errorf("cannot remove paused container %s", ctrID) case oci.ContainerStateCreated, oci.ContainerStateRunning: if force { - _, err = c.ContainerStop(ctx, container, 10) - if err != nil { + if err = c.StopContainer(ctx, ctr, 10); err != nil { return "", errors.Wrapf(err, "unable to stop container %s", ctrID) } } else { diff --git a/internal/lib/sandbox/sandbox.go b/internal/lib/sandbox/sandbox.go index 49e1d6dc517..b4ebcd5b127 100644 --- a/internal/lib/sandbox/sandbox.go +++ b/internal/lib/sandbox/sandbox.go @@ -371,6 +371,11 @@ func (s *Sandbox) createFileInInfraDir(filename string) error { return nil } infra := s.InfraContainer() + // If the infra directory has been cleaned up already, we should not fail to + // create this file. + if _, err := os.Stat(infra.Dir()); os.IsNotExist(err) { + return nil + } f, err := os.Create(filepath.Join(infra.Dir(), filename)) if err == nil { f.Close() diff --git a/internal/lib/stop.go b/internal/lib/stop.go index 08e115684dc..21cefd72073 100644 --- a/internal/lib/stop.go +++ b/internal/lib/stop.go @@ -10,29 +10,22 @@ import ( ) // ContainerStop stops a running container with a grace period (i.e., timeout). -func (c *ContainerServer) ContainerStop(ctx context.Context, container string, timeout int64) (string, error) { - ctr, err := c.LookupContainer(container) - if err != nil { - return "", errors.Wrapf(err, "failed to find container %s", container) - } - ctrID := ctr.ID() - - err = c.runtime.StopContainer(ctx, ctr, timeout) - if err != nil { +func (c *ContainerServer) StopContainer(ctx context.Context, ctr *oci.Container, timeout int64) error { + if err := c.runtime.StopContainer(ctx, ctr, timeout); err != nil { // only fatally error if the error is not that the container was already stopped // we still want to write container state to disk if the container has already // been stopped if err != oci.ErrContainerStopped { - return "", errors.Wrapf(err, "failed to stop container %s", ctrID) + return errors.Wrapf(err, "failed to stop container %s", ctr.ID()) } } else { // we only do these operations if StopContainer didn't fail (even if the failure // was the container already being stopped) if err := c.runtime.WaitContainerStateStopped(ctx, ctr); err != nil { - return "", errors.Wrapf(err, "failed to get container 'stopped' status %s", ctrID) + return errors.Wrapf(err, "failed to get container 'stopped' status %s", ctr.ID()) } - if err := c.storageRuntimeServer.StopContainer(ctrID); err != nil { - return "", errors.Wrapf(err, "failed to unmount container %s", ctrID) + if err := c.storageRuntimeServer.StopContainer(ctr.ID()); err != nil { + return errors.Wrapf(err, "failed to unmount container %s", ctr.ID()) } } @@ -40,5 +33,5 @@ func (c *ContainerServer) ContainerStop(ctx context.Context, container string, t log.Warnf(ctx, "unable to write containers %s state to disk: %v", ctr.ID(), err) } - return ctrID, nil + return nil } diff --git a/internal/lib/stop_test.go b/internal/lib/stop_test.go deleted file mode 100644 index d41a4fafe09..00000000000 --- a/internal/lib/stop_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package lib_test - -import ( - "context" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -// The actual test suite -var _ = t.Describe("ContainerServer", func() { - // Prepare the sut - BeforeEach(beforeEach) - - t.Describe("Stop", func() { - It("should fail on invalid container ID", func() { - // Given - // When - res, err := sut.ContainerStop(context.Background(), "", 0) - - // Then - Expect(err).NotTo(BeNil()) - Expect(res).To(BeEmpty()) - }) - }) -}) diff --git a/internal/resourcestore/resourcecleaner.go b/internal/resourcestore/resourcecleaner.go index addcb7c7f8b..37c9bb784fd 100644 --- a/internal/resourcestore/resourcecleaner.go +++ b/internal/resourcestore/resourcecleaner.go @@ -1,33 +1,85 @@ package resourcestore -// A CleanupFunc is a function that cleans up one piece of -// the associated resource. -type CleanupFunc func() +import ( + "context" + "time" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/cri-o/cri-o/internal/log" +) // ResourceCleaner is a structure that tracks // how to cleanup a resource. // CleanupFuncs can be added to it, and it can be told to // Cleanup the resource type ResourceCleaner struct { - funcs []CleanupFunc + funcs []cleanupFunc } +// A cleanupFunc is a function that cleans up one piece of +// the associated resource. +type cleanupFunc func() error + // NewResourceCleaner creates a new ResourceCleaner func NewResourceCleaner() *ResourceCleaner { - return &ResourceCleaner{ - funcs: make([]CleanupFunc, 0), - } + return &ResourceCleaner{} } // Add adds a new CleanupFunc to the ResourceCleaner -func (r *ResourceCleaner) Add(f func()) { - r.funcs = append(r.funcs, CleanupFunc(f)) +func (r *ResourceCleaner) Add( + ctx context.Context, + description string, + fn func() error, +) { + // Create a retry task on top of the provided function + task := func() error { + err := retry(ctx, fn) + if err != nil { + log.Errorf(ctx, + "Retried cleanup function %q too often, giving up", + description, + ) + } + return err + } + + // Prepend reverse iterate by default + r.funcs = append([]cleanupFunc{task}, r.funcs...) } // Cleanup cleans up the resource, running // the cleanup funcs in opposite chronological order -func (r *ResourceCleaner) Cleanup() { - for i := len(r.funcs) - 1; i >= 0; i-- { - r.funcs[i]() +func (r *ResourceCleaner) Cleanup() error { + for _, f := range r.funcs { + if err := f(); err != nil { + return err + } } + return nil +} + +// retry attempts to execute fn up to defaultRetryTimes if its failure meets +// retryCondition. +func retry(ctx context.Context, fn func() error) error { + backoff := wait.Backoff{ + Duration: 500 * time.Millisecond, + Factor: 1.5, + Steps: defaultRetryTimes, + } + + waitErr := wait.ExponentialBackoff(backoff, func() (bool, error) { + if err := fn(); err != nil { + log.Errorf(ctx, "Failed to cleanup (probably retrying): %v", err) + return false, nil + } + return true, nil + }) + + if waitErr != nil { + return errors.Wrap(waitErr, "wait on retry") + } + + return nil } diff --git a/internal/resourcestore/resourcecleaner_defaults.go b/internal/resourcestore/resourcecleaner_defaults.go new file mode 100644 index 00000000000..0b1c475abff --- /dev/null +++ b/internal/resourcestore/resourcecleaner_defaults.go @@ -0,0 +1,7 @@ +// +build !test + +package resourcestore + +// defaultRetryTimes defines the amount of default retries for each cleanup +// function. +var defaultRetryTimes = 20 diff --git a/internal/resourcestore/resourcecleaner_test.go b/internal/resourcestore/resourcecleaner_test.go new file mode 100644 index 00000000000..54048ad93d0 --- /dev/null +++ b/internal/resourcestore/resourcecleaner_test.go @@ -0,0 +1,105 @@ +package resourcestore_test + +import ( + "errors" + + "github.com/cri-o/cri-o/internal/resourcestore" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "golang.org/x/net/context" +) + +// The actual test suite +var _ = t.Describe("ResourceCleaner", func() { + It("should call the cleanup functions", func() { + // Given + sut := resourcestore.NewResourceCleaner() + called1 := false + called2 := false + sut.Add(context.Background(), "test1", func() error { + called1 = true + return nil + }) + sut.Add(context.Background(), "test2", func() error { + called2 = true + return nil + }) + + // When + err := sut.Cleanup() + + // Then + Expect(err).To(BeNil()) + Expect(called1).To(BeTrue()) + Expect(called2).To(BeTrue()) + }) + + It("should retry the cleanup functions", func() { + // Given + sut := resourcestore.NewResourceCleaner() + called1 := false + called2 := false + sut.Add(context.Background(), "test1", func() error { + called1 = true + return nil + }) + failureCnt := 0 + sut.Add(context.Background(), "test2", func() error { + if failureCnt == 2 { + called2 = true + return nil + } + failureCnt++ + return errors.New("") + }) + + // When + err := sut.Cleanup() + + // Then + Expect(err).To(BeNil()) + Expect(called1).To(BeTrue()) + Expect(called2).To(BeTrue()) + Expect(failureCnt).To(Equal(2)) + }) + + It("should retry three times", func() { + // Given + sut := resourcestore.NewResourceCleaner() + failureCnt := 0 + sut.Add(context.Background(), "test", func() error { + failureCnt++ + return errors.New("") + }) + + // When + err := sut.Cleanup() + + // Then + Expect(err).NotTo(BeNil()) + Expect(failureCnt).To(Equal(3)) + }) + + It("should run in parallel", func() { + // Given + sut := resourcestore.NewResourceCleaner() + testChan := make(chan bool, 1) + succ := false + sut.Add(context.Background(), "test1", func() error { + testChan <- true + return nil + }) + sut.Add(context.Background(), "test2", func() error { + <-testChan + succ = true + return nil + }) + + // When + err := sut.Cleanup() + + // Then + Expect(err).To(BeNil()) + Expect(succ).To(BeTrue()) + }) +}) diff --git a/internal/resourcestore/resourcecleaner_test_inject.go b/internal/resourcestore/resourcecleaner_test_inject.go new file mode 100644 index 00000000000..d1fd315b4e3 --- /dev/null +++ b/internal/resourcestore/resourcecleaner_test_inject.go @@ -0,0 +1,9 @@ +// +build test +// All *_inject.go files are meant to be used by tests only. Purpose of this +// files is to provide a way to inject mocked data into the current setup. + +package resourcestore + +// defaultRetryTimes reduces the amount of default retries for testing +// purposes. +var defaultRetryTimes = 3 diff --git a/internal/resourcestore/resourcestore.go b/internal/resourcestore/resourcestore.go index 94a55370209..d6a1d5371f2 100644 --- a/internal/resourcestore/resourcestore.go +++ b/internal/resourcestore/resourcestore.go @@ -114,7 +114,9 @@ func (rc *ResourceStore) cleanupStaleResources() { for _, r := range resourcesToReap { logrus.Infof("cleaning up stale resource %s", r.name) - r.cleaner.Cleanup() + if err := r.cleaner.Cleanup(); err != nil { + logrus.Errorf("Unable to cleanup: %v", err) + } } } } diff --git a/internal/resourcestore/resourcestore_test.go b/internal/resourcestore/resourcestore_test.go index 6722aa45863..3961944501c 100644 --- a/internal/resourcestore/resourcestore_test.go +++ b/internal/resourcestore/resourcestore_test.go @@ -6,6 +6,7 @@ import ( "github.com/cri-o/cri-o/internal/resourcestore" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "golang.org/x/net/context" ) var ( @@ -117,8 +118,9 @@ var _ = t.Describe("ResourceStore", func() { sut = resourcestore.NewWithTimeout(timeout) timedOutChan := make(chan bool) - cleaner.Add(func() { + cleaner.Add(context.Background(), "test", func() error { timedOutChan <- true + return nil }) go func() { time.Sleep(timeout * 3) diff --git a/internal/storage/runtime.go b/internal/storage/runtime.go index d40ec02e1f0..ec244ac7890 100644 --- a/internal/storage/runtime.go +++ b/internal/storage/runtime.go @@ -71,11 +71,6 @@ type RuntimeServer interface { // Pointer arguments can be nil. Either the image name or ID can be // omitted, but not both. All other arguments are required. CreatePodSandbox(systemContext *types.SystemContext, podName, podID, imageName, imageAuthFile, imageID, containerName, metadataName, uid, namespace string, attempt uint32, idMappingsOptions *storage.IDMappingOptions, labelOptions []string, privileged bool) (ContainerInfo, error) - // RemovePodSandbox deletes a pod sandbox's infrastructure container. - // The CRI expects that a sandbox can't be removed unless its only - // container is its infrastructure container, but we don't enforce that - // here, since we're just keeping track of it for higher level APIs. - RemovePodSandbox(idOrName string) error // GetContainerMetadata returns the metadata we've stored for a container. GetContainerMetadata(idOrName string) (RuntimeContainerMetadata, error) @@ -382,29 +377,6 @@ func (r *runtimeService) deleteLayerIfMapped(imageID, layerID string) { } } -func (r *runtimeService) RemovePodSandbox(idOrName string) error { - container, err := r.storageImageServer.GetStore().Container(idOrName) - if err != nil { - if errors.Is(err, storage.ErrContainerUnknown) { - return ErrInvalidSandboxID - } - return err - } - layer, err := r.storageImageServer.GetStore().Layer(container.LayerID) - if err != nil { - logrus.Debugf("failed to retrieve layer %q: %v", container.LayerID, err) - } - err = r.storageImageServer.GetStore().DeleteContainer(container.ID) - if err != nil { - logrus.Debugf("failed to delete pod sandbox %q: %v", container.ID, err) - return err - } - if layer != nil { - r.deleteLayerIfMapped(container.ImageID, layer.Parent) - } - return nil -} - func (r *runtimeService) DeleteContainer(idOrName string) error { if idOrName == "" { return ErrInvalidContainerID diff --git a/internal/storage/runtime_test.go b/internal/storage/runtime_test.go index d4122c4d74b..6665732751b 100644 --- a/internal/storage/runtime_test.go +++ b/internal/storage/runtime_test.go @@ -467,79 +467,6 @@ var _ = t.Describe("Runtime", func() { }) }) - t.Describe("RemovePodSandbox", func() { - It("should succeed to remove the pod sandbox", func() { - // Given - gomock.InOrder( - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Container(gomock.Any()). - Return(&cs.Container{}, nil), - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Layer("").Return(nil, nil), - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().DeleteContainer(gomock.Any()). - Return(nil), - ) - - // When - err := sut.RemovePodSandbox("id") - - // Then - Expect(err).To(BeNil()) - }) - - It("should fail to remove the pod sandbox on store error", func() { - // Given - gomock.InOrder( - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Container(gomock.Any()). - Return(nil, t.TestError), - ) - - // When - err := sut.RemovePodSandbox("id") - - // Then - Expect(err).NotTo(BeNil()) - }) - - It("should fail to remove the pod sandbox on invalid sandbox ID", func() { - // Given - gomock.InOrder( - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Container(gomock.Any()). - Return(nil, cs.ErrContainerUnknown), - ) - - // When - err := sut.RemovePodSandbox("id") - - // Then - Expect(err).NotTo(BeNil()) - Expect(err).To(Equal(storage.ErrInvalidSandboxID)) - }) - - It("should fail to remove the pod sandbox on deletion error", func() { - // Given - gomock.InOrder( - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Container(gomock.Any()). - Return(&cs.Container{}, nil), - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().Layer("").Return(nil, nil), - imageServerMock.EXPECT().GetStore().Return(storeMock), - storeMock.EXPECT().DeleteContainer(gomock.Any()). - Return(t.TestError), - ) - - // When - err := sut.RemovePodSandbox("id") - - // Then - Expect(err).NotTo(BeNil()) - }) - }) - t.Describe("CreateContainer/CreatePodSandbox", func() { t.Describe("success", func() { var ( diff --git a/pkg/config/config.go b/pkg/config/config.go index 688f38311ab..86ccaad2f7e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -152,6 +152,10 @@ type RootConfig struct { // CleanShutdownFile is the location CRI-O will lay down the clean shutdown file // that checks whether we've had time to sync before shutting down CleanShutdownFile string `toml:"clean_shutdown_file"` + + // InternalWipe is whether CRI-O should wipe containers and images after a reboot when the server starts. + // If set to false, one must use the external command `crio wipe` to wipe the containers and images in these situations. + InternalWipe bool `toml:"internal_wipe"` } // RuntimeHandler represents each item of the "crio.runtime.runtimes" TOML diff --git a/pkg/config/template.go b/pkg/config/template.go index e426c05c23e..dc572c593c5 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -125,6 +125,11 @@ func initCrioTemplateConfig(c *Config) ([]*templateConfigValue, error) { group: crioRootConfig, isDefaultValue: simpleEqual(dc.VersionFilePersist, c.VersionFilePersist), }, + { + templateString: templateStringCrioInternalWipe, + group: crioRootConfig, + isDefaultValue: simpleEqual(dc.InternalWipe, c.InternalWipe), + }, { templateString: templateStringCrioCleanShutdownFile, group: crioRootConfig, @@ -587,6 +592,12 @@ clean_shutdown_file = "{{ .CleanShutdownFile }}" ` +const templateStringCrioInternalWipe = `# InternalWipe is whether CRI-O should wipe containers and images after a reboot when the server starts. +# If set to false, one must use the external command 'crio wipe' to wipe the containers and images in these situations. +internal_wipe = {{ .InternalWipe }} + +` + const templateStringCrioAPI = `# The crio.api table contains settings for the kubelet/gRPC interface. [crio.api] diff --git a/server/container_create.go b/server/container_create.go index 13e5b727475..3b65d492673 100644 --- a/server/container_create.go +++ b/server/container_create.go @@ -432,7 +432,9 @@ func (s *Server) CreateContainer(ctx context.Context, req *types.CreateContainer if retErr == nil || isContextError(retErr) { return } - resourceCleaner.Cleanup() + if err := resourceCleaner.Cleanup(); err != nil { + log.Errorf(ctx, "Unable to cleanup: %v", err) + } }() if _, err = s.ReserveContainerName(ctr.ID(), ctr.Name()); err != nil { @@ -443,37 +445,46 @@ func (s *Server) CreateContainer(ctx context.Context, req *types.CreateContainer return nil, errors.Wrapf(err, resourceErr.Error()) } - resourceCleaner.Add(func() { - log.Infof(ctx, "createCtr: releasing container name %s", ctr.Name()) + description := fmt.Sprintf("createCtr: releasing container name %s", ctr.Name()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) s.ReleaseContainerName(ctr.Name()) + return nil }) newContainer, err := s.createSandboxContainer(ctx, ctr, sb) if err != nil { return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "createCtr: deleting container %s from storage", ctr.ID()) + description = fmt.Sprintf("createCtr: deleting container %s from storage", ctr.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) err2 := s.StorageRuntimeServer().DeleteContainer(ctr.ID()) if err2 != nil { log.Warnf(ctx, "Failed to cleanup container storage: %v", err2) } + return err2 }) s.addContainer(newContainer) - resourceCleaner.Add(func() { - log.Infof(ctx, "createCtr: removing container %s", newContainer.ID()) + description = fmt.Sprintf("createCtr: removing container %s", newContainer.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) s.removeContainer(newContainer) + return nil }) if err := s.CtrIDIndex().Add(ctr.ID()); err != nil { return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "createCtr: deleting container ID %s from idIndex", ctr.ID()) - if err := s.CtrIDIndex().Delete(ctr.ID()); err != nil { + description = fmt.Sprintf("createCtr: deleting container ID %s from idIndex", ctr.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err := s.CtrIDIndex().Delete(ctr.ID()) + if err != nil { log.Warnf(ctx, "couldn't delete ctr id %s from idIndex", ctr.ID()) } + return err }) mappings, err := s.getSandboxIDMappings(sb) @@ -484,13 +495,16 @@ func (s *Server) CreateContainer(ctx context.Context, req *types.CreateContainer if err := s.createContainerPlatform(ctx, newContainer, sb.CgroupParent(), mappings); err != nil { return nil, err } - resourceCleaner.Add(func() { + description = fmt.Sprintf("createCtr: removing container ID %s from runtime", ctr.ID()) + resourceCleaner.Add(ctx, description, func() error { if retErr != nil { - log.Infof(ctx, "createCtr: removing container ID %s from runtime", ctr.ID()) + log.Infof(ctx, description) if err := s.Runtime().DeleteContainer(ctx, newContainer); err != nil { log.Warnf(ctx, "failed to delete container in runtime %s: %v", ctr.ID(), err) + return err } } + return nil }) if err := s.ContainerStateToDisk(ctx, newContainer); err != nil { diff --git a/server/container_stop.go b/server/container_stop.go index 8a780c9f85c..4c4d551c63a 100644 --- a/server/container_stop.go +++ b/server/container_stop.go @@ -14,7 +14,6 @@ import ( // StopContainer stops a running container with a grace period (i.e., timeout). func (s *Server) StopContainer(ctx context.Context, req *types.StopContainerRequest) error { log.Infof(ctx, "Stopping container: %s (timeout: %ds)", req.ContainerID, req.Timeout) - // save container description to print c, err := s.GetContainerFromShortID(req.ContainerID) if err != nil { return status.Errorf(codes.NotFound, "could not find container %q: %v", req.ContainerID, err) @@ -32,8 +31,7 @@ func (s *Server) StopContainer(ctx context.Context, req *types.StopContainerRequ } } - _, err = s.ContainerServer.ContainerStop(ctx, req.ContainerID, req.Timeout) - if err != nil { + if err := s.ContainerServer.StopContainer(ctx, c, req.Timeout); err != nil { return err } diff --git a/server/image_remove.go b/server/image_remove.go index 4271b87037e..b6df0968836 100644 --- a/server/image_remove.go +++ b/server/image_remove.go @@ -11,19 +11,23 @@ import ( // RemoveImage removes the image. func (s *Server) RemoveImage(ctx context.Context, req *types.RemoveImageRequest) error { - image := "" + imageRef := "" img := req.Image if img != nil { - image = img.Image + imageRef = img.Image } - if image == "" { + if imageRef == "" { return fmt.Errorf("no image specified") } + return s.removeImage(ctx, imageRef) +} + +func (s *Server) removeImage(ctx context.Context, imageRef string) error { var deleted bool - images, err := s.StorageImageServer().ResolveNames(s.config.SystemContext, image) + images, err := s.StorageImageServer().ResolveNames(s.config.SystemContext, imageRef) if err != nil { if err == storage.ErrCannotParseImageID { - images = append(images, image) + images = append(images, imageRef) } else { return err } diff --git a/server/sandbox_network.go b/server/sandbox_network.go index 888cfa8fc02..bc1ea3dece9 100644 --- a/server/sandbox_network.go +++ b/server/sandbox_network.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "math" "time" cnitypes "github.com/containernetworking/cni/pkg/types" @@ -11,7 +12,9 @@ import ( "github.com/cri-o/cri-o/internal/lib/sandbox" "github.com/cri-o/cri-o/internal/log" "github.com/cri-o/cri-o/server/metrics" + "github.com/cri-o/ocicni/pkg/ocicni" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/resource" utilnet "k8s.io/utils/net" ) @@ -181,3 +184,50 @@ func (s *Server) networkStop(ctx context.Context, sb *sandbox.Sandbox) error { return sb.SetNetworkStopped(true) } + +func (s *Server) newPodNetwork(sb *sandbox.Sandbox) (ocicni.PodNetwork, error) { + var egress, ingress int64 + + if val, ok := sb.Annotations()["kubernetes.io/egress-bandwidth"]; ok { + egressQ, err := resource.ParseQuantity(val) + if err != nil { + return ocicni.PodNetwork{}, fmt.Errorf("failed to parse egress bandwidth: %v", err) + } else if iegress, isok := egressQ.AsInt64(); isok { + egress = iegress + } + } + if val, ok := sb.Annotations()["kubernetes.io/ingress-bandwidth"]; ok { + ingressQ, err := resource.ParseQuantity(val) + if err != nil { + return ocicni.PodNetwork{}, fmt.Errorf("failed to parse ingress bandwidth: %v", err) + } else if iingress, isok := ingressQ.AsInt64(); isok { + ingress = iingress + } + } + + var bwConfig *ocicni.BandwidthConfig + + if ingress > 0 || egress > 0 { + bwConfig = &ocicni.BandwidthConfig{} + if ingress > 0 { + bwConfig.IngressRate = uint64(ingress) + bwConfig.IngressBurst = math.MaxUint32*8 - 1 // 4GB burst limit + } + if egress > 0 { + bwConfig.EgressRate = uint64(egress) + bwConfig.EgressBurst = math.MaxUint32*8 - 1 // 4GB burst limit + } + } + + network := s.config.CNIPlugin().GetDefaultNetworkName() + return ocicni.PodNetwork{ + Name: sb.KubeName(), + Namespace: sb.Namespace(), + Networks: []ocicni.NetAttachment{}, + ID: sb.ID(), + NetNS: sb.NetNsPath(), + RuntimeConfig: map[string]ocicni.RuntimeConfig{ + network: {Bandwidth: bwConfig}, + }, + }, nil +} diff --git a/server/sandbox_remove.go b/server/sandbox_remove.go index 15f4f2066a1..1fa3ec6762f 100644 --- a/server/sandbox_remove.go +++ b/server/sandbox_remove.go @@ -7,7 +7,6 @@ import ( "github.com/cri-o/cri-o/internal/lib/sandbox" "github.com/cri-o/cri-o/internal/log" oci "github.com/cri-o/cri-o/internal/oci" - pkgstorage "github.com/cri-o/cri-o/internal/storage" "github.com/cri-o/cri-o/server/cri/types" "github.com/pkg/errors" "golang.org/x/net/context" @@ -32,59 +31,16 @@ func (s *Server) RemovePodSandbox(ctx context.Context, req *types.RemovePodSandb log.Warnf(ctx, "could not get sandbox %s, it's probably been removed already: %v", req.PodSandboxID, err) return nil } + return s.removePodSandbox(ctx, sb) +} - podInfraContainer := sb.InfraContainer() +func (s *Server) removePodSandbox(ctx context.Context, sb *sandbox.Sandbox) error { containers := sb.Containers().List() - containers = append(containers, podInfraContainer) // Delete all the containers in the sandbox for _, c := range containers { - if !sb.Stopped() { - cState := c.State() - if cState.Status == oci.ContainerStateCreated || cState.Status == oci.ContainerStateRunning { - timeout := int64(10) - if err := s.Runtime().StopContainer(ctx, c, timeout); err != nil { - // Assume container is already stopped - log.Warnf(ctx, "failed to stop container %s: %v", c.Name(), err) - } - if err := s.Runtime().WaitContainerStateStopped(ctx, c); err != nil { - return fmt.Errorf("failed to get container 'stopped' status %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) - } - } - } - - if err := s.Runtime().DeleteContainer(ctx, c); err != nil { - return fmt.Errorf("failed to delete container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) - } - - if c.ID() == podInfraContainer.ID() { - continue - } - - c.CleanupConmonCgroup() - - if err := s.StorageRuntimeServer().StopContainer(c.ID()); err != nil && err != storage.ErrContainerUnknown { - // assume container already umounted - log.Warnf(ctx, "failed to stop container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) - } - if err := s.StorageRuntimeServer().DeleteContainer(c.ID()); err != nil && err != storage.ErrContainerUnknown { - return fmt.Errorf("failed to delete container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) - } - - s.ReleaseContainerName(c.Name()) - s.removeContainer(c) - if err := s.CtrIDIndex().Delete(c.ID()); err != nil { - return fmt.Errorf("failed to delete container %s in pod sandbox %s from index: %v", c.Name(), sb.ID(), err) - } - } - - s.removeInfraContainer(podInfraContainer) - podInfraContainer.CleanupConmonCgroup() - - // StorageRuntimeServer won't know about this container, as it wasn't created in storage - if !podInfraContainer.Spoofed() { - if err := s.StorageRuntimeServer().StopContainer(sb.ID()); err != nil && !errors.Is(err, storage.ErrContainerUnknown) { - log.Warnf(ctx, "failed to stop sandbox container in pod sandbox %s: %v", sb.ID(), err) + if err := s.removeContainerInPod(ctx, sb, c); err != nil { + return err } } @@ -92,16 +48,18 @@ func (s *Server) RemovePodSandbox(ctx context.Context, req *types.RemovePodSandb return errors.Wrap(err, "unable to unmount SHM") } - if err := s.StorageRuntimeServer().RemovePodSandbox(sb.ID()); err != nil && err != pkgstorage.ErrInvalidSandboxID { - return fmt.Errorf("failed to remove pod sandbox %s: %v", sb.ID(), err) + s.removeInfraContainer(sb.InfraContainer()) + if err := s.removeContainerInPod(ctx, sb, sb.InfraContainer()); err != nil { + return err } - if err := sb.RemoveManagedNamespaces(); err != nil { - return errors.Wrap(err, "unable to remove managed namespaces") + + // Cleanup network resources for this pod + if err := s.networkStop(ctx, sb); err != nil { + return errors.Wrap(err, "stop pod network") } - s.ReleaseContainerName(podInfraContainer.Name()) - if err := s.CtrIDIndex().Delete(podInfraContainer.ID()); err != nil { - return fmt.Errorf("failed to delete infra container %s in pod sandbox %s from index: %v", podInfraContainer.ID(), sb.ID(), err) + if err := sb.RemoveManagedNamespaces(); err != nil { + return errors.Wrap(err, "unable to remove managed namespaces") } s.ReleasePodName(sb.Name()) @@ -115,3 +73,36 @@ func (s *Server) RemovePodSandbox(ctx context.Context, req *types.RemovePodSandb log.Infof(ctx, "Removed pod sandbox: %s", sb.ID()) return nil } + +func (s *Server) removeContainerInPod(ctx context.Context, sb *sandbox.Sandbox, c *oci.Container) error { + if !sb.Stopped() { + if err := s.ContainerServer.StopContainer(ctx, c, int64(10)); err != nil { + return errors.Errorf("failed to stop container for removal") + } + } + + if err := s.Runtime().DeleteContainer(ctx, c); err != nil { + return fmt.Errorf("failed to delete container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) + } + + c.CleanupConmonCgroup() + + if !c.Spoofed() { + if err := s.StorageRuntimeServer().StopContainer(c.ID()); err != nil && err != storage.ErrContainerUnknown { + // assume container already umounted + log.Warnf(ctx, "failed to stop container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) + } + if err := s.StorageRuntimeServer().DeleteContainer(c.ID()); err != nil && err != storage.ErrContainerUnknown { + return fmt.Errorf("failed to delete container %s in pod sandbox %s: %v", c.Name(), sb.ID(), err) + } + } + + s.ReleaseContainerName(c.Name()) + s.removeContainer(c) + if err := s.CtrIDIndex().Delete(c.ID()); err != nil { + return fmt.Errorf("failed to delete container %s in pod sandbox %s from index: %v", c.Name(), sb.ID(), err) + } + sb.RemoveContainer(c) + + return nil +} diff --git a/server/sandbox_run_linux.go b/server/sandbox_run_linux.go index 7c6d8a3c62b..5b9bb54da0e 100644 --- a/server/sandbox_run_linux.go +++ b/server/sandbox_run_linux.go @@ -297,7 +297,9 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ if retErr == nil || isContextError(retErr) { return } - resourceCleaner.Cleanup() + if err := resourceCleaner.Cleanup(); err != nil { + log.Errorf(ctx, "Unable to cleanup: %v", err) + } }() if _, err := s.ReservePodName(sbox.ID(), sbox.Name()); err != nil { @@ -308,9 +310,11 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ return nil, errors.Wrapf(err, resourceErr.Error()) } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: releasing pod sandbox name: %s", sbox.Name()) + description := fmt.Sprintf("runSandbox: releasing pod sandbox name: %s", sbox.Name()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) s.ReleasePodName(sbox.Name()) + return nil }) // validate the runtime handler @@ -336,9 +340,11 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ if err != nil { return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: releasing container name: %s", containerName) + description = fmt.Sprintf("runSandbox: releasing container name: %s", containerName) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) s.ReleaseContainerName(containerName) + return nil }) var labelOptions []string @@ -374,11 +380,14 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ if err != nil { return nil, fmt.Errorf("error creating pod sandbox with name %q: %v", sbox.Name(), err) } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: removing pod sandbox from storage: %s", sbox.ID()) - if err2 := s.StorageRuntimeServer().RemovePodSandbox(sbox.ID()); err2 != nil { + description = fmt.Sprintf("runSandbox: removing pod sandbox from storage: %s", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err2 := s.StorageRuntimeServer().DeleteContainer(sbox.ID()) + if err2 != nil { log.Warnf(ctx, "could not cleanup pod sandbox %q: %v", sbox.ID(), err2) } + return err2 }) // set log directory @@ -511,11 +520,14 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ return nil, err } pathsToChown = append(pathsToChown, shmPath) - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: unmounting shmPath for sandbox %s", sbox.ID()) - if err2 := unix.Unmount(shmPath, unix.MNT_DETACH); err2 != nil { + description = fmt.Sprintf("runSandbox: unmounting shmPath for sandbox %s", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err2 := unix.Unmount(shmPath, unix.MNT_DETACH) + if err2 != nil { log.Warnf(ctx, "failed to unmount shm for sandbox: %v", err2) } + return err2 }) } @@ -537,11 +549,14 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: deleting container ID from idIndex for sandbox %s", sbox.ID()) - if err2 := s.CtrIDIndex().Delete(sbox.ID()); err2 != nil { + description = fmt.Sprintf("runSandbox: deleting container ID from idIndex for sandbox %s", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err2 := s.CtrIDIndex().Delete(sbox.ID()) + if err2 != nil { log.Warnf(ctx, "could not delete ctr id %s from idIndex", sbox.ID()) } + return err2 }) // set log path inside log directory @@ -634,22 +649,28 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ if err := s.addSandbox(sb); err != nil { return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: removing pod sandbox %s", sbox.ID()) - if err := s.removeSandbox(sbox.ID()); err != nil { + description = fmt.Sprintf("runSandbox: removing pod sandbox %s", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err := s.removeSandbox(sbox.ID()) + if err != nil { log.Warnf(ctx, "could not remove pod sandbox: %v", err) } + return err }) if err := s.PodIDIndex().Add(sbox.ID()); err != nil { return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: deleting pod ID %s from idIndex", sbox.ID()) - if err := s.PodIDIndex().Delete(sbox.ID()); err != nil { + description = fmt.Sprintf("runSandbox: deleting pod ID %s from idIndex", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err := s.PodIDIndex().Delete(sbox.ID()) + if err != nil { log.Warnf(ctx, "could not delete pod id %s from idIndex", sbox.ID()) } + return err }) for k, v := range kubeAnnotations { @@ -665,15 +686,19 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ // set up namespaces nsCleanupFuncs, err := s.configureGeneratorForSandboxNamespaces(hostNetwork, hostIPC, hostPID, sandboxIDMappings, sysctls, sb, g) // We want to cleanup after ourselves if we are managing any namespaces and fail in this function. - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: cleaning up namespaces after failing to run sandbox %s", sbox.ID()) + nsCleanupDescription := fmt.Sprintf("runSandbox: cleaning up namespaces after failing to run sandbox %s", sbox.ID()) + nsCleanupFunc := func() error { + log.Infof(ctx, description) for idx := range nsCleanupFuncs { if err2 := nsCleanupFuncs[idx](); err2 != nil { log.Infof(ctx, "runSandbox: failed to cleanup namespace: %s", err2.Error()) + return err2 } } - }) + return nil + } if err != nil { + resourceCleaner.Add(ctx, nsCleanupDescription, nsCleanupFunc) return nil, err } @@ -683,14 +708,21 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ ips, result, err = s.networkStart(ctx, sb) if err != nil { + resourceCleaner.Add(ctx, nsCleanupDescription, nsCleanupFunc) return nil, err } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: stopping network for sandbox %s", sb.ID()) + description = fmt.Sprintf("runSandbox: stopping network for sandbox %s", sb.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) // use a new context to prevent an expired context from preventing a stop - if err2 := s.networkStop(context.Background(), sb); err2 != nil { - log.Errorf(ctx, "error stopping network on cleanup: %v", err2) + if err := s.networkStop(context.Background(), sb); err != nil { + log.Errorf(ctx, "error stopping network on cleanup: %v", err) + return err } + + // Now that we've succeeded in stopping the network, cleanup namespaces + log.Infof(ctx, nsCleanupDescription) + return nsCleanupFunc() }) if result != nil { resultCurrent, err := current.NewResultFromResult(result) @@ -721,11 +753,14 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ if err != nil { return nil, fmt.Errorf("failed to mount container %s in pod sandbox %s(%s): %v", containerName, sb.Name(), sbox.ID(), err) } - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: stopping storage container for sandbox %s", sbox.ID()) - if err2 := s.StorageRuntimeServer().StopContainer(sbox.ID()); err2 != nil { + description = fmt.Sprintf("runSandbox: stopping storage container for sandbox %s", sbox.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) + err2 := s.StorageRuntimeServer().StopContainer(sbox.ID()) + if err2 != nil { log.Warnf(ctx, "could not stop storage container: %v: %v", sbox.ID(), err2) } + return err2 }) g.AddAnnotation(annotations.MountPoint, mountPoint) @@ -859,9 +894,11 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ } s.addInfraContainer(container) - resourceCleaner.Add(func() { - log.Infof(ctx, "runSandbox: removing infra container %s", container.ID()) + description = fmt.Sprintf("runSandbox: removing infra container %s", container.ID()) + resourceCleaner.Add(ctx, description, func() error { + log.Infof(ctx, description) s.removeInfraContainer(container) + return nil }) if sandboxIDMappings != nil { @@ -884,23 +921,25 @@ func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequ return nil, err } - resourceCleaner.Add(func() { + description = fmt.Sprintf("runSandbox: stopping container %s", container.ID()) + resourceCleaner.Add(ctx, description, func() error { // Clean-up steps from RemovePodSanbox - log.Infof(ctx, "runSandbox: stopping container %s", container.ID()) - if err2 := s.Runtime().StopContainer(ctx, container, int64(10)); err2 != nil { - log.Warnf(ctx, "failed to stop container %s: %v", container.Name(), err2) - } - if err2 := s.Runtime().WaitContainerStateStopped(ctx, container); err2 != nil { - log.Warnf(ctx, "failed to get container 'stopped' status %s in pod sandbox %s: %v", container.Name(), sb.ID(), err2) + log.Infof(ctx, description) + if err := s.ContainerServer.StopContainer(ctx, container, int64(10)); err != nil { + return errors.Errorf("failed to stop container for removal") } + log.Infof(ctx, "runSandbox: deleting container %s", container.ID()) if err2 := s.Runtime().DeleteContainer(ctx, container); err2 != nil { log.Warnf(ctx, "failed to delete container %s in pod sandbox %s: %v", container.Name(), sb.ID(), err2) + return err2 } log.Infof(ctx, "runSandbox: writing container %s state to disk", container.ID()) if err2 := s.ContainerStateToDisk(ctx, container); err2 != nil { log.Warnf(ctx, "failed to write container state %s in pod sandbox %s: %v", container.Name(), sb.ID(), err2) + return err2 } + return nil }) if err := s.ContainerStateToDisk(ctx, container); err != nil { diff --git a/server/sandbox_run_test.go b/server/sandbox_run_test.go index 6078bf9cb40..37b008dfbf2 100644 --- a/server/sandbox_run_test.go +++ b/server/sandbox_run_test.go @@ -40,7 +40,7 @@ var _ = t.Describe("RunPodSandbox", func() { Return(storage.RuntimeContainerMetadata{}, nil), runtimeServerMock.EXPECT().SetContainerMetadata(gomock.Any(), gomock.Any()).Return(nil), - runtimeServerMock.EXPECT().RemovePodSandbox(gomock.Any()). + runtimeServerMock.EXPECT().DeleteContainer(gomock.Any()). Return(nil), ) @@ -115,7 +115,7 @@ var _ = t.Describe("RunPodSandbox", func() { gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(storage.ContainerInfo{}, nil), - runtimeServerMock.EXPECT().RemovePodSandbox(gomock.Any()). + runtimeServerMock.EXPECT().DeleteContainer(gomock.Any()). Return(nil), ) diff --git a/server/sandbox_stop.go b/server/sandbox_stop.go index ba1d2262416..cd813c2bbfb 100644 --- a/server/sandbox_stop.go +++ b/server/sandbox_stop.go @@ -1,6 +1,9 @@ package server import ( + "fmt" + + "github.com/cri-o/cri-o/internal/lib/sandbox" "github.com/cri-o/cri-o/internal/log" "github.com/cri-o/cri-o/server/cri/types" "golang.org/x/net/context" @@ -10,17 +13,32 @@ import ( // sandbox, they should be force terminated. func (s *Server) StopPodSandbox(ctx context.Context, req *types.StopPodSandboxRequest) error { // platform dependent call - return s.stopPodSandbox(ctx, req) + log.Infof(ctx, "Stopping pod sandbox: %s", req.PodSandboxID) + sb, err := s.getPodSandboxFromRequest(req.PodSandboxID) + if err != nil { + if err == sandbox.ErrIDEmpty { + return err + } + if err == errSandboxNotCreated { + return fmt.Errorf("StopPodSandbox failed as the sandbox is not created: %s", sb.ID()) + } + + // If the sandbox isn't found we just return an empty response to adhere + // the CRI interface which expects to not error out in not found + // cases. + + log.Warnf(ctx, "could not get sandbox %s, it's probably been stopped already: %v", req.PodSandboxID, err) + log.Debugf(ctx, "StopPodSandboxResponse %s", req.PodSandboxID) + return nil + } + return s.stopPodSandbox(ctx, sb) } // stopAllPodSandboxes removes all pod sandboxes func (s *Server) stopAllPodSandboxes(ctx context.Context) { log.Debugf(ctx, "stopAllPodSandboxes") for _, sb := range s.ContainerServer.ListSandboxes() { - pod := &types.StopPodSandboxRequest{ - PodSandboxID: sb.ID(), - } - if err := s.StopPodSandbox(ctx, pod); err != nil { + if err := s.stopPodSandbox(ctx, sb); err != nil { log.Warnf(ctx, "could not StopPodSandbox %s: %v", sb.ID(), err) } } diff --git a/server/sandbox_stop_linux.go b/server/sandbox_stop_linux.go index af9ac4975ac..ce2674f2af0 100644 --- a/server/sandbox_stop_linux.go +++ b/server/sandbox_stop_linux.go @@ -10,31 +10,12 @@ import ( "github.com/cri-o/cri-o/internal/log" oci "github.com/cri-o/cri-o/internal/oci" "github.com/cri-o/cri-o/internal/runtimehandlerhooks" - "github.com/cri-o/cri-o/server/cri/types" "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/sync/errgroup" ) -func (s *Server) stopPodSandbox(ctx context.Context, req *types.StopPodSandboxRequest) error { - log.Infof(ctx, "Stopping pod sandbox: %s", req.PodSandboxID) - sb, err := s.getPodSandboxFromRequest(req.PodSandboxID) - if err != nil { - if err == sandbox.ErrIDEmpty { - return err - } - if err == errSandboxNotCreated { - return fmt.Errorf("StopPodSandbox failed as the sandbox is not created: %s", sb.ID()) - } - - // If the sandbox isn't found we just return an empty response to adhere - // the CRI interface which expects to not error out in not found - // cases. - - log.Warnf(ctx, "could not get sandbox %s, it's probably been stopped already: %v", req.PodSandboxID, err) - log.Debugf(ctx, "StopPodSandboxResponse %s", req.PodSandboxID) - return nil - } +func (s *Server) stopPodSandbox(ctx context.Context, sb *sandbox.Sandbox) error { stopMutex := sb.StopMutex() stopMutex.Lock() defer stopMutex.Unlock() @@ -44,17 +25,17 @@ func (s *Server) stopPodSandbox(ctx context.Context, req *types.StopPodSandboxRe return err } + if sb.Stopped() { + log.Infof(ctx, "Stopped pod sandbox (already stopped): %s", sb.ID()) + return nil + } + // Get high-performance runtime hook to trigger preStop step for each container hooks, err := runtimehandlerhooks.GetRuntimeHandlerHooks(ctx, &s.config, sb.RuntimeHandler(), sb.Annotations()) if err != nil { return fmt.Errorf("failed to get runtime handler %q hooks", sb.RuntimeHandler()) } - if sb.Stopped() { - log.Infof(ctx, "Stopped pod sandbox (already stopped): %s", sb.ID()) - return nil - } - podInfraContainer := sb.InfraContainer() containers := sb.Containers().List() containers = append(containers, podInfraContainer) diff --git a/server/server.go b/server/server.go index 75a94e3f9c8..b43313339b1 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/cri-o/cri-o/internal/resourcestore" "github.com/cri-o/cri-o/internal/runtimehandlerhooks" "github.com/cri-o/cri-o/internal/storage" + "github.com/cri-o/cri-o/internal/version" libconfig "github.com/cri-o/cri-o/pkg/config" "github.com/cri-o/cri-o/server/cri/types" "github.com/cri-o/cri-o/server/metrics" @@ -421,6 +422,8 @@ func New( s.restore(ctx) s.cleanupSandboxesOnShutdown(ctx) + s.wipeIfAppropriate(ctx) + var bindAddressStr string bindAddress := net.ParseIP(config.StreamAddress) if bindAddress != nil { @@ -489,6 +492,78 @@ func New( return s, nil } +func (s *Server) wipeIfAppropriate(ctx context.Context) { + if !s.config.InternalWipe { + return + } + // First, check if the node was rebooted. + // We know this happened because the VersionFile (which lives in a tmpfs) + // will not be there. + shouldWipeContainers, err := version.ShouldCrioWipe(s.config.VersionFile) + if err != nil { + log.Warnf(ctx, "error encountered when checking whether cri-o should wipe containers: %v", err) + } + + // there are two locations we check before wiping: + // one in a temporary directory. This is to check whether the node has rebooted. + // if so, we should remove containers + // another is needed in a persistent directory. This is to check whether we've upgraded + // if we've upgraded, we should wipe images + shouldWipeImages, err := version.ShouldCrioWipe(s.config.VersionFilePersist) + if err != nil { + log.Warnf(ctx, "error encountered when checking whether cri-o should wipe images: %v", err) + } + + shouldWipeContainers = shouldWipeContainers || shouldWipeImages + + // First, save the images we should be wiping + // We won't remember if we wipe all the containers first + var imagesToWipe []string + if shouldWipeImages { + containers, err := s.ContainerServer.ListContainers() + if err != nil { + log.Warnf(ctx, "Failed to list containers: %v", err) + } + for _, c := range containers { + imagesToWipe = append(imagesToWipe, c.ImageRef()) + } + } + + wipeResourceCleaner := resourcestore.NewResourceCleaner() + if shouldWipeContainers { + for _, sb := range s.ContainerServer.ListSandboxes() { + sb := sb + cleanupFunc := func() error { + if err := s.stopPodSandbox(ctx, sb); err != nil { + return err + } + return s.removePodSandbox(ctx, sb) + } + if err := cleanupFunc(); err != nil { + log.Warnf(ctx, "Failed to cleanup pod %s (will retry): %v", sb.ID(), err) + wipeResourceCleaner.Add(ctx, "stop and remove pod sandbox", cleanupFunc) + } + } + } + + go func() { + if err := wipeResourceCleaner.Cleanup(); err != nil { + log.Errorf(ctx, "Cleanup during server startup failed: %v", err) + } + }() + + // Note: some of these will fail if some aspect of the pod cleanup failed as well, + // but this is best-effort anyway, as the Kubelet will eventually cleanup images when + // disk usage gets too high. + if shouldWipeImages { + for _, img := range imagesToWipe { + if err := s.removeImage(ctx, img); err != nil { + log.Warnf(ctx, "failed to remove image %s: %v", img, err) + } + } + } +} + func (s *Server) addSandbox(sb *sandbox.Sandbox) error { return s.ContainerServer.AddSandbox(sb) } diff --git a/server/utils.go b/server/utils.go index 4c8f2662854..9a5e8cd38aa 100644 --- a/server/utils.go +++ b/server/utils.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "io/ioutil" - "math" "os" "path/filepath" "strings" @@ -15,16 +14,13 @@ import ( encconfig "github.com/containers/ocicrypt/config" cryptUtils "github.com/containers/ocicrypt/utils" "github.com/containers/storage/pkg/mount" - "github.com/cri-o/cri-o/internal/lib/sandbox" "github.com/cri-o/cri-o/internal/log" "github.com/cri-o/cri-o/server/cri/types" - "github.com/cri-o/ocicni/pkg/ocicni" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-tools/validate" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/syndtr/gocapability/capability" - "k8s.io/apimachinery/pkg/api/resource" kubeletTypes "k8s.io/kubernetes/pkg/kubelet/types" ) @@ -107,53 +103,6 @@ func parseDNSOptions(servers, searches, options []string, path string) error { return nil } -func (s *Server) newPodNetwork(sb *sandbox.Sandbox) (ocicni.PodNetwork, error) { - var egress, ingress int64 = 0, 0 - - if val, ok := sb.Annotations()["kubernetes.io/egress-bandwidth"]; ok { - egressQ, err := resource.ParseQuantity(val) - if err != nil { - return ocicni.PodNetwork{}, fmt.Errorf("failed to parse egress bandwidth: %v", err) - } else if iegress, isok := egressQ.AsInt64(); isok { - egress = iegress - } - } - if val, ok := sb.Annotations()["kubernetes.io/ingress-bandwidth"]; ok { - ingressQ, err := resource.ParseQuantity(val) - if err != nil { - return ocicni.PodNetwork{}, fmt.Errorf("failed to parse ingress bandwidth: %v", err) - } else if iingress, isok := ingressQ.AsInt64(); isok { - ingress = iingress - } - } - - var bwConfig *ocicni.BandwidthConfig - - if ingress > 0 || egress > 0 { - bwConfig = &ocicni.BandwidthConfig{} - if ingress > 0 { - bwConfig.IngressRate = uint64(ingress) - bwConfig.IngressBurst = math.MaxUint32*8 - 1 // 4GB burst limit - } - if egress > 0 { - bwConfig.EgressRate = uint64(egress) - bwConfig.EgressBurst = math.MaxUint32*8 - 1 // 4GB burst limit - } - } - - network := s.config.CNIPlugin().GetDefaultNetworkName() - return ocicni.PodNetwork{ - Name: sb.KubeName(), - Namespace: sb.Namespace(), - Networks: []ocicni.NetAttachment{}, - ID: sb.ID(), - NetNS: sb.NetNsPath(), - RuntimeConfig: map[string]ocicni.RuntimeConfig{ - network: {Bandwidth: bwConfig}, - }, - }, nil -} - // inStringSlice checks whether a string is inside a string slice. // Comparison is case insensitive. func inStringSlice(ss []string, str string) bool { diff --git a/test/crio-wipe.bats b/test/crio-wipe.bats index f96175c62c9..6a317ca9370 100644 --- a/test/crio-wipe.bats +++ b/test/crio-wipe.bats @@ -48,20 +48,25 @@ function test_crio_wiped_images() { # check that the pause image was removed, as we removed a pod # that used it output=$(crictl images) - [[ ! "$output" == *"pause"* ]] + [[ ! "$output" == *"$IMAGE_USED"* ]] } function test_crio_did_not_wipe_images() { # check that the pause image was not removed output=$(crictl images) - [[ "$output" == *"pause"* ]] + [[ "$output" == *"$IMAGE_USED"* ]] } function start_crio_with_stopped_pod() { start_crio + # it must be everything before the tag, because crictl output won't match (the columns for image and tag are separated by space) + IMAGE_USED=$(jq -r .image.image < "$TESTDATA"/container_config.json | cut -f1 -d ':') + local pod_id pod_id=$(crictl runp "$TESTDATA"/sandbox_config.json) + ctr_id=$(crictl create "$pod_id" "$TESTDATA"/container_config.json "$TESTDATA"/sandbox_config.json) + crictl start "$ctr_id" crictl stopp "$pod_id" } @@ -198,3 +203,95 @@ function start_crio_with_stopped_pod() { test_crio_did_not_wipe_containers test_crio_did_not_wipe_images } + +@test "internal_wipe remove containers and images when remove both" { + start_crio_with_stopped_pod + stop_crio_no_clean + + rm "$CONTAINER_VERSION_FILE" + rm "$CONTAINER_VERSION_FILE_PERSIST" + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + test_crio_wiped_containers + test_crio_wiped_images +} + +@test "internal_wipe remove containers when remove temporary" { + start_crio_with_stopped_pod + stop_crio_no_clean + + rm "$CONTAINER_VERSION_FILE" + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + test_crio_wiped_containers + test_crio_did_not_wipe_images +} + +@test "internal_wipe clear both when remove persist" { + start_crio_with_stopped_pod + stop_crio_no_clean + + rm "$CONTAINER_VERSION_FILE_PERSIST" + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + test_crio_wiped_containers + test_crio_wiped_images +} + +@test "internal_wipe don't clear podman containers" { + if [ -z "$PODMAN_BINARY" ]; then + skip "Podman not installed" + fi + + start_crio_with_stopped_pod + stop_crio_no_clean + + run_podman_with_args run --name test -d quay.io/crio/busybox:latest top + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + + run_podman_with_args ps -a | grep test +} + +@test "internal_wipe don't clear containers on a forced restart of crio" { + start_crio_with_stopped_pod + stop_crio_no_clean "-9" || true + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + + test_crio_did_not_wipe_containers + test_crio_did_not_wipe_images +} + +@test "internal_wipe eventually cleans network on forced restart of crio if network is slow to come up" { + CNI_RESULTS_DIR=/var/lib/cni/results + + start_crio + + pod_id=$(crictl runp "$TESTDATA"/sandbox_config.json) + ctr_id=$(crictl create "$pod_id" "$TESTDATA"/container_config.json "$TESTDATA"/sandbox_config.json) + crictl start "$ctr_id" + + stop_crio_no_clean + + runtime kill "$ctr_id" || true + runtime kill "$pod_id" || true + + # pretend like the CNI plugin is waiting for a container to start + mv "$CRIO_CNI_PLUGIN"/"$CNI_TYPE" "$CRIO_CNI_PLUGIN"/"$CNI_TYPE"-hidden + rm "$CONTAINER_VERSION_FILE" + + CONTAINER_INTERNAL_WIPE=true start_crio_no_setup + + # allow cri-o to catchup + sleep 5s + + # pretend like the CNI container has started + mv "$CRIO_CNI_PLUGIN"/"$CNI_TYPE"-hidden "$CRIO_CNI_PLUGIN"/"$CNI_TYPE" + + # allow cri-o to catch up + sleep 5s + + # make sure network resources were cleaned up + ! ls "$CNI_RESULTS_DIR"/*"$pod_id"* +} diff --git a/test/mocks/criostorage/criostorage.go b/test/mocks/criostorage/criostorage.go index 349dc9ea25b..ce71fd4cb7c 100644 --- a/test/mocks/criostorage/criostorage.go +++ b/test/mocks/criostorage/criostorage.go @@ -252,20 +252,6 @@ func (mr *MockRuntimeServerMockRecorder) GetWorkDir(arg0 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkDir", reflect.TypeOf((*MockRuntimeServer)(nil).GetWorkDir), arg0) } -// RemovePodSandbox mocks base method. -func (m *MockRuntimeServer) RemovePodSandbox(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemovePodSandbox", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemovePodSandbox indicates an expected call of RemovePodSandbox. -func (mr *MockRuntimeServerMockRecorder) RemovePodSandbox(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePodSandbox", reflect.TypeOf((*MockRuntimeServer)(nil).RemovePodSandbox), arg0) -} - // SetContainerMetadata mocks base method. func (m *MockRuntimeServer) SetContainerMetadata(arg0 string, arg1 *storage0.RuntimeContainerMetadata) error { m.ctrl.T.Helper() diff --git a/test/network.bats b/test/network.bats index 862c78cdf61..934a02fe426 100644 --- a/test/network.bats +++ b/test/network.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +# vim:set ft=bash : + load helpers function setup() { @@ -140,3 +142,29 @@ function check_networking() { check_networking } + +@test "Clean up network if pod sandbox gets killed" { + start_crio + + CNI_RESULTS_DIR=/var/lib/cni/results + POD=$(crictl runp "$TESTDATA/sandbox_config.json") + + # CNI result is there + # shellcheck disable=SC2010 + [[ $(ls $CNI_RESULTS_DIR | grep "$POD") != "" ]] + + # kill the sandbox + runtime kill "$POD" KILL + + # wait for the pod to be killed + while crictl inspectp "$POD" | jq -e '.status.state != "SANDBOX_NOTREADY"' > /dev/null; do + echo Waiting for sandbox to be stopped + done + + # now remove the sandbox + crictl rmp "$POD" + + # CNI result is gone + # shellcheck disable=SC2010 + [[ $(ls $CNI_RESULTS_DIR | grep "$POD") == "" ]] +}