未验证 提交 d1b560ed 编写于 作者: M Medya Ghazizadeh 提交者: GitHub

Merge pull request #10924 from tharun208/feat/remove_image_command

add minikube image rm command
......@@ -33,9 +33,8 @@ import (
// imageCmd represents the image command
var imageCmd = &cobra.Command{
Use: "image",
Short: "Load a local image into minikube",
Long: "Load a local image into minikube",
Use: "image COMMAND",
Short: "Manage images",
}
var (
......@@ -125,8 +124,30 @@ var loadImageCmd = &cobra.Command{
},
}
var removeImageCmd = &cobra.Command{
Use: "rm IMAGE [IMAGE...]",
Short: "Remove one or more images",
Example: `
$ minikube image rm image busybox
$ minikube image unload image busybox
`,
Args: cobra.MinimumNArgs(1),
Aliases: []string{"unload"},
Run: func(cmd *cobra.Command, args []string) {
profile, err := config.LoadProfile(viper.GetString(config.ProfileName))
if err != nil {
exit.Error(reason.Usage, "loading profile", err)
}
if err := machine.RemoveImages(args, 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")
}
......@@ -96,6 +96,11 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", cmd.UseLine()))
}
if len(cmd.Aliases) > 0 {
buf.WriteString("### Aliases\n\n")
buf.WriteString(fmt.Sprintf("%s\n\n", cmd.Aliases))
}
if len(cmd.Example) > 0 {
buf.WriteString("### Examples\n\n")
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
......
......@@ -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)
......
......@@ -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 {
......
......@@ -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")
......
......@@ -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
......
......@@ -63,14 +63,19 @@ func TestImageExists(t *testing.T) {
sha string
want bool
}{
{"docker", "missing", "0000000000000000000000000000000000000000000000000000000000000000", false},
{"docker", "image", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", true},
{"crio", "missing", "0000000000000000000000000000000000000000000000000000000000000000", false},
{"crio", "image", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", true},
{"docker", "missing-image", "0000000000000000000000000000000000000000000000000000000000000000", false},
{"docker", "available-image", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", true},
{"crio", "missing-image", "0000000000000000000000000000000000000000000000000000000000000000", false},
{"crio", "available-image", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", true},
}
for _, tc := range tests {
runner := NewFakeRunner(t)
runner.images = map[string]string{
"available-image": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
}
t.Run(tc.runtime, func(t *testing.T) {
r, err := New(Config{Type: tc.runtime, Runner: NewFakeRunner(t)})
r, err := New(Config{Type: tc.runtime, Runner: runner})
if err != nil {
t.Fatalf("New(%s): %v", tc.runtime, err)
}
......@@ -157,6 +162,7 @@ type FakeRunner struct {
cmds []string
services map[string]serviceState
containers map[string]string
images map[string]string
t *testing.T
}
......@@ -167,6 +173,7 @@ func NewFakeRunner(t *testing.T) *FakeRunner {
cmds: []string{},
t: t,
containers: map[string]string{},
images: map[string]string{},
}
}
......@@ -277,10 +284,23 @@ func (f *FakeRunner) dockerRm(args []string) (string, error) {
func (f *FakeRunner) dockerInspect(args []string) (string, error) {
if args[1] == "--format" && args[2] == "{{.Id}}" {
if args[3] == "missing" {
image, ok := f.images[args[3]]
if !ok {
return "", &exec.ExitError{Stderr: []byte("Error: No such object: missing")}
}
return "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", nil
return "sha256:" + image, nil
}
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
}
......@@ -308,6 +328,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 +440,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 +690,9 @@ func TestContainerFunctions(t *testing.T) {
"fgh1": prefix + "coredns",
"xyz2": prefix + "storage",
}
runner.images = map[string]string{
"image1": "latest",
}
cr, err := New(Config{Type: tc.runtime, Runner: runner})
if err != nil {
t.Fatalf("New(%s): %v", tc.runtime, err)
......@@ -709,6 +742,14 @@ 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)
}
if len(runner.images) > 0 {
t.Errorf("RemoveImage = %v, want 0 items", len(runner.images))
}
})
}
}
......@@ -172,6 +172,19 @@ 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)
if r.UseCRI {
return removeCRIImage(r.Runner, 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
......
......@@ -292,3 +292,83 @@ 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(cruntime cruntime.Manager, images []string) error {
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 cruntime.RemoveImage(image)
})
}
if err := g.Wait(); err != nil {
return errors.Wrap(err, "error removing images")
}
klog.Infoln("Successfully removed images")
return nil
}
func RemoveImages(images []string, profile *config.Profile) error {
api, err := NewAPIClient()
if err != nil {
return errors.Wrap(err, "error creating api client")
}
defer api.Close()
succeeded := []string{}
failed := []string{}
pName := profile.Name
c, err := config.Load(pName)
if err != nil {
klog.Errorf("Failed to load profile %q: %v", pName, err)
return errors.Wrapf(err, "error loading config for profile :%v", pName)
}
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)
continue
}
if status == state.Running.String() {
h, err := api.Load(m)
if err != nil {
klog.Warningf("Failed to load machine %q: %v", m, err)
continue
}
runner, err := CommandRunner(h)
if err != nil {
return err
}
cruntime, err := cruntime.New(cruntime.Config{Type: c.KubernetesConfig.ContainerRuntime, Runner: runner})
if err != nil {
return errors.Wrap(err, "error creating container runtime")
}
err = removeImages(cruntime, images)
if err != nil {
failed = append(failed, m)
klog.Warningf("Failed to remove images for profile %s %v", pName, err.Error())
continue
}
succeeded = append(succeeded, m)
}
}
klog.Infof("succeeded removing from: %s", strings.Join(succeeded, " "))
klog.Infof("failed removing from: %s", strings.Join(failed, " "))
return nil
}
......@@ -247,6 +247,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}
......
---
title: "image"
description: >
Load a local image into minikube
Manage images
---
## minikube image
Load a local image into minikube
Manage images
### Synopsis
Load a local image into minikube
Manage images
### Options inherited from parent commands
......@@ -118,3 +118,51 @@ 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
Remove one or more images
```shell
minikube image rm IMAGE [IMAGE...] [flags]
```
### Aliases
[unload]
### Examples
```
$ minikube image rm image busybox
$ minikube image unload 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
```
......@@ -133,6 +133,7 @@ func TestFunctional(t *testing.T) {
{"DockerEnv", validateDockerEnv},
{"NodeLabels", validateNodeLabels},
{"LoadImage", validateLoadImage},
{"RemoveImage", validateRemoveImage},
}
for _, tc := range tests {
tc := tc
......@@ -219,27 +220,80 @@ func validateLoadImage(ctx context.Context, t *testing.T, profile string) {
}
// make sure the image was correctly loaded
rr, err = inspectImage(ctx, t, profile, newImage)
if err != nil {
t.Fatalf("listing images: %v\n%s", err, rr.Output())
}
if !strings.Contains(rr.Output(), newImage) {
t.Fatalf("expected %s to be loaded into minikube but the image is not there", newImage)
}
}
// validateRemoveImage makes sures that `minikube rm image` works as expected
func validateRemoveImage(ctx context.Context, t *testing.T, profile string) {
if NoneDriver() {
t.Skip("load image not available on none driver")
}
if GithubActionRunner() && runtime.GOOS == "darwin" {
t.Skip("skipping on github actions and darwin, as this test requires a running docker daemon")
}
defer PostMortemLogs(t, profile)
// pull busybox
busyboxImage := "busybox:latest"
rr, err := Run(t, exec.CommandContext(ctx, "docker", "pull", busyboxImage))
if err != nil {
t.Fatalf("failed to setup test (pull image): %v\n%s", err, rr.Output())
}
// try to load the image into minikube
rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "image", "load", busyboxImage))
if err != nil {
t.Fatalf("loading image into minikube: %v\n%s", err, rr.Output())
}
// try to remove the image from minikube
rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "image", "rm", busyboxImage))
if err != nil {
t.Fatalf("removing image from minikube: %v\n%s", err, rr.Output())
}
// make sure the image was removed
var cmd *exec.Cmd
if ContainerRuntime() == "docker" {
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "docker", "image", "inspect", newImage)
} else if ContainerRuntime() == "containerd" {
// crictl inspecti busybox:test-example
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", newImage)
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "docker", "images")
} else {
// crio adds localhost prefix
// crictl inspecti localhost/busybox:test-example
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", "localhost/"+newImage)
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "images")
}
rr, err = Run(t, cmd)
if err != nil {
t.Fatalf("listing images: %v\n%s", err, rr.Output())
}
if !strings.Contains(rr.Output(), newImage) {
t.Fatalf("expected %s to be loaded into minikube but the image is not there", newImage)
if strings.Contains(rr.Output(), busyboxImage) {
t.Fatalf("expected %s to be removed from minikube but the image is there", busyboxImage)
}
}
func inspectImage(ctx context.Context, t *testing.T, profile string, image string) (*RunResult, error) {
var cmd *exec.Cmd
if ContainerRuntime() == "docker" {
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "docker", "image", "inspect", image)
} else if ContainerRuntime() == "containerd" {
// crictl inspecti busybox:test-example
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", image)
} else {
// crio adds localhost prefix
// crictl inspecti localhost/busybox:test-example
cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", "localhost/"+image)
}
rr, err := Run(t, cmd)
if err != nil {
return rr, err
}
return rr, nil
}
// check functionality of minikube after evaling docker-env
// TODO: Add validatePodmanEnv for crio runtime: #10231
func validateDockerEnv(ctx context.Context, t *testing.T, profile string) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册