diff options
29 files changed, 496 insertions, 117 deletions
@@ -5,7 +5,7 @@ Podman (the POD MANager) is a tool for managing containers and images, volumes mounted into those containers, and pods made from groups of containers. Podman is based on libpod, a library for container lifecycle management that is also contained in this repository. The libpod library provides APIs for managing containers, pods, container images, and volumes. -* [Latest Version: 2.1.0](https://github.com/containers/podman/releases/latest) +* [Latest Version: 2.1.1](https://github.com/containers/podman/releases/latest) * Latest Remote client for Windows * Latest Remote client for MacOs * Latest Static Remote client for Linux diff --git a/cmd/podman/common/createparse.go b/cmd/podman/common/createparse.go index 059f9050f..09ee5aa0c 100644 --- a/cmd/podman/common/createparse.go +++ b/cmd/podman/common/createparse.go @@ -10,7 +10,7 @@ import ( func (c *ContainerCLIOpts) validate() error { var () if c.Rm && c.Restart != "" && c.Restart != "no" { - return errors.Errorf("the --rm option conflicts with --restart") + return errors.Errorf(`the --rm option conflicts with --restart, when the restartPolicy is not "" and "no"`) } if _, err := util.ValidatePullType(c.Pull); err != nil { diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index e7b88eb3f..84ae70b6a 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -233,7 +233,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string // validate flags as needed if err := c.validate(); err != nil { - return nil + return err } s.User = c.User diff --git a/cmd/podman/containers/diff.go b/cmd/podman/containers/diff.go index 227c13f4c..a3ca6edf9 100644 --- a/cmd/podman/containers/diff.go +++ b/cmd/podman/containers/diff.go @@ -1,6 +1,7 @@ package containers import ( + "github.com/containers/podman/v2/cmd/podman/parse" "github.com/containers/podman/v2/cmd/podman/registry" "github.com/containers/podman/v2/cmd/podman/report" "github.com/containers/podman/v2/cmd/podman/validate" @@ -52,11 +53,11 @@ func diff(cmd *cobra.Command, args []string) error { return err } - switch diffOpts.Format { - case "": - return report.ChangesToTable(results) - case "json": + switch { + case parse.MatchesJSONFormat(diffOpts.Format): return report.ChangesToJSON(results) + case diffOpts.Format == "": + return report.ChangesToTable(results) default: return errors.New("only supported value for '--format' is 'json'") } diff --git a/cmd/podman/images/list.go b/cmd/podman/images/list.go index ffb341fc4..239da9d28 100644 --- a/cmd/podman/images/list.go +++ b/cmd/podman/images/list.go @@ -11,7 +11,9 @@ import ( "unicode" "github.com/containers/image/v5/docker/reference" + "github.com/containers/podman/v2/cmd/podman/parse" "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/report" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/docker/go-units" "github.com/pkg/errors" @@ -106,9 +108,12 @@ func images(cmd *cobra.Command, args []string) error { switch { case listFlag.quiet: return writeID(imgs) - case cmd.Flag("format").Changed && listFlag.format == "json": + case parse.MatchesJSONFormat(listFlag.format): return writeJSON(imgs) default: + if cmd.Flag("format").Changed { + listFlag.noHeading = true // V1 compatibility + } return writeTemplate(imgs) } } @@ -156,25 +161,29 @@ func writeJSON(images []imageReporter) error { } func writeTemplate(imgs []imageReporter) error { - var ( - hdr, row string - ) - if len(listFlag.format) < 1 { - hdr, row = imageListFormat(listFlag) + hdrs := report.Headers(imageReporter{}, map[string]string{ + "ID": "IMAGE ID", + "ReadOnly": "R/O", + }) + + var row string + if listFlag.format == "" { + row = lsFormatFromFlags(listFlag) } else { - row = listFlag.format - if !strings.HasSuffix(row, "\n") { - row += "\n" - } + row = report.NormalizeFormat(listFlag.format) } - format := hdr + "{{range . }}" + row + "{{end}}" - tmpl, err := template.New("list").Parse(format) - if err != nil { - return err - } - tmpl = template.Must(tmpl, nil) + + format := "{{range . }}" + row + "{{end}}" + tmpl := template.Must(template.New("list").Parse(format)) w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) defer w.Flush() + + if !listFlag.noHeading { + if err := tmpl.Execute(w, hdrs); err != nil { + return err + } + } + return tmpl.Execute(w, imgs) } @@ -276,40 +285,27 @@ func sortFunc(key string, data []imageReporter) func(i, j int) bool { } } -func imageListFormat(flags listFlagType) (string, string) { - // Defaults - hdr := "REPOSITORY\tTAG" - row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}" +func lsFormatFromFlags(flags listFlagType) string { + row := []string{ + "{{if .Repository}}{{.Repository}}{{else}}<none>{{end}}", + "{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}", + } if flags.digests { - hdr += "\tDIGEST" - row += "\t{{.Digest}}" + row = append(row, "{{.Digest}}") } - hdr += "\tIMAGE ID" - row += "\t{{.ID}}" - - hdr += "\tCREATED\tSIZE" - row += "\t{{.Created}}\t{{.Size}}" + row = append(row, "{{.ID}}", "{{.Created}}", "{{.Size}}") if flags.history { - hdr += "\tHISTORY" - row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}" + row = append(row, "{{if .History}}{{.History}}{{else}}<none>{{end}}") } if flags.readOnly { - hdr += "\tReadOnly" - row += "\t{{.ReadOnly}}" - } - - if flags.noHeading { - hdr = "" - } else { - hdr += "\n" + row = append(row, "{{.ReadOnly}}") } - row += "\n" - return hdr, row + return strings.Join(row, "\t") + "\n" } type imageReporter struct { diff --git a/cmd/podman/parse/json.go b/cmd/podman/parse/json.go index 95a6633b8..40ac415db 100644 --- a/cmd/podman/parse/json.go +++ b/cmd/podman/parse/json.go @@ -2,8 +2,9 @@ package parse import "regexp" -var jsonFormatRegex = regexp.MustCompile(`^(\s*json\s*|\s*{{\s*json\s*\.\s*}}\s*)$`) +var jsonFormatRegex = regexp.MustCompile(`^\s*(json|{{\s*json\s*( \.)?\s*}})\s*$`) +// MatchesJSONFormat test CLI --format string to be a JSON request func MatchesJSONFormat(s string) bool { return jsonFormatRegex.Match([]byte(s)) } diff --git a/cmd/podman/parse/json_test.go b/cmd/podman/parse/json_test.go index 5cad185fd..ec3b5664b 100644 --- a/cmd/podman/parse/json_test.go +++ b/cmd/podman/parse/json_test.go @@ -1,6 +1,8 @@ package parse import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -13,18 +15,31 @@ func TestMatchesJSONFormat(t *testing.T) { }{ {"json", true}, {" json", true}, - {"json ", true}, + {" json ", true}, {" json ", true}, + {"{{json}}", true}, + {"{{json }}", true}, {"{{json .}}", true}, {"{{ json .}}", true}, - {"{{json . }}", true}, - {" {{ json . }} ", true}, - {"{{json }}", false}, - {"{{json .", false}, + {"{{ json . }}", true}, + {" {{ json . }} ", true}, + {"{{ json .", false}, {"json . }}", false}, + {"{{.ID }} json .", false}, + {"json .", false}, + {"{{json.}}", false}, } for _, tt := range tests { assert.Equal(t, tt.expected, MatchesJSONFormat(tt.input)) } + + for _, tc := range tests { + tc := tc + label := "MatchesJSONFormat/" + strings.ReplaceAll(tc.input, " ", "_") + t.Run(label, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, MatchesJSONFormat(tc.input), fmt.Sprintf("Scanning %q failed", tc.input)) + }) + } } diff --git a/cmd/podman/report/format.go b/cmd/podman/report/format.go new file mode 100644 index 000000000..32d92bec5 --- /dev/null +++ b/cmd/podman/report/format.go @@ -0,0 +1,68 @@ +package report + +import ( + "reflect" + "strings" +) + +// tableReplacer will remove 'table ' prefix and clean up tabs +var tableReplacer = strings.NewReplacer( + "table ", "", + `\t`, "\t", + `\n`, "\n", + " ", "\t", +) + +// escapedReplacer will clean up escaped characters from CLI +var escapedReplacer = strings.NewReplacer( + `\t`, "\t", + `\n`, "\n", +) + +// NormalizeFormat reads given go template format provided by CLI and munges it into what we need +func NormalizeFormat(format string) string { + f := format + // two replacers used so we only remove the prefix keyword `table` + if strings.HasPrefix(f, "table ") { + f = tableReplacer.Replace(f) + } else { + f = escapedReplacer.Replace(format) + } + + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + + return f +} + +// Headers queries the interface for field names +func Headers(object interface{}, overrides map[string]string) []map[string]string { + value := reflect.ValueOf(object) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + // Column header will be field name upper-cased. + headers := make(map[string]string, value.NumField()) + for i := 0; i < value.Type().NumField(); i++ { + field := value.Type().Field(i) + // Recurse to find field names from promoted structs + if field.Type.Kind() == reflect.Struct && field.Anonymous { + h := Headers(reflect.New(field.Type).Interface(), nil) + for k, v := range h[0] { + headers[k] = v + } + continue + } + headers[field.Name] = strings.ToUpper(field.Name) + } + + if len(overrides) > 0 { + // Override column header as provided + for k, v := range overrides { + headers[k] = strings.ToUpper(v) + } + } + return []map[string]string{headers} +} diff --git a/cmd/podman/report/format_test.go b/cmd/podman/report/format_test.go new file mode 100644 index 000000000..7dd62e899 --- /dev/null +++ b/cmd/podman/report/format_test.go @@ -0,0 +1,35 @@ +package report + +import ( + "strings" + "testing" +) + +func TestNormalizeFormat(t *testing.T) { + cases := []struct { + format string + expected string + }{ + {"table {{.ID}}", "{{.ID}}\n"}, + {"table {{.ID}} {{.C}}", "{{.ID}}\t{{.C}}\n"}, + {"{{.ID}}", "{{.ID}}\n"}, + {"{{.ID}}\n", "{{.ID}}\n"}, + {"{{.ID}} {{.C}}", "{{.ID}} {{.C}}\n"}, + {"\t{{.ID}}", "\t{{.ID}}\n"}, + {`\t` + "{{.ID}}", "\t{{.ID}}\n"}, + {"table {{.ID}}\t{{.C}}", "{{.ID}}\t{{.C}}\n"}, + {"{{.ID}} table {{.C}}", "{{.ID}} table {{.C}}\n"}, + } + for _, tc := range cases { + tc := tc + + label := strings.ReplaceAll(tc.format, " ", "<sp>") + t.Run("NormalizeFormat/"+label, func(t *testing.T) { + t.Parallel() + actual := NormalizeFormat(tc.format) + if actual != tc.expected { + t.Errorf("Expected %q, actual %q", tc.expected, actual) + } + }) + } +} diff --git a/completions/bash/podman b/completions/bash/podman index a83cfc790..e12862126 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -407,7 +407,7 @@ __podman_local_interfaces() { __podman_complete_restart() { case "$prev" in --restart) - COMPREPLY=( $( compgen -W "always no on-failure" -- "$cur") ) + COMPREPLY=( $( compgen -W "always no on-failure unless-stopped" -- "$cur") ) return ;; esac diff --git a/contrib/cirrus/logformatter b/contrib/cirrus/logformatter index f97638b6f..2ab8aa117 100755 --- a/contrib/cirrus/logformatter +++ b/contrib/cirrus/logformatter @@ -31,6 +31,12 @@ our $CSS = <<'END_CSS'; /* wrap long lines - don't require user to scroll right */ pre { line-break: normal; overflow-wrap: normal; white-space: pre-wrap; } +/* synopsis table at top */ +table.synopsis { border: none; border-collapse: collapse; margin-left: 2em; margin-top: 2ex; } +.synopsis th { font-weight: normal; font-size: 110%; text-align: right; } +.synopsis td { font-weight: bold; font-size: 120%; font-family: monospace; } + +/* test results */ .boring { color: #999; } .timestamp { color: #999; } .log-debug { color: #999; } @@ -171,9 +177,20 @@ window.addEventListener("load", scrollToBottom, false); </script> </head> <body> -<pre> <!-- begin processed output --> END_HTML + # Synopsis of this job: show job environment, links to PR and Cirrus + print { $out_fh } "<h2>Synopsis</h2>\n<hr/>\n", + job_synopsis($test_name), "<hr/>\n"; + + # FOR DEBUGGING: dump environment, but in HTML comments to not clutter + print { $out_fh } "<!-- Environment: -->\n"; + for my $e (sort keys %ENV) { + my $val = escapeHTML($ENV{$e}); + $val =~ s/--/--/g; # double dash not valid in comments + printf { $out_fh } "<!-- %-20s %s -->\n", $e, $val; + } + # State variables my $previous_timestamp = ''; # timestamp of previous line my $cirrus_task; # Cirrus task number, used for linking @@ -185,6 +202,8 @@ END_HTML my $looks_like_bats; # binary flag: for detecting BATS results my %bats_count; # For summary line: count of pass/fail/skip + print { $out_fh } "<pre> <!-- begin processed output -->\n"; + # Main loop: read input, one line at a time, and write out reformatted LINE: while (my $line = <STDIN>) { @@ -232,8 +251,9 @@ END_HTML my $css; # Readability: /long/path/to/podman -> podman (hover for full path) - $line =~ s{^(#\s+(#|\$)\s+)(\S+/)(podman\S*)\s} - {$1<span title="$3$4">$4</span> }; + # Also make it boldface, to make commands stand out + $line =~ s{^(#\s+(#|\$)\s+)(\S+/)(podman\S*)(\s.*)} + {$1<b><span title="$3$4">$4</span>$5</b>}; if ($line =~ /^ok\s.*\s# skip/) { $css = 'skipped' } elsif ($line =~ /^ok\s/) { $css = 'passed' } @@ -470,6 +490,83 @@ sub make_id { } +############################################################################### +# BEGIN job_synopsis and related helpers + +################## +# job_synopsis # Job details, links to github/cirrus +################## +sub job_synopsis { + my $subtest_name = shift; # e.g. integration_test + + my $s = <<"END_SYNOPSIS"; +<table class="synopsis"> +END_SYNOPSIS + + # PR 1234 - title of the pr + my $pr_title = escapeHTML(_env_replace("{CIRRUS_CHANGE_TITLE}")); + $s .= _tr("Github PR", sprintf("%s - %s", + _a("{CIRRUS_PR}", "https://{CIRRUS_REPO_CLONE_HOST}/{CIRRUS_REPO_FULL_NAME}/pull/{CIRRUS_PR}"), + $pr_title)); + + # PR author, if signed-off-by + if (my $msg = _env_replace("{CIRRUS_COMMIT_MESSAGE}")) { + while ($msg =~ /^Signed-off-by:\s+(\S.*\S)$/gmi) { + $s .= _tr("Author", escapeHTML($1)); + } + } + + # eg "test fedora", "special_testing_rootless" + my $test_name = _env_replace("{CIRRUS_TASK_NAME}"); + if (my $rcli = $ENV{RCLI}) { + $test_name .= " [remote]" if $rcli eq 'true'; + } + else { + $test_name .= " [no RCLI; cannot determine remote/local]"; + } + $s .= _tr("Test name", $test_name); + + # Subtest, e.g. system_test + $s .= _tr("Subtest", $subtest_name); + + # Link to further Cirrus results, e.g. other runs. + # Build is mostly boring, it's usually TASK that we want to see. + $s .= _tr("Cirrus Build ID", "<small>" . _a("{CIRRUS_BUILD_ID}", "https://cirrus-ci.com/build/{CIRRUS_BUILD_ID}") . "</small>"); + $s .= _tr("Cirrus <b>Task</b> ID", _a("{CIRRUS_TASK_ID}", "https://cirrus-ci.com/task/{CIRRUS_TASK_ID}")); + + # "none", "rootless" + $s .= _tr("Special mode", _env_replace("{SPECIALMODE}")); + + $s .= "</table>\n"; + return $s; +} + + +sub _tr { + my ($th, $td) = @_; + return "<tr><th>$th:</th><td>$td</td></tr>\n"; +} + +sub _a { + my ($name, $href) = map { _env_replace($_) } @_; + + if ($href =~ /UNDEFINED/) { + return "$name ($href)"; + } + return "<a href='$href'>$name</a>"; +} + +sub _env_replace { + my $s_in = shift; + + $s_in =~ s[\{(.*?)\}][$ENV{$1} || "[$1 UNDEFINED]"]ge; + + return $s_in; +} + +# END job_synopsis and related helpers +############################################################################### +# BEGIN html-formatting helpers sub escapeHTML { my $s = shift; @@ -492,5 +589,7 @@ sub unescapeHTML { return $s; } +# END html-formatting helpers +############################################################################### 1; diff --git a/contrib/cirrus/logformatter.t b/contrib/cirrus/logformatter.t index 2075bff96..bd4179b5e 100755 --- a/contrib/cirrus/logformatter.t +++ b/contrib/cirrus/logformatter.t @@ -96,7 +96,7 @@ ok 4 blah <span class='bats-passed'><a name='t--00001'>ok 1 hi</a></span> <span class='bats-skipped'><a name='t--00002'>ok 2 bye # skip no reason</a></span> <span class='bats-failed'><a name='t--00003'>not ok 3 fail</a></span> -<span class='bats-log'># $ <span title="/path/to/podman">podman</span> foo -bar</span> +<span class='bats-log'># $ <b><span title="/path/to/podman">podman</span> foo -bar</b></span> <span class='bats-log-esm'># #| FAIL: exit code is 123; expected 321</span> <span class='bats-passed'><a name='t--00004'>ok 4 blah</a></span> <hr/><span class='bats-summary'>Summary: <span class='bats-passed'>2 Passed</span>, <span class='bats-failed'>1 Failed</span>, <span class='bats-skipped'>1 Skipped</span>. Total tests: 4</span> @@ -147,11 +147,11 @@ $SCRIPT_BASE/integration_test.sh |& ${TIMESTAMP} <pre> <span class="timestamp">[+0103s] </span>Podman pod restart <span class="timestamp"> </span><a name='t--podman-pod-restart-single-empty-pod--1'><h2> podman pod restart single empty pod</h2></a> -<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L41'>/containers/libpod/test/e2e/pod_restart_test.go:41</a> +<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L41'>/containers/podman/test/e2e/pod_restart_test.go:41</a> <span class="timestamp"> </span>[BeforeEach] Podman pod restart -<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L18'>/containers/libpod/test/e2e/pod_restart_test.go:18</a> +<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L18'>/containers/podman/test/e2e/pod_restart_test.go:18</a> <span class="timestamp"> </span>[It] podman pod restart single empty pod -<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L41'>/containers/libpod/test/e2e/pod_restart_test.go:41</a> +<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L41'>/containers/podman/test/e2e/pod_restart_test.go:41</a> <span class="timestamp"> </span>Running: <span title="/var/tmp/go/src/github.com/containers/podman/bin/podman"><b>podman</b></span> <span class="boring" title="--storage-opt vfs.imagestore=/tmp/podman/imagecachedir --root /tmp/podman_test553496330/crio --runroot /tmp/podman_test553496330/crio-run @@ -176,7 +176,7 @@ $SCRIPT_BASE/integration_test.sh |& ${TIMESTAMP} <span class="timestamp"> </span><span class='log-warn'>Error: no containers in pod 4810be0cfbd42241e349dbe7d50fbc54405cd320a6637c65fd5323f34d64af89 have no dependencies, cannot start pod: no such container</span> <span class="timestamp"> </span>output: <span class="timestamp"> </span>[AfterEach] Podman pod restart -<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L28'>/containers/libpod/test/e2e/pod_restart_test.go:28</a> +<span class="timestamp"> </span> /var/tmp/go/src/github.com<a class="codelink" href='https://github.com/containers/podman/blob/40f5d8b1becd381c4e8283ed3940d09193e4fe06/test/e2e/pod_restart_test.go#L28'>/containers/podman/test/e2e/pod_restart_test.go:28</a> <span class="timestamp"> </span>Running: <span title="/var/tmp/go/src/github.com/containers/podman/bin/podman"><b>podman</b></span> <span class="boring" title="--storage-opt vfs.imagestore=/tmp/podman/imagecachedir --root /tmp/podman_test553496330/crio --runroot /tmp/podman_test553496330/crio-run @@ -12,7 +12,7 @@ require ( github.com/containernetworking/cni v0.8.0 github.com/containernetworking/plugins v0.8.7 github.com/containers/buildah v1.16.4 - github.com/containers/common v0.23.0 + github.com/containers/common v0.24.0 github.com/containers/conmon v2.0.20+incompatible github.com/containers/image/v5 v5.6.0 github.com/containers/psgo v1.5.1 @@ -73,8 +73,8 @@ github.com/containernetworking/plugins v0.8.7/go.mod h1:R7lXeZaBzpfqapcAbHRW8/CY github.com/containers/buildah v1.16.4 h1:bxthp2FoGcpc2O/RyvbGUAZoefmc5hRBqWQi3BjRu7w= github.com/containers/buildah v1.16.4/go.mod h1:i1XqXgpCROnfcq4oNtfrFEk7UzNDxLJ/PZ+CnPyoIq8= github.com/containers/common v0.21.0/go.mod h1:8w8SVwc+P2p1MOnRMbSKNWXt1Iwd2bKFu2LLZx55DTM= -github.com/containers/common v0.23.0 h1:+g4mI3wUYSzOtoWU9TNVoV4K52/aN6JEz0qs1YdPEe8= -github.com/containers/common v0.23.0/go.mod h1:E56/N0beWGf+lrrJX32atuo2hkjzHwSC8n1vCG+TAR0= +github.com/containers/common v0.24.0 h1:5C03ROzmRvZCyooNJVkZ4Q8T2d04g+VVyPMQ428XC4Y= +github.com/containers/common v0.24.0/go.mod h1:BFRo6uRh1TbkZgR2oYTILxc2BNZTBtBffa9xtu881QI= github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg= github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I= github.com/containers/image/v5 v5.5.2/go.mod h1:4PyNYR0nwlGq/ybVJD9hWlhmIsNra4Q8uOQX2s6E2uM= diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index eba732d2a..514cdaee1 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "math" "net" "os" "os/user" @@ -35,6 +36,7 @@ import ( "github.com/containers/podman/v2/pkg/util" "github.com/containers/podman/v2/utils" "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" securejoin "github.com/cyphar/filepath-securejoin" runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -416,9 +418,43 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // Look up and add groups the user belongs to, if a group wasn't directly specified if !strings.Contains(c.config.User, ":") { + // the gidMappings that are present inside the container user namespace + var gidMappings []idtools.IDMap + + switch { + case len(c.config.IDMappings.GIDMap) > 0: + gidMappings = c.config.IDMappings.GIDMap + case rootless.IsRootless(): + // Check whether the current user namespace has enough gids available. + availableGids, err := rootless.GetAvailableGids() + if err != nil { + return nil, errors.Wrapf(err, "cannot read number of available GIDs") + } + gidMappings = []idtools.IDMap{{ + ContainerID: 0, + HostID: 0, + Size: int(availableGids), + }} + default: + gidMappings = []idtools.IDMap{{ + ContainerID: 0, + HostID: 0, + Size: math.MaxInt32, + }} + } for _, gid := range execUser.Sgids { - // FIXME: We need to add a flag to containers.conf to not add these for HPC Users. - g.AddProcessAdditionalGid(uint32(gid)) + isGidAvailable := false + for _, m := range gidMappings { + if gid >= m.ContainerID && gid < m.ContainerID+m.Size { + isGidAvailable = true + break + } + } + if isGidAvailable { + g.AddProcessAdditionalGid(uint32(gid)) + } else { + logrus.Warnf("additional gid=%d is not present in the user namespace, skip setting it", gid) + } } } diff --git a/libpod/container_log_linux.go b/libpod/container_log_linux.go index 73c2df76e..d895171cf 100644 --- a/libpod/container_log_linux.go +++ b/libpod/container_log_linux.go @@ -33,7 +33,7 @@ const ( func (c *Container) readFromJournal(ctx context.Context, options *logs.LogOptions, logChannel chan *logs.LogLine) error { var config journal.JournalReaderConfig if options.Tail < 0 { - config.NumFromTail = math.MaxUint64 + config.NumFromTail = 0 } else { config.NumFromTail = uint64(options.Tail) } diff --git a/libpod/image/prune.go b/libpod/image/prune.go index fcc65fb03..b38265a7e 100644 --- a/libpod/image/prune.go +++ b/libpod/image/prune.go @@ -125,29 +125,39 @@ func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ( filterFuncs = append(filterFuncs, generatedFunc) } - pruneImages, err := ir.GetPruneImages(ctx, all, filterFuncs) - if err != nil { - return nil, errors.Wrap(err, "unable to get images to prune") - } - prunedCids := make([]string, 0, len(pruneImages)) - for _, p := range pruneImages { - repotags, err := p.RepoTags() + pruned := []string{} + prev := 0 + for { + toPrune, err := ir.GetPruneImages(ctx, all, filterFuncs) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to get images to prune") } - if err := p.Remove(ctx, true); err != nil { - if errors.Cause(err) == storage.ErrImageUsedByContainer { - logrus.Warnf("Failed to prune image %s as it is in use: %v.\nA container associated with containers/storage i.e. Buildah, CRI-O, etc., maybe associated with this image.\nUsing the rmi command with the --force option will remove the container and image, but may cause failures for other dependent systems.", p.ID(), err) - continue - } - return nil, errors.Wrap(err, "failed to prune image") + numImages := len(toPrune) + if numImages == 0 || numImages == prev { + // If there's nothing left to do, return. + break } - defer p.newImageEvent(events.Prune) - nameOrID := p.ID() - if len(repotags) > 0 { - nameOrID = repotags[0] + prev = numImages + for _, img := range toPrune { + repotags, err := img.RepoTags() + if err != nil { + return nil, err + } + if err := img.Remove(ctx, false); err != nil { + if errors.Cause(err) == storage.ErrImageUsedByContainer { + logrus.Warnf("Failed to prune image %s as it is in use: %v.\nA container associated with containers/storage (e.g., Buildah, CRI-O, etc.) maybe associated with this image.\nUsing the rmi command with the --force option will remove the container and image, but may cause failures for other dependent systems.", img.ID(), err) + continue + } + return nil, errors.Wrap(err, "failed to prune image") + } + defer img.newImageEvent(events.Prune) + nameOrID := img.ID() + if len(repotags) > 0 { + nameOrID = repotags[0] + } + pruned = append(pruned, nameOrID) } - prunedCids = append(prunedCids, nameOrID) + } - return prunedCids, nil + return pruned, nil } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index d56dc7d94..965c63bec 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -46,11 +46,7 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.Boo } func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { - return ir.pruneImagesHelper(ctx, opts.All, opts.Filter) -} - -func (ir *ImageEngine) pruneImagesHelper(ctx context.Context, all bool, filters []string) (*entities.ImagePruneReport, error) { - results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, all, filters) + results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, opts.Filter) if err != nil { return nil, err } diff --git a/pkg/rootless/rootless.go b/pkg/rootless/rootless.go index d02721ea9..799c793d8 100644 --- a/pkg/rootless/rootless.go +++ b/pkg/rootless/rootless.go @@ -2,8 +2,10 @@ package rootless import ( "os" + "sync" "github.com/containers/storage" + "github.com/opencontainers/runc/libcontainer/user" "github.com/pkg/errors" ) @@ -46,3 +48,26 @@ func TryJoinPauseProcess(pausePidPath string) (bool, int, error) { } return became, ret, err } + +var ( + availableGids int64 + availableGidsErr error + availableGidsOnce sync.Once +) + +// GetAvailableGids returns how many GIDs are available in the +// current user namespace. +func GetAvailableGids() (int64, error) { + availableGidsOnce.Do(func() { + idMap, err := user.ParseIDMapFile("/proc/self/gid_map") + if err != nil { + availableGidsErr = err + return + } + availableGids = int64(0) + for _, r := range idMap { + availableGids += r.Count + } + }) + return availableGids, availableGidsErr +} diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index b57ddf1aa..f02432f5b 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -10,7 +10,6 @@ import ( "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/rootless" "github.com/containers/podman/v2/pkg/specgen" - "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" @@ -200,7 +199,7 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt } gid5Available := true if isRootless { - nGids, err := GetAvailableGids() + nGids, err := rootless.GetAvailableGids() if err != nil { return nil, err } @@ -360,15 +359,3 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt return configSpec, nil } - -func GetAvailableGids() (int64, error) { - idMap, err := user.ParseIDMapFile("/proc/self/gid_map") - if err != nil { - return 0, err - } - count := int64(0) - for _, r := range idMap { - count += r.Count - } - return count, nil -} diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index bcbfdd80a..49f5f0ce6 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -50,15 +50,17 @@ var _ = Describe("Podman Info", func() { {"{{ json .}}", true, 0}, {"{{json . }}", true, 0}, {" {{ json . }} ", true, 0}, - {"{{json }}", false, 125}, + {"{{json }}", true, 0}, {"{{json .", false, 125}, - {"json . }}", false, 0}, // Note: this does NOT fail but produces garbage + {"json . }}", false, 0}, // without opening {{ template seen as string literal } for _, tt := range tests { session := podmanTest.Podman([]string{"info", "--format", tt.input}) session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(tt.exitCode)) - Expect(session.IsJSONOutputValid()).To(Equal(tt.success)) + + desc := fmt.Sprintf("JSON test(%q)", tt.input) + Expect(session).Should(Exit(tt.exitCode), desc) + Expect(session.IsJSONOutputValid()).To(Equal(tt.success), desc) } }) diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go index 3aa3cf409..67ab71d20 100644 --- a/test/e2e/logs_test.go +++ b/test/e2e/logs_test.go @@ -203,6 +203,7 @@ var _ = Describe("Podman logs", func() { results.WaitWithDefaultTimeout() Expect(results).To(Exit(0)) Expect(len(results.OutputToStringArray())).To(Equal(3)) + Expect(results.OutputToString()).To(Equal("podman podman podman")) }) It("using journald tail two lines", func() { diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 292df529c..05aede122 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -67,6 +67,30 @@ var _ = Describe("Podman run", func() { Expect(session.ExitCode()).To(Equal(0)) }) + It("podman run --rm with --restart", func() { + session := podmanTest.Podman([]string{"run", "--rm", "--restart", "", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "--rm", "--restart", "no", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + // the --rm option conflicts with --restart, when the restartPolicy is not "" and "no" + // so the exitCode should not equal 0 + session = podmanTest.Podman([]string{"run", "--rm", "--restart", "on-failure", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"run", "--rm", "--restart", "always", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"run", "--rm", "--restart", "unless-stopped", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + It("podman run a container based on on a short name with localhost", func() { tag := podmanTest.Podman([]string{"tag", nginx, "localhost/libpod/alpine_nginx:latest"}) tag.WaitWithDefaultTimeout() diff --git a/test/e2e/version_test.go b/test/e2e/version_test.go index 695cccc11..903748b58 100644 --- a/test/e2e/version_test.go +++ b/test/e2e/version_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "os" . "github.com/containers/podman/v2/test/utils" @@ -68,15 +69,17 @@ var _ = Describe("Podman version", func() { {"{{ json .}}", true, 0}, {"{{json . }}", true, 0}, {" {{ json . }} ", true, 0}, - {"{{json }}", false, 125}, + {"{{json }}", true, 0}, {"{{json .", false, 125}, - {"json . }}", false, 0}, // Note: this does NOT fail but produces garbage + {"json . }}", false, 0}, // without opening {{ template seen as string literal } for _, tt := range tests { session := podmanTest.Podman([]string{"version", "--format", tt.input}) session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(tt.exitCode)) - Expect(session.IsJSONOutputValid()).To(Equal(tt.success)) + + desc := fmt.Sprintf("JSON test(%q)", tt.input) + Expect(session).Should(Exit(tt.exitCode), desc) + Expect(session.IsJSONOutputValid()).To(Equal(tt.success), desc) } }) diff --git a/test/system/035-logs.bats b/test/system/035-logs.bats index cbb2091e5..130bc5243 100644 --- a/test/system/035-logs.bats +++ b/test/system/035-logs.bats @@ -50,4 +50,15 @@ ${cid[1]} c ${cid[0]} d" "Sequential output from logs" } +@test "podman logs over journald" { + msg=$(random_string 20) + + run_podman run --name myctr --log-driver journald $IMAGE echo $msg + + run_podman logs myctr + is "$output" "$msg" "check that log output equals the container output" + + run_podman rm myctr +} + # vim: filetype=sh diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 150626ded..a923402ac 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -82,6 +82,8 @@ load helpers # "network create" now works rootless, with the help of a special container @test "podman network create" { + skip_if_remote "FIXME: pending #7808" + local mynetname=testnet-$(random_string 10) local mysubnet=$(random_rfc1918_subnet) diff --git a/vendor/github.com/containers/common/pkg/completion/completion.go b/vendor/github.com/containers/common/pkg/completion/completion.go index 6e7ddff30..07451e992 100644 --- a/vendor/github.com/containers/common/pkg/completion/completion.go +++ b/vendor/github.com/containers/common/pkg/completion/completion.go @@ -1,6 +1,14 @@ package completion -import "github.com/spf13/cobra" +import ( + "bufio" + "os" + "strings" + "unicode" + + "github.com/containers/common/pkg/capabilities" + "github.com/spf13/cobra" +) // FlagCompletions - hold flag completion functions to be applied later with CompleteCommandFlags() type FlagCompletions map[string]func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) @@ -24,3 +32,62 @@ func AutocompleteNone(cmd *cobra.Command, args []string, toComplete string) ([]s func AutocompleteDefault(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveDefault } + +// AutocompleteCapabilities - Autocomplete linux capabilities options. +// Used by --cap-add and --cap-drop. +func AutocompleteCapabilities(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + caps := capabilities.AllCapabilities() + + // convertCase will convert a string to lowercase only if the user input is lowercase + convertCase := func(s string) string { return s } + if len(toComplete) > 0 && unicode.IsLower(rune(toComplete[0])) { + convertCase = strings.ToLower + } + + // offset is used to trim "CAP_" if the user doesn't type CA... or ca... + offset := 0 + if !strings.HasPrefix(toComplete, convertCase("CA")) { + // setting the offset to 4 is safe since each cap starts with CAP_ + offset = 4 + } + + var completions []string + for _, cap := range caps { + completions = append(completions, convertCase(cap)[offset:]) + } + + // add ALL here which is also a valid argument + completions = append(completions, convertCase(capabilities.All)) + return completions, cobra.ShellCompDirectiveNoFileComp +} + +// autocompleteSubIDName - autocomplete the names in /etc/subuid or /etc/subgid +func autocompleteSubIDName(filename string) ([]string, cobra.ShellCompDirective) { + file, err := os.Open(filename) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + defer file.Close() + + var names []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + name := strings.SplitN(scanner.Text(), ":", 2)[0] + names = append(names, name) + } + if err = scanner.Err(); err != nil { + return nil, cobra.ShellCompDirectiveError + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + +// AutocompleteSubgidName - Autocomplete subgidname based on the names in the /etc/subgid file. +func AutocompleteSubgidName(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return autocompleteSubIDName("/etc/subgid") +} + +// AutocompleteSubuidName - Autocomplete subuidname based on the names in the /etc/subuid file. +func AutocompleteSubuidName(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return autocompleteSubIDName("/etc/subuid") +} diff --git a/vendor/github.com/containers/common/version/version.go b/vendor/github.com/containers/common/version/version.go index eaa728791..8e69c7daf 100644 --- a/vendor/github.com/containers/common/version/version.go +++ b/vendor/github.com/containers/common/version/version.go @@ -1,4 +1,4 @@ package version // Version is the version of the build. -const Version = "0.23.0" +const Version = "0.24.0" diff --git a/vendor/modules.txt b/vendor/modules.txt index 0a7e6ad44..e1c2b6300 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -87,7 +87,7 @@ github.com/containers/buildah/pkg/secrets github.com/containers/buildah/pkg/supplemented github.com/containers/buildah/pkg/umask github.com/containers/buildah/util -# github.com/containers/common v0.23.0 +# github.com/containers/common v0.24.0 github.com/containers/common/pkg/apparmor github.com/containers/common/pkg/apparmor/internal/supported github.com/containers/common/pkg/auth |