diff --git a/cmd/minikube/cmd/image.go b/cmd/minikube/cmd/image.go index de3e80e389fa1559d61e0e4f8a8b2fc4c87854cc..1640d937c1536ff20e9c60cbc9cab82e02149ae1 100644 --- a/cmd/minikube/cmd/image.go +++ b/cmd/minikube/cmd/image.go @@ -125,8 +125,28 @@ var loadImageCmd = &cobra.Command{ }, } +var removeImageCmd = &cobra.Command{ + Use: "rm IMAGE [IMAGE...]", + Short: "Remove one or more images", + Long: "Load a image into minikube", + Example: "minikube image rm image busybox", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + exit.Message(reason.Usage, "Please provide an image to remove via ") + } + profile, err := config.LoadProfile(viper.GetString(config.ProfileName)) + if err != nil { + exit.Error(reason.Usage, "loading profile", err) + } + if err := machine.RemoveImages(args, []*config.Profile{profile}); err != nil { + exit.Error(reason.GuestImageRemove, "Failed to remove image", err) + } + }, +} + func init() { imageCmd.AddCommand(loadImageCmd) + imageCmd.AddCommand(removeImageCmd) loadImageCmd.Flags().BoolVar(&imgDaemon, "daemon", false, "Cache image from docker daemon") loadImageCmd.Flags().BoolVar(&imgRemote, "remote", false, "Cache image from remote registry") } diff --git a/pkg/minikube/cruntime/containerd.go b/pkg/minikube/cruntime/containerd.go index dee2a1dfd950b3971cf7f0d536361feb33d83c30..ca937cca9162e1806de6271c828f89df35500bb3 100644 --- a/pkg/minikube/cruntime/containerd.go +++ b/pkg/minikube/cruntime/containerd.go @@ -249,6 +249,11 @@ func (r *Containerd) LoadImage(path string) error { return nil } +// RemoveImage removes a image +func (r *Containerd) RemoveImage(name string) error { + return removeCRIImage(r.Runner, name) +} + // CGroupDriver returns cgroup driver ("cgroupfs" or "systemd") func (r *Containerd) CGroupDriver() (string, error) { info, err := getCRIInfo(r.Runner) diff --git a/pkg/minikube/cruntime/cri.go b/pkg/minikube/cruntime/cri.go index 1a9b46a0c7df38cf25e031a3f566b85328f9e776..5ad27492870755f1aa8d1e4af4c78f11be0dacd3 100644 --- a/pkg/minikube/cruntime/cri.go +++ b/pkg/minikube/cruntime/cri.go @@ -187,6 +187,19 @@ func killCRIContainers(cr CommandRunner, ids []string) error { return nil } +// removeCRIImage remove image using crictl +func removeCRIImage(cr CommandRunner, name string) error { + klog.Infof("Removing image: %s", name) + + crictl := getCrictlPath(cr) + args := append([]string{crictl, "rmi"}, name) + c := exec.Command("sudo", args...) + if _, err := cr.RunCmd(c); err != nil { + return errors.Wrap(err, "crictl") + } + return nil +} + // stopCRIContainers stops containers using crictl func stopCRIContainers(cr CommandRunner, ids []string) error { if len(ids) == 0 { diff --git a/pkg/minikube/cruntime/crio.go b/pkg/minikube/cruntime/crio.go index 8483752d86824584a6924ea215e39117bfb0f042..63d961aed3a9c20a7e86ba41b3d2b5464f181b86 100644 --- a/pkg/minikube/cruntime/crio.go +++ b/pkg/minikube/cruntime/crio.go @@ -177,6 +177,11 @@ func (r *CRIO) LoadImage(path string) error { return nil } +// RemoveImage removes a image +func (r *CRIO) RemoveImage(name string) error { + return removeCRIImage(r.Runner, name) +} + // CGroupDriver returns cgroup driver ("cgroupfs" or "systemd") func (r *CRIO) CGroupDriver() (string, error) { c := exec.Command("crio", "config") diff --git a/pkg/minikube/cruntime/cruntime.go b/pkg/minikube/cruntime/cruntime.go index f7111e91aff773376358ce7232069bfdf829712f..dd60a432a9e9fb6bc4d33e3754b232f0d1affdda 100644 --- a/pkg/minikube/cruntime/cruntime.go +++ b/pkg/minikube/cruntime/cruntime.go @@ -99,6 +99,9 @@ type Manager interface { // ImageExists takes image name and image sha checks if an it exists ImageExists(string, string) bool + // RemoveImage remove image based on name + RemoveImage(string) error + // ListContainers returns a list of managed by this container runtime ListContainers(ListOptions) ([]string, error) // KillContainers removes containers based on ID diff --git a/pkg/minikube/cruntime/cruntime_test.go b/pkg/minikube/cruntime/cruntime_test.go index 461809e56894f7c94aa54cbeb94125cfe2180414..fc783c2aebcb526915e45faf1ec16b3419ea3092 100644 --- a/pkg/minikube/cruntime/cruntime_test.go +++ b/pkg/minikube/cruntime/cruntime_test.go @@ -157,6 +157,7 @@ type FakeRunner struct { cmds []string services map[string]serviceState containers map[string]string + images map[string]string t *testing.T } @@ -167,6 +168,7 @@ func NewFakeRunner(t *testing.T) *FakeRunner { cmds: []string{}, t: t, containers: map[string]string{}, + images: map[string]string{}, } } @@ -285,6 +287,18 @@ func (f *FakeRunner) dockerInspect(args []string) (string, error) { return "", nil } +func (f *FakeRunner) dockerRmi(args []string) (string, error) { + // Skip "-f" argument + for _, id := range args[1:] { + f.t.Logf("fake docker: Removing id %q", id) + if f.images[id] == "" { + return "", fmt.Errorf("no such image") + } + delete(f.images, id) + } + return "", nil +} + // docker is a fake implementation of docker func (f *FakeRunner) docker(args []string, _ bool) (string, error) { switch cmd := args[0]; cmd { @@ -308,6 +322,9 @@ func (f *FakeRunner) docker(args []string, _ bool) (string, error) { return f.dockerInspect(args[1:]) } + case "rmi": + return f.dockerRmi(args) + case "inspect": return f.dockerInspect(args) @@ -417,7 +434,14 @@ func (f *FakeRunner) crictl(args []string, _ bool) (string, error) { delete(f.containers, id) } - + case "rmi": + for _, id := range args[1:] { + f.t.Logf("fake crictl: Removing id %q", id) + if f.images[id] == "" { + return "", fmt.Errorf("no such image") + } + delete(f.images, id) + } } return "", nil } @@ -660,6 +684,9 @@ func TestContainerFunctions(t *testing.T) { "fgh1": prefix + "coredns", "xyz2": prefix + "storage", } + runner.images = map[string]string{ + "image1": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + } cr, err := New(Config{Type: tc.runtime, Runner: runner}) if err != nil { t.Fatalf("New(%s): %v", tc.runtime, err) @@ -709,6 +736,15 @@ func TestContainerFunctions(t *testing.T) { if len(got) > 0 { t.Errorf("ListContainers(apiserver) = %v, want 0 items", got) } + + // Remove a image + if err := cr.RemoveImage("image1"); err != nil { + t.Fatalf("RemoveImage: %v", err) + } + // imageExists := cr.ImageExists("image1", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + if len(runner.images) > 0 { + t.Errorf("RemoveImage = %v, want 0 items", len(runner.images)) + } }) } } diff --git a/pkg/minikube/cruntime/docker.go b/pkg/minikube/cruntime/docker.go index 6ce546e9fc6fdae781ec98b318d0a90e4acbe668..40efb9c8f4e71abd61f5511d0d4c667cb5f1a1e2 100644 --- a/pkg/minikube/cruntime/docker.go +++ b/pkg/minikube/cruntime/docker.go @@ -172,6 +172,16 @@ func (r *Docker) LoadImage(path string) error { return nil } +// RemoveImage removes a image +func (r *Docker) RemoveImage(name string) error { + klog.Infof("Removing image: %s", name) + c := exec.Command("docker", "rmi", name) + if _, err := r.Runner.RunCmd(c); err != nil { + return errors.Wrap(err, "remove image docker.") + } + return nil +} + // CGroupDriver returns cgroup driver ("cgroupfs" or "systemd") func (r *Docker) CGroupDriver() (string, error) { // Note: the server daemon has to be running, for this call to return successfully diff --git a/pkg/minikube/machine/cache_images.go b/pkg/minikube/machine/cache_images.go index 304d9a6b7085475696e73779429cb5684444734a..255c655e4ee37760dca718ac738cedd769af740a 100644 --- a/pkg/minikube/machine/cache_images.go +++ b/pkg/minikube/machine/cache_images.go @@ -292,3 +292,90 @@ func transferAndLoadImage(cr command.Runner, k8s config.KubernetesConfig, src st klog.Infof("Transferred and loaded %s from cache", src) return nil } + +// removeImages removes images from the container run time +func removeImages(cc *config.ClusterConfig, runner command.Runner, images []string) error { + cr, err := cruntime.New(cruntime.Config{Type: cc.KubernetesConfig.ContainerRuntime, Runner: runner}) + if err != nil { + return errors.Wrap(err, "runtime") + } + + klog.Infof("RemovingImages start: %s", images) + start := time.Now() + + defer func() { + klog.Infof("RemovingImages completed in %s", time.Since(start)) + }() + + var g errgroup.Group + + for _, image := range images { + image := image + g.Go(func() error { + return cr.RemoveImage(image) + }) + } + if err := g.Wait(); err != nil { + return errors.Wrap(err, "removing images") + } + klog.Infoln("Successfully removed images") + return nil +} + +func RemoveImages(images []string, profiles []*config.Profile) error { + api, err := NewAPIClient() + if err != nil { + return errors.Wrap(err, "api") + } + defer api.Close() + + succeeded := []string{} + failed := []string{} + + for _, p := range profiles { // loading images to all running profiles + pName := p.Name // capture the loop variable + + c, err := config.Load(pName) + if err != nil { + // Non-fatal because it may race with profile deletion + klog.Errorf("Failed to load profile %q: %v", pName, err) + failed = append(failed, pName) + continue + } + + for _, n := range c.Nodes { + m := config.MachineName(*c, n) + + status, err := Status(api, m) + if err != nil { + klog.Warningf("error getting status for %s: %v", m, err) + failed = append(failed, m) + continue + } + + if status == state.Running.String() { // the not running hosts will load on next start + h, err := api.Load(m) + if err != nil { + klog.Warningf("Failed to load machine %q: %v", m, err) + failed = append(failed, m) + continue + } + cr, err := CommandRunner(h) + if err != nil { + return err + } + err = removeImages(c, cr, images) + if err != nil { + failed = append(failed, m) + klog.Warningf("Failed to load cached images for profile %s. make sure the profile is running. %v", pName, err) + continue + } + succeeded = append(succeeded, m) + } + } + } + + klog.Infof("succeeded removing to: %s", strings.Join(succeeded, " ")) + klog.Infof("failed removing to: %s", strings.Join(failed, " ")) + return nil +} diff --git a/pkg/minikube/reason/reason.go b/pkg/minikube/reason/reason.go index 854547b14f055ecb55f003d5657c9ad3f9ff2502..1bd8ebb20610a4ec9faf2f9019d87843ef963fa3 100644 --- a/pkg/minikube/reason/reason.go +++ b/pkg/minikube/reason/reason.go @@ -246,6 +246,7 @@ var ( GuestCpConfig = Kind{ID: "GUEST_CP_CONFIG", ExitCode: ExGuestConfig} GuestDeletion = Kind{ID: "GUEST_DELETION", ExitCode: ExGuestError} GuestImageLoad = Kind{ID: "GUEST_IMAGE_LOAD", ExitCode: ExGuestError} + GuestImageRemove = Kind{ID: "GUEST_IMAGE_REMOVE", ExitCode: ExGuestError} GuestLoadHost = Kind{ID: "GUEST_LOAD_HOST", ExitCode: ExGuestError} GuestMount = Kind{ID: "GUEST_MOUNT", ExitCode: ExGuestError} GuestMountConflict = Kind{ID: "GUEST_MOUNT_CONFLICT", ExitCode: ExGuestConflict} diff --git a/site/content/en/docs/commands/image.md b/site/content/en/docs/commands/image.md index af13106b244de6cf1ee062d9a25d897437153364..86c1c51ae405d12a82fe69f18e9445796a9ada27 100644 --- a/site/content/en/docs/commands/image.md +++ b/site/content/en/docs/commands/image.md @@ -118,3 +118,43 @@ minikube image load image.tar --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging ``` +## minikube image rm + +Remove one or more images + +### Synopsis + +Load a image into minikube + +```shell +minikube image rm IMAGE [IMAGE...] [flags] +``` + +### Examples + +``` +minikube image rm image busybox +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files + -b, --bootstrapper string The name of the cluster bootstrapper that will set up the Kubernetes cluster. (default "kubeadm") + -h, --help + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --log_file string If non-empty, use this log file + --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level) + -p, --profile string The name of the minikube VM being used. This can be set to allow having multiple instances of minikube independently. (default "minikube") + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + --user string Specifies the user executing the operation. Useful for auditing operations executed by 3rd party tools. Defaults to the operating system username. + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` +