diff options
Diffstat (limited to 'cmd/podman')
-rw-r--r-- | cmd/podman/cliconfig/config.go | 4 | ||||
-rw-r--r-- | cmd/podman/cliconfig/create.go | 1 | ||||
-rw-r--r-- | cmd/podman/commands.go | 16 | ||||
-rw-r--r-- | cmd/podman/commands_remoteclient.go | 10 | ||||
-rw-r--r-- | cmd/podman/cp.go | 21 | ||||
-rw-r--r-- | cmd/podman/create.go | 25 | ||||
-rw-r--r-- | cmd/podman/healthcheck.go | 25 | ||||
-rw-r--r-- | cmd/podman/healthcheck_run.go | 53 | ||||
-rw-r--r-- | cmd/podman/main.go | 1 | ||||
-rw-r--r-- | cmd/podman/pod.go | 3 | ||||
-rw-r--r-- | cmd/podman/pod_stats.go | 47 | ||||
-rw-r--r-- | cmd/podman/pod_top.go | 31 | ||||
-rw-r--r-- | cmd/podman/varlink/io.podman.varlink | 15 | ||||
-rw-r--r-- | cmd/podman/wait.go | 53 |
14 files changed, 192 insertions, 113 deletions
diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index ea99aafc2..7945cb6cb 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -217,6 +217,10 @@ type PauseValues struct { All bool } +type HealthCheckValues struct { + PodmanCommand +} + type KubePlayValues struct { PodmanCommand Authfile string diff --git a/cmd/podman/cliconfig/create.go b/cmd/podman/cliconfig/create.go index b5ca1be9c..49ab3d827 100644 --- a/cmd/podman/cliconfig/create.go +++ b/cmd/podman/cliconfig/create.go @@ -23,4 +23,5 @@ type BuildValues struct { type CpValues struct { PodmanCommand + Extract bool } diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index c261e54e2..d37af70c1 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -35,7 +35,6 @@ func getMainCommands() []*cobra.Command { _topCommand, _umountCommand, _unpauseCommand, - _waitCommand, } if len(_varlinkCommand.Use) > 0 { @@ -85,14 +84,6 @@ func getContainerSubCommands() []*cobra.Command { } } -// Commands that the local client implements -func getPodSubCommands() []*cobra.Command { - return []*cobra.Command{ - _podStatsCommand, - _podTopCommand, - } -} - func getGenerateSubCommands() []*cobra.Command { return []*cobra.Command{ _containerKubeCommand, @@ -121,3 +112,10 @@ func getSystemSubCommands() []*cobra.Command { _renumberCommand, } } + +// Commands that the local client implements +func getHealtcheckSubCommands() []*cobra.Command { + return []*cobra.Command{ + _healthcheckrunCommand, + } +} diff --git a/cmd/podman/commands_remoteclient.go b/cmd/podman/commands_remoteclient.go index 081043b25..9b09e7dbc 100644 --- a/cmd/podman/commands_remoteclient.go +++ b/cmd/podman/commands_remoteclient.go @@ -29,11 +29,6 @@ func getContainerSubCommands() []*cobra.Command { } // commands that only the remoteclient implements -func getPodSubCommands() []*cobra.Command { - return []*cobra.Command{} -} - -// commands that only the remoteclient implements func getGenerateSubCommands() []*cobra.Command { return []*cobra.Command{} } @@ -52,3 +47,8 @@ func getTrustSubCommands() []*cobra.Command { func getSystemSubCommands() []*cobra.Command { return []*cobra.Command{} } + +// Commands that the remoteclient implements +func getHealtcheckSubCommands() []*cobra.Command { + return []*cobra.Command{} +} diff --git a/cmd/podman/cp.go b/cmd/podman/cp.go index 5958370b6..a3411b158 100644 --- a/cmd/podman/cp.go +++ b/cmd/podman/cp.go @@ -43,6 +43,8 @@ var ( func init() { cpCommand.Command = _cpCommand + flags := cpCommand.Flags() + flags.BoolVar(&cpCommand.Extract, "extract", false, "Extract the tar file into the destination directory.") rootCmd.AddCommand(cpCommand.Command) } @@ -61,10 +63,11 @@ func cpCmd(c *cliconfig.CpValues) error { } defer runtime.Shutdown(false) - return copyBetweenHostAndContainer(runtime, args[0], args[1]) + extract := c.Flag("extract").Changed + return copyBetweenHostAndContainer(runtime, args[0], args[1], extract) } -func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest string) error { +func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest string, extract bool) error { srcCtr, srcPath := parsePath(runtime, src) destCtr, destPath := parsePath(runtime, dest) @@ -166,7 +169,7 @@ func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest strin var lastError error for _, src := range glob { - err := copy(src, destPath, dest, idMappingOpts, &containerOwner) + err := copy(src, destPath, dest, idMappingOpts, &containerOwner, extract) if lastError != nil { logrus.Error(lastError) } @@ -219,7 +222,7 @@ func getPathInfo(path string) (string, os.FileInfo, error) { return path, srcfi, nil } -func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair) error { +func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract bool) error { srcPath, err := filepath.EvalSymlinks(src) if err != nil { return errors.Wrapf(err, "error evaluating symlinks %q", srcPath) @@ -240,6 +243,7 @@ func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, ch // return functions for copying items copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) + untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) if srcfi.IsDir() { @@ -263,6 +267,15 @@ func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, ch destPath = filepath.Join(destPath, filepath.Base(srcPath)) } } + + if extract { + // We're extracting an archive into the destination directory. + logrus.Debugf("extracting contents of %q into %q", srcPath, destPath) + if err = untarPath(srcPath, destPath); err != nil { + return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath) + } + return nil + } // Copy the file, preserving attributes. logrus.Debugf("copying %q to %q", srcPath, destPath) if err = copyFileWithTar(srcPath, destPath); err != nil { diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 129c886b2..a7b9bbf31 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -12,6 +12,7 @@ import ( "strings" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/shared" @@ -117,6 +118,10 @@ func createInit(c *cliconfig.PodmanCommand) error { } func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) { + var ( + hasHealthCheck bool + healthCheck *manifest.Schema2HealthConfig + ) if c.Bool("trace") { span, _ := opentracing.StartSpanFromContext(Ctx, "createContainer") defer span.Finish() @@ -163,12 +168,32 @@ func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libp } else { imageName = newImage.ID() } + + // add healthcheck if it exists AND is correct mediatype + _, mediaType, err := newImage.Manifest(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to determine mediatype of image %s", newImage.ID()) + } + if mediaType == manifest.DockerV2Schema2MediaType { + healthCheck, err = newImage.GetHealthCheck(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to get healthcheck for %s", c.InputArgs[0]) + } + if healthCheck != nil { + hasHealthCheck = true + } + } } createConfig, err := parseCreateOpts(ctx, c, runtime, imageName, data) if err != nil { return nil, nil, err } + // Because parseCreateOpts does derive anything from the image, we add health check + // at this point. The rest is done by WithOptions. + createConfig.HasHealthCheck = hasHealthCheck + createConfig.HealthCheck = healthCheck + ctr, err := createContainerFromCreateConfig(runtime, createConfig, ctx, nil) if err != nil { return nil, nil, err diff --git a/cmd/podman/healthcheck.go b/cmd/podman/healthcheck.go new file mode 100644 index 000000000..e7cc125cc --- /dev/null +++ b/cmd/podman/healthcheck.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/spf13/cobra" +) + +var healthcheckDescription = "Manage health checks on containers" +var healthcheckCommand = cliconfig.PodmanCommand{ + Command: &cobra.Command{ + Use: "healthcheck", + Short: "Manage Healthcheck", + Long: healthcheckDescription, + }, +} + +// Commands that are universally implemented +var healthcheckCommands []*cobra.Command + +func init() { + healthcheckCommand.AddCommand(healthcheckCommands...) + healthcheckCommand.AddCommand(getHealtcheckSubCommands()...) + healthcheckCommand.SetUsageTemplate(UsageTemplate()) + rootCmd.AddCommand(healthcheckCommand.Command) +} diff --git a/cmd/podman/healthcheck_run.go b/cmd/podman/healthcheck_run.go new file mode 100644 index 000000000..d92b2ac01 --- /dev/null +++ b/cmd/podman/healthcheck_run.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + healthcheckRunCommand cliconfig.HealthCheckValues + healthcheckRunDescription = "run the health check of a container" + _healthcheckrunCommand = &cobra.Command{ + Use: "run [flags] CONTAINER", + Short: "run the health check of a container", + Long: healthcheckRunDescription, + Example: `podman healthcheck run mywebapp`, + RunE: func(cmd *cobra.Command, args []string) error { + healthcheckRunCommand.InputArgs = args + healthcheckRunCommand.GlobalFlags = MainGlobalOpts + return healthCheckCmd(&healthcheckRunCommand) + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 || len(args) > 1 { + return errors.New("must provide the name or ID of one container") + } + return nil + }, + } +) + +func init() { + healthcheckRunCommand.Command = _healthcheckrunCommand + healthcheckRunCommand.SetUsageTemplate(UsageTemplate()) +} + +func healthCheckCmd(c *cliconfig.HealthCheckValues) error { + runtime, err := adapter.GetRuntime(&c.PodmanCommand) + if err != nil { + return errors.Wrap(err, "could not get runtime") + } + status, err := runtime.HealthCheck(c) + if err != nil { + if status == libpod.HealthCheckFailure { + fmt.Println("\nunhealthy") + } + return err + } + fmt.Println("\nhealthy") + return nil +} diff --git a/cmd/podman/main.go b/cmd/podman/main.go index b3faf05c0..97ffa8930 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -52,6 +52,7 @@ var mainCommands = []*cobra.Command{ _stopCommand, _tagCommand, _versionCommand, + _waitCommand, imageCommand.Command, systemCommand.Command, } diff --git a/cmd/podman/pod.go b/cmd/podman/pod.go index c1350bd4d..7067f2429 100644 --- a/cmd/podman/pod.go +++ b/cmd/podman/pod.go @@ -29,12 +29,13 @@ var podSubCommands = []*cobra.Command{ _podRestartCommand, _podRmCommand, _podStartCommand, + _podStatsCommand, _podStopCommand, + _podTopCommand, _podUnpauseCommand, } func init() { podCommand.AddCommand(podSubCommands...) - podCommand.AddCommand(getPodSubCommands()...) podCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/pod_stats.go b/cmd/podman/pod_stats.go index f5edd21f8..2761ce9cc 100644 --- a/cmd/podman/pod_stats.go +++ b/cmd/podman/pod_stats.go @@ -13,8 +13,8 @@ import ( tm "github.com/buger/goterm" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/formats" - "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/ulule/deepcopier" @@ -51,10 +51,6 @@ func init() { } func podStatsCmd(c *cliconfig.PodStatsValues) error { - var ( - podFunc func() ([]*libpod.Pod, error) - ) - format := c.Format all := c.All latest := c.Latest @@ -76,7 +72,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { all = true } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { return errors.Wrapf(err, "could not get runtime") } @@ -87,29 +83,12 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { times = 1 } - if len(c.InputArgs) > 0 { - podFunc = func() ([]*libpod.Pod, error) { return getPodsByList(c.InputArgs, runtime) } - } else if latest { - podFunc = func() ([]*libpod.Pod, error) { - latestPod, err := runtime.GetLatestPod() - if err != nil { - return nil, err - } - return []*libpod.Pod{latestPod}, err - } - } else if all { - podFunc = runtime.GetAllPods - } else { - podFunc = runtime.GetRunningPods - } - - pods, err := podFunc() + pods, err := runtime.GetStatPods(c) if err != nil { return errors.Wrapf(err, "unable to get a list of pods") } - // First we need to get an initial pass of pod/ctr stats (these are not printed) - var podStats []*libpod.PodContainerStats + var podStats []*adapter.PodContainerStats for _, p := range pods { cons, err := p.AllContainersByID() if err != nil { @@ -120,7 +99,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { for _, c := range cons { emptyStats[c] = &libpod.ContainerStats{} } - ps := libpod.PodContainerStats{ + ps := adapter.PodContainerStats{ Pod: p, ContainerStats: emptyStats, } @@ -128,10 +107,10 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { } // Create empty container stat results for our first pass - var previousPodStats []*libpod.PodContainerStats + var previousPodStats []*adapter.PodContainerStats for _, p := range pods { cs := make(map[string]*libpod.ContainerStats) - pcs := libpod.PodContainerStats{ + pcs := adapter.PodContainerStats{ Pod: p, ContainerStats: cs, } @@ -164,7 +143,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { } for i := 0; i < times; i += step { - var newStats []*libpod.PodContainerStats + var newStats []*adapter.PodContainerStats for _, p := range pods { prevStat := getPreviousPodContainerStats(p.ID(), previousPodStats) newPodStats, err := p.GetPodStats(prevStat) @@ -174,7 +153,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { if err != nil { return err } - newPod := libpod.PodContainerStats{ + newPod := adapter.PodContainerStats{ Pod: p, ContainerStats: newPodStats, } @@ -202,7 +181,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { time.Sleep(time.Second) previousPodStats := new([]*libpod.PodContainerStats) deepcopier.Copy(newStats).To(previousPodStats) - pods, err = podFunc() + pods, err = runtime.GetStatPods(c) if err != nil { return err } @@ -211,7 +190,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { return nil } -func podContainerStatsToPodStatOut(stats []*libpod.PodContainerStats) []*podStatOut { +func podContainerStatsToPodStatOut(stats []*adapter.PodContainerStats) []*podStatOut { var out []*podStatOut for _, p := range stats { for _, c := range p.ContainerStats { @@ -295,7 +274,7 @@ func outputToStdOut(stats []*podStatOut) { w.Flush() } -func getPreviousPodContainerStats(podID string, prev []*libpod.PodContainerStats) map[string]*libpod.ContainerStats { +func getPreviousPodContainerStats(podID string, prev []*adapter.PodContainerStats) map[string]*libpod.ContainerStats { for _, p := range prev { if podID == p.Pod.ID() { return p.ContainerStats @@ -304,7 +283,7 @@ func getPreviousPodContainerStats(podID string, prev []*libpod.PodContainerStats return map[string]*libpod.ContainerStats{} } -func outputJson(stats []*libpod.PodContainerStats) error { +func outputJson(stats []*adapter.PodContainerStats) error { b, err := json.MarshalIndent(&stats, "", " ") if err != nil { return err diff --git a/cmd/podman/pod_top.go b/cmd/podman/pod_top.go index 6a26e3dff..c5383d376 100644 --- a/cmd/podman/pod_top.go +++ b/cmd/podman/pod_top.go @@ -2,13 +2,12 @@ package main import ( "fmt" + "github.com/containers/libpod/pkg/adapter" "os" "strings" "text/tabwriter" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -50,8 +49,9 @@ func init() { } func podTopCmd(c *cliconfig.PodTopValues) error { - var pod *libpod.Pod - var err error + var ( + descriptors []string + ) args := c.InputArgs if c.ListDescriptors { @@ -67,39 +67,22 @@ func podTopCmd(c *cliconfig.PodTopValues) error { return errors.Errorf("you must provide the name or id of a running pod") } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { return errors.Wrapf(err, "error creating libpod runtime") } defer runtime.Shutdown(false) - var descriptors []string if c.Latest { descriptors = args - pod, err = runtime.GetLatestPod() } else { descriptors = args[1:] - pod, err = runtime.LookupPod(args[0]) } - - if err != nil { - return errors.Wrapf(err, "unable to lookup requested container") - } - - podStatus, err := shared.GetPodStatus(pod) - if err != nil { - return err - } - if podStatus != "Running" { - return errors.Errorf("pod top can only be used on pods with at least one running container") - } - - psOutput, err := pod.GetPodPidInformation(descriptors) + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + psOutput, err := runtime.PodTop(c, descriptors) if err != nil { return err } - - w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) for _, proc := range psOutput { fmt.Fprintln(w, proc) } diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 73e6c15f9..6109bd290 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -549,6 +549,10 @@ method ExportContainer(name: string, path: string) -> (tarfile: string) # ~~~ method GetContainerStats(name: string) -> (container: ContainerStats) +# GetContainerStatsWithHistory takes a previous set of container statistics and uses libpod functions +# to calculate the containers statistics based on current and previous measurements. +method GetContainerStatsWithHistory(previousStats: ContainerStats) -> (container: ContainerStats) + # This method has not be implemented yet. # method ResizeContainerTty() -> (notimplemented: NotImplemented) @@ -617,10 +621,10 @@ method UnpauseContainer(name: string) -> (container: string) # ~~~ method GetAttachSockets(name: string) -> (sockets: Sockets) -# WaitContainer takes the name or ID of a container and waits until the container stops. Upon stopping, the return -# code of the container is returned. If the container container cannot be found by ID or name, -# a [ContainerNotFound](#ContainerNotFound) error is returned. -method WaitContainer(name: string) -> (exitcode: int) +# WaitContainer takes the name or ID of a container and waits the given interval in milliseconds until the container +# stops. Upon stopping, the return code of the container is returned. If the container container cannot be found by ID +# or name, a [ContainerNotFound](#ContainerNotFound) error is returned. +method WaitContainer(name: string, interval: int) -> (exitcode: int) # RemoveContainer requires the name or ID of container as well a boolean representing whether a running container can be stopped and removed, and a boolean # indicating whether to remove builtin volumes. Upon successful removal of the @@ -952,8 +956,7 @@ method RemovePod(name: string, force: bool) -> (pod: string) # This method has not be implemented yet. # method WaitPod() -> (notimplemented: NotImplemented) -# This method has not been implemented yet. -# method TopPod() -> (notimplemented: NotImplemented) +method TopPod(pod: string, latest: bool, descriptors: []string) -> (stats: []string) # GetPodStats takes the name or ID of a pod and returns a pod name and slice of ContainerStats structure which # contains attributes like memory and cpu usage. If the pod cannot be found, a [PodNotFound](#PodNotFound) diff --git a/cmd/podman/wait.go b/cmd/podman/wait.go index 9df2e3208..6c2a8c9ff 100644 --- a/cmd/podman/wait.go +++ b/cmd/podman/wait.go @@ -2,11 +2,11 @@ package main import ( "fmt" - "os" + "reflect" "time" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -49,43 +49,36 @@ func waitCmd(c *cliconfig.WaitValues) error { return errors.Errorf("you must provide at least one container name or id") } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + if c.Interval == 0 { + return errors.Errorf("interval must be greater then 0") + } + interval := time.Duration(c.Interval) * time.Millisecond + + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { - return errors.Wrapf(err, "error creating libpod runtime") + return errors.Wrapf(err, "error creating runtime") } defer runtime.Shutdown(false) + ok, failures, err := runtime.WaitOnContainers(getContext(), c, interval) if err != nil { - return errors.Wrapf(err, "could not get config") + return err } - var lastError error - if c.Latest { - latestCtr, err := runtime.GetLatestContainer() - if err != nil { - return errors.Wrapf(err, "unable to wait on latest container") - } - args = append(args, latestCtr.ID()) + for _, id := range ok { + fmt.Println(id) } - for _, container := range args { - ctr, err := runtime.LookupContainer(container) - if err != nil { - return errors.Wrapf(err, "unable to find container %s", container) - } - if c.Interval == 0 { - return errors.Errorf("interval must be greater then 0") - } - returnCode, err := ctr.WaitWithInterval(time.Duration(c.Interval) * time.Millisecond) - if err != nil { - if lastError != nil { - fmt.Fprintln(os.Stderr, lastError) - } - lastError = errors.Wrapf(err, "failed to wait for the container %v", container) - } else { - fmt.Println(returnCode) + if len(failures) > 0 { + keys := reflect.ValueOf(failures).MapKeys() + lastKey := keys[len(keys)-1].String() + lastErr := failures[lastKey] + delete(failures, lastKey) + + for _, err := range failures { + outputError(err) } + return lastErr } - - return lastError + return nil } |