From 66d000301ed7f4b55cfe0c577fbf4425d4006f19 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Wed, 16 Jan 2019 10:33:01 -0500 Subject: Ensure that wait exits on state transition When waiting for a container, there is a long interval between status checks - plenty long enough for the container in question to start, then subsequently be cleaned up and returned to Created state to be restarted. As such, we can't wait on container state to go to Stopped or Exited - anything that is not Running or Paused indicates the container is dead. Signed-off-by: Matthew Heon --- libpod/container_internal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 04d67b1aa..0284dde2f 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -531,7 +531,7 @@ func (c *Container) isStopped() (bool, error) { if err != nil { return true, err } - return (c.state.State == ContainerStateStopped || c.state.State == ContainerStateExited), nil + return (c.state.State != ContainerStateRunning && c.state.State != ContainerStatePaused), nil } // save container state to the database -- cgit v1.2.3-54-g00ecf From 2a6d8b3bd69fe2aedf9a30744befed1aead5e2b9 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Fri, 18 Jan 2019 14:30:45 +0100 Subject: podman-inspect: don't ignore errors Return errors when executing the --format templates. Otherwise, Podman will just silently ignore them and not print any output that could guide user into solving the issue. Fixes: #2159 Signed-off-by: Valentin Rothberg --- cmd/podman/formats/formats.go | 13 +++++++------ cmd/podman/inspect.go | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cmd/podman/formats/formats.go b/cmd/podman/formats/formats.go index 3da0ea385..c454c39bd 100644 --- a/cmd/podman/formats/formats.go +++ b/cmd/podman/formats/formats.go @@ -20,6 +20,8 @@ const ( JSONString = "json" // IDString const to save on duplicates for Go templates IDString = "{{.ID}}" + + parsingErrorStr = "Template parsing error" ) // Writer interface for outputs @@ -96,7 +98,7 @@ func (t StdoutTemplateArray) Out() error { t.Template = strings.Replace(strings.TrimSpace(t.Template[5:]), " ", "\t", -1) headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) if err != nil { - return errors.Wrapf(err, "Template parsing error") + return errors.Wrapf(err, parsingErrorStr) } err = headerTmpl.Execute(w, t.Fields) if err != nil { @@ -107,13 +109,12 @@ func (t StdoutTemplateArray) Out() error { t.Template = strings.Replace(t.Template, " ", "\t", -1) tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) if err != nil { - return errors.Wrapf(err, "Template parsing error") + return errors.Wrapf(err, parsingErrorStr) } - for i, img := range t.Output { + for i, raw := range t.Output { basicTmpl := tmpl.Funcs(basicFunctions) - err = basicTmpl.Execute(w, img) - if err != nil { - return err + if err := basicTmpl.Execute(w, raw); err != nil { + return errors.Wrapf(err, parsingErrorStr) } if i != len(t.Output)-1 { fmt.Fprintln(w, "") diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index 6ffcde55f..2f1e97c6c 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -87,6 +87,9 @@ func inspectCmd(c *cli.Context) error { } inspectedObjects, iterateErr := iterateInput(getContext(), c, args, runtime, inspectType) + if iterateErr != nil { + return iterateErr + } var out formats.Writer if outputFormat != "" && outputFormat != formats.JSONString { @@ -97,8 +100,7 @@ func inspectCmd(c *cli.Context) error { out = formats.JSONStructArray{Output: inspectedObjects} } - formats.Writer(out).Out() - return iterateErr + return formats.Writer(out).Out() } // func iterateInput iterates the images|containers the user has requested and returns the inspect data and error -- cgit v1.2.3-54-g00ecf From 2265038a55f81d559b135e5b8384959b1cd4cbfd Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 18 Jan 2019 10:27:51 -0500 Subject: Do not unmarshal into c.config.Spec We try to keep c.config immutable, but Go doesn't really agree with me that things other than strings and ints can be immutable, so occasionally things like this slip through. When unmarshalling the OCI spec from disk, do it into a separate struct, to ensure we don't make lasting modifications to the spec in the Container struct (which could affect container restart). Signed-off-by: Matthew Heon --- libpod/container.go | 7 ++++--- libpod/container_easyjson.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libpod/container.go b/libpod/container.go index f18f36160..c15633d34 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -410,14 +410,15 @@ func (c *Container) Spec() *spec.Spec { // config does not exist (e.g., because the container was never started) return // the spec from the config. func (c *Container) specFromState() (*spec.Spec, error) { - spec := c.config.Spec + returnSpec := c.config.Spec if f, err := os.Open(c.state.ConfigPath); err == nil { + returnSpec = new(spec.Spec) content, err := ioutil.ReadAll(f) if err != nil { return nil, errors.Wrapf(err, "error reading container config") } - if err := json.Unmarshal([]byte(content), &spec); err != nil { + if err := json.Unmarshal([]byte(content), &returnSpec); err != nil { return nil, errors.Wrapf(err, "error unmarshalling container config") } } else { @@ -427,7 +428,7 @@ func (c *Container) specFromState() (*spec.Spec, error) { } } - return spec, nil + return returnSpec, nil } // ID returns the container's ID diff --git a/libpod/container_easyjson.go b/libpod/container_easyjson.go index 8bf5cb64f..61ee83231 100644 --- a/libpod/container_easyjson.go +++ b/libpod/container_easyjson.go @@ -1,6 +1,6 @@ // +build seccomp ostree selinux varlink exclude_graphdriver_devicemapper -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT . package libpod -- cgit v1.2.3-54-g00ecf From 2ba7b991b2bae4148eef977136c57f9da828f9bf Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 18 Jan 2019 17:12:23 +0100 Subject: rootless: fix --pid=host without --privileged When using --pid=host don't try to cover /proc paths, as they are coming from the /proc bind mounted from the host. Signed-off-by: Giuseppe Scrivano --- pkg/spec/spec.go | 4 ++++ test/e2e/rootless_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 9ef0223f2..46105af4a 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -376,6 +376,10 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint } func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) { + if config.PidMode.IsHost() && rootless.IsRootless() { + return + } + if !config.Privileged { for _, mp := range []string{ "/proc/acpi", diff --git a/test/e2e/rootless_test.go b/test/e2e/rootless_test.go index 8e9f9fc8d..e9606f859 100644 --- a/test/e2e/rootless_test.go +++ b/test/e2e/rootless_test.go @@ -274,6 +274,10 @@ var _ = Describe("Podman rootless", func() { runRootlessHelper([]string{"--net", "host"}) }) + It("podman rootless rootfs --pid host", func() { + runRootlessHelper([]string{"--pid", "host"}) + }) + It("podman rootless rootfs --privileged", func() { runRootlessHelper([]string{"--privileged"}) }) -- cgit v1.2.3-54-g00ecf From 14eaca337af0fe3c66624a1a808157b6de8bff68 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Fri, 18 Jan 2019 15:01:53 -0500 Subject: Vendor in latest opencontainers/selinux This will now verify labels passed in by the user. Will also prevent users from accidently relabeling their homedir. podman run -ti -v ~/home/user:Z fedora sh Is not a good idea. Signed-off-by: Daniel J Walsh --- cmd/podman/create.go | 12 ++++- libpod/oci.go | 17 ++++-- vendor.conf | 2 +- .../selinux/go-selinux/label/label.go | 4 +- .../selinux/go-selinux/label/label_selinux.go | 62 ++++++++++++++++++++-- .../selinux/go-selinux/selinux_linux.go | 36 +++++++++---- .../selinux/go-selinux/selinux_stub.go | 8 +-- 7 files changed, 111 insertions(+), 30 deletions(-) diff --git a/cmd/podman/create.go b/cmd/podman/create.go index d98b78bd4..93f38d2db 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -172,7 +172,11 @@ func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error { if err != nil { return errors.Wrapf(err, "container %q not found", config.PidMode.Container()) } - labelOpts = append(labelOpts, label.DupSecOpt(ctr.ProcessLabel())...) + secopts, err := label.DupSecOpt(ctr.ProcessLabel()) + if err != nil { + return errors.Wrapf(err, "failed to duplicate label %q ", ctr.ProcessLabel()) + } + labelOpts = append(labelOpts, secopts...) } if config.IpcMode.IsHost() { @@ -182,7 +186,11 @@ func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error { if err != nil { return errors.Wrapf(err, "container %q not found", config.IpcMode.Container()) } - labelOpts = append(labelOpts, label.DupSecOpt(ctr.ProcessLabel())...) + secopts, err := label.DupSecOpt(ctr.ProcessLabel()) + if err != nil { + return errors.Wrapf(err, "failed to duplicate label %q ", ctr.ProcessLabel()) + } + labelOpts = append(labelOpts, secopts...) } for _, opt := range securityOpts { diff --git a/libpod/oci.go b/libpod/oci.go index 31c1a7e85..a1894b52f 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -357,18 +357,25 @@ func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, res // Set the label of the conmon process to be level :s0 // This will allow the container processes to talk to fifo-files // passed into the container by conmon - var plabel string + var ( + plabel string + con selinux.Context + ) plabel, err = selinux.CurrentLabel() if err != nil { childPipe.Close() return errors.Wrapf(err, "Failed to get current SELinux label") } - c := selinux.NewContext(plabel) + con, err = selinux.NewContext(plabel) + if err != nil { + return errors.Wrapf(err, "Failed to get new context from SELinux label") + } + runtime.LockOSThread() - if c["level"] != "s0" && c["level"] != "" { - c["level"] = "s0" - if err = label.SetProcessLabel(c.Get()); err != nil { + if con["level"] != "s0" && con["level"] != "" { + con["level"] = "s0" + if err = label.SetProcessLabel(con.Get()); err != nil { runtime.UnlockOSThread() return err } diff --git a/vendor.conf b/vendor.conf index 18283cae6..82ae1260e 100644 --- a/vendor.conf +++ b/vendor.conf @@ -51,7 +51,7 @@ github.com/opencontainers/image-spec v1.0.0 github.com/opencontainers/runc bbb17efcb4c0ab986407812a31ba333a7450064c github.com/opencontainers/runtime-spec d810dbc60d8c5aeeb3d054bd1132fab2121968ce github.com/opencontainers/runtime-tools master -github.com/opencontainers/selinux 51c6c0a5dbc675792e953298cb9871819d6f9bb8 +github.com/opencontainers/selinux v1.1 github.com/ostreedev/ostree-go master github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2 diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go index bb27ac936..4e9a8c54f 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label.go @@ -75,8 +75,8 @@ func ReleaseLabel(label string) error { // DupSecOpt takes a process label and returns security options that // can be used to set duplicate labels on future container processes -func DupSecOpt(src string) []string { - return nil +func DupSecOpt(src string) ([]string, error) { + return nil, nil } // DisableSecOpt returns a security opt that can disable labeling diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_selinux.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_selinux.go index de214b2d5..d4e26909d 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_selinux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_selinux.go @@ -4,6 +4,8 @@ package label import ( "fmt" + "os" + "os/user" "strings" "github.com/opencontainers/selinux/go-selinux" @@ -35,8 +37,15 @@ func InitLabels(options []string) (plabel string, mlabel string, Err error) { ReleaseLabel(mountLabel) } }() - pcon := selinux.NewContext(processLabel) - mcon := selinux.NewContext(mountLabel) + pcon, err := selinux.NewContext(processLabel) + if err != nil { + return "", "", err + } + + mcon, err := selinux.NewContext(mountLabel) + if err != nil { + return "", "", err + } for _, opt := range options { if opt == "disable" { return "", mountLabel, nil @@ -146,13 +155,56 @@ func Relabel(path string, fileLabel string, shared bool) error { return nil } - exclude_paths := map[string]bool{"/": true, "/usr": true, "/etc": true, "/tmp": true, "/home": true, "/run": true, "/var": true, "/root": true} + exclude_paths := map[string]bool{ + "/": true, + "/bin": true, + "/boot": true, + "/dev": true, + "/etc": true, + "/etc/passwd": true, + "/etc/pki": true, + "/etc/shadow": true, + "/home": true, + "/lib": true, + "/lib64": true, + "/media": true, + "/opt": true, + "/proc": true, + "/root": true, + "/run": true, + "/sbin": true, + "/srv": true, + "/sys": true, + "/tmp": true, + "/usr": true, + "/var": true, + "/var/lib": true, + "/var/log": true, + } + + if home := os.Getenv("HOME"); home != "" { + exclude_paths[home] = true + } + + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + if usr, err := user.Lookup(sudoUser); err == nil { + exclude_paths[usr.HomeDir] = true + } + } + + if path != "/" { + path = strings.TrimSuffix(path, "/") + } if exclude_paths[path] { return fmt.Errorf("SELinux relabeling of %s is not allowed", path) } if shared { - c := selinux.NewContext(fileLabel) + c, err := selinux.NewContext(fileLabel) + if err != nil { + return err + } + c["level"] = "s0" fileLabel = c.Get() } @@ -195,7 +247,7 @@ func ReleaseLabel(label string) error { // DupSecOpt takes a process label and returns security options that // can be used to set duplicate labels on future container processes -func DupSecOpt(src string) []string { +func DupSecOpt(src string) ([]string, error) { return selinux.DupSecOpt(src) } diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go index 7832f7497..5adafd317 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go @@ -52,6 +52,8 @@ var ( ErrMCSAlreadyExists = errors.New("MCS label already exists") // ErrEmptyPath is returned when an empty path has been specified. ErrEmptyPath = errors.New("empty path") + // InvalidLabel is returned when an invalid label is specified. + InvalidLabel = errors.New("Invalid Label") assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`) roFileLabel string @@ -405,11 +407,14 @@ func (c Context) Get() string { } // NewContext creates a new Context struct from the specified label -func NewContext(label string) Context { +func NewContext(label string) (Context, error) { c := make(Context) if len(label) != 0 { con := strings.SplitN(label, ":", 4) + if len(con) < 3 { + return c, InvalidLabel + } c["user"] = con[0] c["role"] = con[1] c["type"] = con[2] @@ -417,7 +422,7 @@ func NewContext(label string) Context { c["level"] = con[3] } } - return c + return c, nil } // ClearLabels clears all reserved labels @@ -630,12 +635,12 @@ func ContainerLabels() (processLabel string, fileLabel string) { roFileLabel = fileLabel } exit: - scon := NewContext(processLabel) + scon, _ := NewContext(processLabel) if scon["level"] != "" { mcs := uniqMcs(1024) scon["level"] = mcs processLabel = scon.Get() - scon = NewContext(fileLabel) + scon, _ = NewContext(fileLabel) scon["level"] = mcs fileLabel = scon.Get() } @@ -661,8 +666,14 @@ func CopyLevel(src, dest string) (string, error) { if err := SecurityCheckContext(dest); err != nil { return "", err } - scon := NewContext(src) - tcon := NewContext(dest) + scon, err := NewContext(src) + if err != nil { + return "", err + } + tcon, err := NewContext(dest) + if err != nil { + return "", err + } mcsDelete(tcon["level"]) mcsAdd(scon["level"]) tcon["level"] = scon["level"] @@ -714,15 +725,18 @@ func Chcon(fpath string, label string, recurse bool) error { // DupSecOpt takes an SELinux process label and returns security options that // can be used to set the SELinux Type and Level for future container processes. -func DupSecOpt(src string) []string { +func DupSecOpt(src string) ([]string, error) { if src == "" { - return nil + return nil, nil + } + con, err := NewContext(src) + if err != nil { + return nil, err } - con := NewContext(src) if con["user"] == "" || con["role"] == "" || con["type"] == "" { - return nil + return nil, nil } dup := []string{"user:" + con["user"], "role:" + con["role"], @@ -733,7 +747,7 @@ func DupSecOpt(src string) []string { dup = append(dup, "level:"+con["level"]) } - return dup + return dup, nil } // DisableSecOpt returns a security opt that can be used to disable SELinux diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go index 99efa155a..9497acbd0 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go @@ -115,9 +115,9 @@ func (c Context) Get() string { } // NewContext creates a new Context struct from the specified label -func NewContext(label string) Context { +func NewContext(label string) (Context, error) { c := make(Context) - return c + return c, nil } // ClearLabels clears all reserved MLS/MCS levels @@ -195,8 +195,8 @@ func Chcon(fpath string, label string, recurse bool) error { // DupSecOpt takes an SELinux process label and returns security options that // can be used to set the SELinux Type and Level for future container processes. -func DupSecOpt(src string) []string { - return nil +func DupSecOpt(src string) ([]string, error) { + return nil, nil } // DisableSecOpt returns a security opt that can be used to disable SELinux -- cgit v1.2.3-54-g00ecf From c5f408b00887c0c3130e06b5dc234a6b0ca99b2d Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Mon, 21 Jan 2019 13:35:06 -0600 Subject: Show a better error message when podman info fails during a refresh Signed-off-by: Ryan Gonzalez --- libpod/runtime.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libpod/runtime.go b/libpod/runtime.go index facbe5d66..11c90166d 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -772,7 +772,11 @@ func (r *Runtime) refreshRootless() error { // Take advantage of a command that requires a new userns // so that we are running as the root user and able to use refresh() cmd := exec.Command(os.Args[0], "info") - return cmd.Run() + err := cmd.Run() + if err != nil { + return errors.Wrapf(err, "Error running %s info while refreshing state", os.Args[0]) + } + return nil } // Reconfigures the runtime after a reboot -- cgit v1.2.3-54-g00ecf From 9dfefed334cda999b55820368507209f8c6721b0 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Tue, 29 Jan 2019 16:10:51 +0000 Subject: Make --quiet work in podman create/run The --queit option is supposed to suppress the pulling messages when a new image is being pulled down. This patch fixes this issue. Signed-off-by: Daniel J Walsh --- cmd/podman/create.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 93f38d2db..e10009971 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "os" "path/filepath" "strconv" @@ -127,7 +128,12 @@ func createContainer(c *cli.Context, runtime *libpod.Runtime) (*libpod.Container var data *inspect.ImageData = nil if rootfs == "" && !rootless.SkipStorageSetup() { - newImage, err := runtime.ImageRuntime().New(ctx, c.Args()[0], rtc.SignaturePolicyPath, "", os.Stderr, nil, image.SigningOptions{}, false) + var writer io.Writer + if !c.Bool("quiet") { + writer = os.Stderr + } + + newImage, err := runtime.ImageRuntime().New(ctx, c.Args()[0], rtc.SignaturePolicyPath, "", writer, nil, image.SigningOptions{}, false) if err != nil { return nil, nil, err } -- cgit v1.2.3-54-g00ecf From 28f5d25e8dabb6647699c2ccab8621cefac37d3b Mon Sep 17 00:00:00 2001 From: baude Date: Thu, 10 Jan 2019 14:39:39 -0600 Subject: Add varlink support for prune Add the ability to prune unused images using the varlink API. Signed-off-by: baude --- API.md | 8 ++++++++ cmd/podman/images_prune.go | 15 +++++++++++++-- cmd/podman/shared/prune.go | 24 ------------------------ cmd/podman/varlink/io.podman.varlink | 4 ++++ pkg/varlinkapi/images.go | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+), 26 deletions(-) delete mode 100644 cmd/podman/shared/prune.go diff --git a/API.md b/API.md index 3722c2864..0cbdffea4 100755 --- a/API.md +++ b/API.md @@ -57,6 +57,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [func ImageExists(name: string) int](#ImageExists) +[func ImagesPrune() []string](#ImagesPrune) + [func ImportImage(source: string, reference: string, message: string, changes: []string) string](#ImportImage) [func InspectContainer(name: string) string](#InspectContainer) @@ -543,6 +545,12 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageExists '{"name": "im "exists": 1 } ~~~ +### func ImagesPrune +
+ +method ImagesPrune() [[]string](#[]string)
+ImagesPrune removes all unused images from the local store. Upon successful pruning, +the IDs of the removed images are returned. ### func ImportImage
diff --git a/cmd/podman/images_prune.go b/cmd/podman/images_prune.go index cb72a498f..06879e02d 100644 --- a/cmd/podman/images_prune.go +++ b/cmd/podman/images_prune.go @@ -1,8 +1,8 @@ package main import ( + "fmt" "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/cmd/podman/shared" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -30,5 +30,16 @@ func pruneImagesCmd(c *cli.Context) error { } defer runtime.Shutdown(false) - return shared.Prune(runtime.ImageRuntime()) + pruneImages, err := runtime.ImageRuntime().GetPruneImages() + if err != nil { + return err + } + + for _, i := range pruneImages { + if err := i.Remove(true); err != nil { + return errors.Wrapf(err, "failed to remove %s", i.ID()) + } + fmt.Println(i.ID()) + } + return nil } diff --git a/cmd/podman/shared/prune.go b/cmd/podman/shared/prune.go deleted file mode 100644 index 90cfe4475..000000000 --- a/cmd/podman/shared/prune.go +++ /dev/null @@ -1,24 +0,0 @@ -package shared - -import ( - "fmt" - "github.com/pkg/errors" - - "github.com/containers/libpod/libpod/image" -) - -// Prune removes all unnamed and unused images from the local store -func Prune(ir *image.Runtime) error { - pruneImages, err := ir.GetPruneImages() - if err != nil { - return err - } - - for _, i := range pruneImages { - if err := i.Remove(true); err != nil { - return errors.Wrapf(err, "failed to remove %s", i.ID()) - } - fmt.Println(i.ID()) - } - return nil -} diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 4e8b69faf..7cd6c16b2 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -1015,6 +1015,10 @@ method MountContainer(name: string) -> (path: string) # ~~~ method UnmountContainer(name: string, force: bool) -> () +# ImagesPrune removes all unused images from the local store. Upon successful pruning, +# the IDs of the removed images are returned. +method ImagesPrune() -> (pruned: []string) + # This function is not implemented yet. method ListContainerPorts(name: string) -> (notimplemented: NotImplemented) diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index 8f8934025..4a4f20031 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -620,3 +620,21 @@ func (i *LibpodAPI) ContainerRunlabel(call iopodman.VarlinkCall, input iopodman. } return call.ReplyContainerRunlabel() } + +// ImagesPrune .... +func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall) error { + var ( + pruned []string + ) + pruneImages, err := i.Runtime.ImageRuntime().GetPruneImages() + if err != nil { + return err + } + for _, i := range pruneImages { + if err := i.Remove(true); err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + pruned = append(pruned, i.ID()) + } + return call.ReplyImagesPrune(pruned) +} -- cgit v1.2.3-54-g00ecf From 431459caf946c1645da31b4fc6c953ad77f9bc85 Mon Sep 17 00:00:00 2001 From: baude Date: Tue, 22 Jan 2019 09:17:34 -0600 Subject: podman image prune -- implement all flag we now, by default, only prune dangling images. if --all is passed, we prune dangling images AND images that do not have an associated containers. also went ahead and enabled the podman-remote image prune side of things. Fixes: #2192 Signed-off-by: baude MH: Removed dependence on remote-client adapter work to limit scale of changes Signed-off-by: Matthew Heon --- API.md | 4 ++-- cmd/podman/images_prune.go | 26 ++++++++++++++---------- cmd/podman/varlink/io.podman.varlink | 2 +- completions/bash/podman | 2 ++ docs/podman-image-prune.1.md | 21 ++++++++++++++++--- libpod/image/prune.go | 39 +++++++++++++++++++++++++++--------- pkg/varlinkapi/images.go | 17 ++++------------ test/e2e/prune_test.go | 5 ++++- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/API.md b/API.md index 0cbdffea4..e4576850b 100755 --- a/API.md +++ b/API.md @@ -57,7 +57,7 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [func ImageExists(name: string) int](#ImageExists) -[func ImagesPrune() []string](#ImagesPrune) +[func ImagesPrune(all: bool) []string](#ImagesPrune) [func ImportImage(source: string, reference: string, message: string, changes: []string) string](#ImportImage) @@ -548,7 +548,7 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageExists '{"name": "im ### func ImagesPrune
-method ImagesPrune() [[]string](#[]string)
+method ImagesPrune(all: [bool](https://godoc.org/builtin#bool)) [[]string](#[]string)
ImagesPrune removes all unused images from the local store. Upon successful pruning, the IDs of the removed images are returned. ### func ImportImage diff --git a/cmd/podman/images_prune.go b/cmd/podman/images_prune.go index 06879e02d..7310137e7 100644 --- a/cmd/podman/images_prune.go +++ b/cmd/podman/images_prune.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/pkg/errors" "github.com/urfave/cli" @@ -13,13 +14,19 @@ var ( Removes all unnamed images from local storage ` - + pruneImageFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "remove all unused images, not just dangling ones", + }, + } pruneImagesCommand = cli.Command{ Name: "prune", Usage: "Remove unused images", Description: pruneImagesDescription, Action: pruneImagesCmd, OnUsageError: usageErrorHandler, + Flags: pruneImageFlags, } ) @@ -30,16 +37,13 @@ func pruneImagesCmd(c *cli.Context) error { } defer runtime.Shutdown(false) - pruneImages, err := runtime.ImageRuntime().GetPruneImages() - if err != nil { - return err - } - - for _, i := range pruneImages { - if err := i.Remove(true); err != nil { - return errors.Wrapf(err, "failed to remove %s", i.ID()) + // Call prune; if any cids are returned, print them and then + // return err in case an error also came up + pruneCids, err := runtime.ImageRuntime().PruneImages(c.Bool("all")) + if len(pruneCids) > 0 { + for _, cid := range pruneCids { + fmt.Println(cid) } - fmt.Println(i.ID()) } - return nil + return err } diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 7cd6c16b2..6f3bf34a2 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -1017,7 +1017,7 @@ method UnmountContainer(name: string, force: bool) -> () # ImagesPrune removes all unused images from the local store. Upon successful pruning, # the IDs of the removed images are returned. -method ImagesPrune() -> (pruned: []string) +method ImagesPrune(all: bool) -> (pruned: []string) # This function is not implemented yet. method ListContainerPorts(name: string) -> (notimplemented: NotImplemented) diff --git a/completions/bash/podman b/completions/bash/podman index 6333dfdf2..410180638 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -2453,6 +2453,8 @@ _podman_images_prune() { " local boolean_options=" + -a + --all -h --help " diff --git a/docs/podman-image-prune.1.md b/docs/podman-image-prune.1.md index db76b26e0..df912c380 100644 --- a/docs/podman-image-prune.1.md +++ b/docs/podman-image-prune.1.md @@ -6,23 +6,38 @@ podman-image-prune - Remove all unused images # SYNOPSIS **podman image prune** +[**-a**|**--all**] [**-h**|**--help**] # DESCRIPTION -**podman image prune** removes all unused images from local storage. An unused image -is defined as an image that does not have any containers based on it. +**podman image prune** removes all dangling images from local storage. With the `all` option, +you can delete all unused images. Unused images are dangling images as well as any image that +does not have any containers based on it. + +## OPTIONS +**--all, -a** + +Remove dangling images and images that have no associated containers. ## Examples ## -Remove all unused images from local storage +Remove all dangling images from local storage ``` $ sudo podman image prune f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e 324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907 +``` + +Remove all unused images from local storage +``` +$ sudo podman image prune -a +f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e +324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907 6125002719feb1ddf3030acab1df6156da7ce0e78e571e9b6e9c250424d6220c 91e732da5657264c6f4641b8d0c4001c218ae6c1adb9dcef33ad00cafd37d8b6 e4e5109420323221f170627c138817770fb64832da7d8fe2babd863148287fca 77a57fa8285e9656dbb7b23d9efa837a106957409ddd702f995605af27a45ebe + ``` ## SEE ALSO diff --git a/libpod/image/prune.go b/libpod/image/prune.go index 6a1f160d5..8602c222c 100644 --- a/libpod/image/prune.go +++ b/libpod/image/prune.go @@ -1,9 +1,11 @@ package image +import "github.com/pkg/errors" + // GetPruneImages returns a slice of images that have no names/unused -func (ir *Runtime) GetPruneImages() ([]*Image, error) { +func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) { var ( - unamedImages []*Image + pruneImages []*Image ) allImages, err := ir.GetImages() if err != nil { @@ -11,16 +13,35 @@ func (ir *Runtime) GetPruneImages() ([]*Image, error) { } for _, i := range allImages { if len(i.Names()) == 0 { - unamedImages = append(unamedImages, i) + pruneImages = append(pruneImages, i) continue } - containers, err := i.Containers() - if err != nil { - return nil, err + if all { + containers, err := i.Containers() + if err != nil { + return nil, err + } + if len(containers) < 1 { + pruneImages = append(pruneImages, i) + } } - if len(containers) < 1 { - unamedImages = append(unamedImages, i) + } + return pruneImages, nil +} + +// PruneImages prunes dangling and optionally all unused images from the local +// image store +func (ir *Runtime) PruneImages(all bool) ([]string, error) { + var prunedCids []string + pruneImages, err := ir.GetPruneImages(all) + if err != nil { + return nil, errors.Wrap(err, "unable to get images to prune") + } + for _, p := range pruneImages { + if err := p.Remove(true); err != nil { + return nil, errors.Wrap(err, "failed to prune image") } + prunedCids = append(prunedCids, p.ID()) } - return unamedImages, nil + return prunedCids, nil } diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index 4a4f20031..cfcdde6ef 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -622,19 +622,10 @@ func (i *LibpodAPI) ContainerRunlabel(call iopodman.VarlinkCall, input iopodman. } // ImagesPrune .... -func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall) error { - var ( - pruned []string - ) - pruneImages, err := i.Runtime.ImageRuntime().GetPruneImages() +func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool) error { + prunedImages, err := i.Runtime.ImageRuntime().PruneImages(all) if err != nil { - return err - } - for _, i := range pruneImages { - if err := i.Remove(true); err != nil { - return call.ReplyErrorOccurred(err.Error()) - } - pruned = append(pruned, i.ID()) + return call.ReplyErrorOccurred(err.Error()) } - return call.ReplyImagesPrune(pruned) + return call.ReplyImagesPrune(prunedImages) } diff --git a/test/e2e/prune_test.go b/test/e2e/prune_test.go index 6679a676c..81fb82b20 100644 --- a/test/e2e/prune_test.go +++ b/test/e2e/prune_test.go @@ -39,6 +39,7 @@ var _ = Describe("Podman rm", func() { }) It("podman container prune containers", func() { + SkipIfRemote() top := podmanTest.RunTopContainer("") top.WaitWithDefaultTimeout() Expect(top.ExitCode()).To(Equal(0)) @@ -55,6 +56,7 @@ var _ = Describe("Podman rm", func() { }) It("podman image prune none images", func() { + SkipIfRemote() podmanTest.BuildImage(pruneImage, "alpine_bash:latest", "true") none := podmanTest.Podman([]string{"images", "-a"}) @@ -72,10 +74,11 @@ var _ = Describe("Podman rm", func() { Expect(none.ExitCode()).To(Equal(0)) hasNoneAfter, _ := after.GrepString("") Expect(hasNoneAfter).To(BeFalse()) + Expect(len(after.OutputToStringArray()) > 1).To(BeTrue()) }) It("podman image prune unused images", func() { - prune := podmanTest.Podman([]string{"image", "prune"}) + prune := podmanTest.Podman([]string{"image", "prune", "-a"}) prune.WaitWithDefaultTimeout() Expect(prune.ExitCode()).To(Equal(0)) -- cgit v1.2.3-54-g00ecf From 53e70e268a849fcba2301f340544a57401337056 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 31 Jan 2019 10:57:49 -0800 Subject: libpod/image: Use RepoDigests() in Inspect() To get the more-robust handling from 0f6535cf (libpod/image: Use ParseNormalizedNamed in RepoDigests, 2019-01-08, #2106) here too. Signed-off-by: W. Trevor King --- libpod/image/image.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libpod/image/image.go b/libpod/image/image.go index 2e12adb70..8b650f25f 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -824,9 +824,9 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) { return nil, err } - var repoDigests []string - for _, name := range i.Names() { - repoDigests = append(repoDigests, strings.SplitN(name, ":", 2)[0]+"@"+i.Digest().String()) + repoDigests, err := i.RepoDigests() + if err != nil { + return nil, err } driver, err := i.DriverData() -- cgit v1.2.3-54-g00ecf From e95f4aa3b0715f87c55a60aa9bf395cbdfc2f3d9 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Tue, 5 Feb 2019 15:37:56 -0500 Subject: Preserve exited state across reboot Instead of unconditionally resetting to ContainerStateConfigured after a reboot, allow containers in the Exited state to remain there, preserving their exit code in podman ps after a reboot. This does not affect the ability to use and restart containers after a reboot, as the Exited state can be used (mostly) interchangeably with Configured for starting and managing containers. Signed-off-by: Matthew Heon --- libpod/container_internal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 0284dde2f..89ca59bbb 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -393,7 +393,9 @@ func resetState(state *containerState) error { state.PID = 0 state.Mountpoint = "" state.Mounted = false - state.State = ContainerStateConfigured + if state.State != ContainerStateExited { + state.State = ContainerStateConfigured + } state.ExecSessions = make(map[string]*ExecSession) state.NetworkStatus = nil state.BindMounts = make(map[string]string) -- cgit v1.2.3-54-g00ecf From c62efd08f75e97ba27d9e34068bcc2b3bf122896 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 11 Jan 2019 11:23:19 +0100 Subject: rootless: create the userns immediately when creating a new pod Closes: https://github.com/containers/libpod/issues/2124 Signed-off-by: Giuseppe Scrivano --- cmd/podman/create.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/podman/create.go b/cmd/podman/create.go index e10009971..a66603360 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -435,6 +435,16 @@ func parseCreateOpts(ctx context.Context, c *cli.Context, runtime *libpod.Runtim } if c.IsSet("pod") { if strings.HasPrefix(originalPodName, "new:") { + if rootless.IsRootless() { + // To create a new pod, we must immediately create the userns. + became, ret, err := rootless.BecomeRootInUserNS() + if err != nil { + return nil, err + } + if became { + os.Exit(ret) + } + } // pod does not exist; lets make it var podOptions []libpod.PodCreateOption podOptions = append(podOptions, libpod.WithPodName(podName), libpod.WithInfraContainer(), libpod.WithPodCgroups()) -- cgit v1.2.3-54-g00ecf From 550d39c3e9602096e456172f6d89eaa04712c1fa Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 11 Jan 2019 10:34:27 +0100 Subject: spec: add nosuid,noexec,nodev to ro bind mount runc fails to change the ro mode of a rootless bind mount if the other flags are not kept. Signed-off-by: Giuseppe Scrivano --- libpod/container_internal_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 2f03d45ea..9c343d051 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -227,7 +227,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { Options: []string{"bind", "private"}, } if c.IsReadOnly() && dstPath != "/dev/shm" { - newMount.Options = append(newMount.Options, "ro") + newMount.Options = append(newMount.Options, "ro", "nosuid", "noexec", "nodev") } if !MountExists(g.Mounts(), dstPath) { g.AddMount(newMount) -- cgit v1.2.3-54-g00ecf From b478f42656115eb83ff42a4a65afc10ec8864ae4 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 11 Jan 2019 11:51:21 +0100 Subject: rootless: join both userns and mount namespace with --pod When --pod is specified then join both the user and mount namespace for the pod so we can initialize the storage. Signed-off-by: Giuseppe Scrivano --- cmd/podman/create.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cmd/podman/create.go b/cmd/podman/create.go index a66603360..8c45b568a 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "os" "path/filepath" "strconv" @@ -809,11 +810,15 @@ func joinOrCreateRootlessUserNamespace(createConfig *cc.CreateConfig, runtime *l if s != libpod.ContainerStateRunning && s != libpod.ContainerStatePaused { continue } - pid, err := prevCtr.PID() + data, err := ioutil.ReadFile(prevCtr.Config().ConmonPidFile) if err != nil { - return false, -1, err + return false, -1, errors.Wrapf(err, "cannot read conmon PID file %q", prevCtr.Config().ConmonPidFile) } - return rootless.JoinNS(uint(pid)) + conmonPid, err := strconv.Atoi(string(data)) + if err != nil { + return false, -1, errors.Wrapf(err, "cannot parse PID %q", data) + } + return rootless.JoinDirectUserAndMountNS(uint(conmonPid)) } } -- cgit v1.2.3-54-g00ecf From 6226e24d40310d451a4c357a6bcc3ab81f27e876 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 8 Feb 2019 14:38:41 -0500 Subject: Fix tests after cherry-picking Signed-off-by: Matthew Heon --- test/e2e/prune_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/e2e/prune_test.go b/test/e2e/prune_test.go index 81fb82b20..149f1ac60 100644 --- a/test/e2e/prune_test.go +++ b/test/e2e/prune_test.go @@ -39,7 +39,6 @@ var _ = Describe("Podman rm", func() { }) It("podman container prune containers", func() { - SkipIfRemote() top := podmanTest.RunTopContainer("") top.WaitWithDefaultTimeout() Expect(top.ExitCode()).To(Equal(0)) @@ -56,7 +55,6 @@ var _ = Describe("Podman rm", func() { }) It("podman image prune none images", func() { - SkipIfRemote() podmanTest.BuildImage(pruneImage, "alpine_bash:latest", "true") none := podmanTest.Podman([]string{"images", "-a"}) -- cgit v1.2.3-54-g00ecf From 2c74edd0ac6509d6e533cb4e012e3e3f9e03434d Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Mon, 11 Feb 2019 08:57:41 -0500 Subject: Bump to v1.0.1 Signed-off-by: Matthew Heon --- changelog.txt | 17 +++++++++++++++++ version/version.go | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 8ee11cdc4..836ff7ed7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,20 @@ +- Changelog for v1.0.1 (2019-02-11): + * Fix tests after cherry-picking + * rootless: join both userns and mount namespace with --pod + * spec: add nosuid,noexec,nodev to ro bind mount + * rootless: create the userns immediately when creating a new pod + * Preserve exited state across reboot + * libpod/image: Use RepoDigests() in Inspect() + * podman image prune -- implement all flag + * Add varlink support for prune + * Make --quiet work in podman create/run + * Show a better error message when podman info fails during a refresh + * Vendor in latest opencontainers/selinux + * rootless: fix --pid=host without --privileged + * Do not unmarshal into c.config.Spec + * podman-inspect: don't ignore errors + * Ensure that wait exits on state transition + - Changelog for v1.0.0 (2018-1-11) * Update release notes for v1.0 * Remove clientintegration from Makefile diff --git a/version/version.go b/version/version.go index ea5a92286..30a589635 100644 --- a/version/version.go +++ b/version/version.go @@ -4,4 +4,4 @@ package version // NOTE: remember to bump the version at the top // of the top-level README.md file when this is // bumped. -const Version = "1.0.1-dev" +const Version = "1.0.1" -- cgit v1.2.3-54-g00ecf From 7577d7e1efb40626cb06a43717e7e211c79b7fa3 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Mon, 11 Feb 2019 08:58:22 -0500 Subject: Bump to v1.0.2-dev Signed-off-by: Matthew Heon --- contrib/spec/podman.spec.in | 2 +- version/version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index bf75522dc..acc12fc8c 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -39,7 +39,7 @@ %global shortcommit_conmon %(c=%{commit_conmon}; echo ${c:0:7}) Name: podman -Version: 1.0.1 +Version: 1.0.2 Release: #COMMITDATE#.git%{shortcommit0}%{?dist} Summary: Manage Pods, Containers and Container Images License: ASL 2.0 diff --git a/version/version.go b/version/version.go index 30a589635..99e30f002 100644 --- a/version/version.go +++ b/version/version.go @@ -4,4 +4,4 @@ package version // NOTE: remember to bump the version at the top // of the top-level README.md file when this is // bumped. -const Version = "1.0.1" +const Version = "1.0.2-dev" -- cgit v1.2.3-54-g00ecf From cbeca379250932f01fa4e2ffc70e9c2ecd6701bc Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Mon, 11 Feb 2019 09:35:19 -0500 Subject: Up timeout for build_each_commit Lots of backports = lots of commits = long timeouts needed. Signed-off-by: Matthew Heon --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 67af6f69a..443410847 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -115,7 +115,7 @@ build_each_commit_task: matrix: image_name: "fedora-29-libpod-9afa57a9" - timeout_in: 20m + timeout_in: 40m script: - $SCRIPT_BASE/setup_environment.sh -- cgit v1.2.3-54-g00ecf