diff options
37 files changed, 608 insertions, 165 deletions
| 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/containers/ps.go b/cmd/podman/containers/ps.go index a78b35c08..c4c8b60f3 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -240,7 +240,7 @@ func ps(cmd *cobra.Command, args []string) error {  func createPsOut() (string, string) {  	var row string  	if listOpts.Namespace { -		headers := "CONTAINER ID\tNAMES\tPID\tCGROUPNS\tIPC\tMNT\tNET\tPIDN\tUSERNS\tUTS\n" +		headers := "CONTAINER ID\tNAMES\tPID\tCGROUPNS\tIPC\tMNT\tNET\tPIDNS\tUSERNS\tUTS\n"  		row := "{{.ID}}\t{{.Names}}\t{{.Pid}}\t{{.Namespaces.Cgroup}}\t{{.Namespaces.IPC}}\t{{.Namespaces.MNT}}\t{{.Namespaces.NET}}\t{{.Namespaces.PIDNS}}\t{{.Namespaces.User}}\t{{.Namespaces.UTS}}\n"  		return headers, row  	} 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/cmd/podman/root.go b/cmd/podman/root.go index 6424ec12e..1e73f7540 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -154,34 +154,35 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {  		}  	} -	if cmd.Flag("cpu-profile").Changed { -		f, err := os.Create(cfg.CPUProfile) -		if err != nil { -			return errors.Wrapf(err, "unable to create cpu profiling file %s", -				cfg.CPUProfile) -		} -		if err := pprof.StartCPUProfile(f); err != nil { -			return err +	if !registry.IsRemote() { +		if cmd.Flag("cpu-profile").Changed { +			f, err := os.Create(cfg.CPUProfile) +			if err != nil { +				return errors.Wrapf(err, "unable to create cpu profiling file %s", +					cfg.CPUProfile) +			} +			if err := pprof.StartCPUProfile(f); err != nil { +				return err +			}  		} -	} -	if cmd.Flag("trace").Changed { -		tracer, closer := tracing.Init("podman") -		opentracing.SetGlobalTracer(tracer) -		cfg.SpanCloser = closer +		if cmd.Flag("trace").Changed { +			tracer, closer := tracing.Init("podman") +			opentracing.SetGlobalTracer(tracer) +			cfg.SpanCloser = closer -		cfg.Span = tracer.StartSpan("before-context") -		cfg.SpanCtx = opentracing.ContextWithSpan(registry.Context(), cfg.Span) -		opentracing.StartSpanFromContext(cfg.SpanCtx, cmd.Name()) -	} +			cfg.Span = tracer.StartSpan("before-context") +			cfg.SpanCtx = opentracing.ContextWithSpan(registry.Context(), cfg.Span) +			opentracing.StartSpanFromContext(cfg.SpanCtx, cmd.Name()) +		} -	if cfg.MaxWorks <= 0 { -		return errors.Errorf("maximum workers must be set to a positive number (got %d)", cfg.MaxWorks) -	} -	if err := parallel.SetMaxThreads(uint(cfg.MaxWorks)); err != nil { -		return err +		if cfg.MaxWorks <= 0 { +			return errors.Errorf("maximum workers must be set to a positive number (got %d)", cfg.MaxWorks) +		} +		if err := parallel.SetMaxThreads(uint(cfg.MaxWorks)); err != nil { +			return err +		}  	} -  	// Setup Rootless environment, IFF:  	// 1) in ABI mode  	// 2) running as non-root @@ -206,12 +207,14 @@ func persistentPostRunE(cmd *cobra.Command, args []string) error {  	}  	cfg := registry.PodmanConfig() -	if cmd.Flag("cpu-profile").Changed { -		pprof.StopCPUProfile() -	} -	if cmd.Flag("trace").Changed { -		cfg.Span.Finish() -		cfg.SpanCloser.Close() +	if !registry.IsRemote() { +		if cmd.Flag("cpu-profile").Changed { +			pprof.StopCPUProfile() +		} +		if cmd.Flag("trace").Changed { +			cfg.Span.Finish() +			cfg.SpanCloser.Close() +		}  	}  	registry.ImageEngine().Shutdown(registry.Context()) @@ -249,51 +252,57 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) {  	srv, uri, ident := resolveDestination()  	lFlags := cmd.Flags() -	lFlags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)")  	lFlags.StringVarP(&opts.Engine.ActiveService, "connection", "c", srv, "Connection to use for remote Podman service")  	lFlags.StringVar(&opts.URI, "url", uri, "URL to access Podman service (CONTAINER_HOST)")  	lFlags.StringVar(&opts.Identity, "identity", ident, "path to SSH identity file, (CONTAINER_SSHKEY)") +	lFlags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)")  	pFlags := cmd.PersistentFlags() -	pFlags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") -	pFlags.StringVar(&opts.CPUProfile, "cpu-profile", "", "Path for the cpu profiling results") -	pFlags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") -	pFlags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") -	pFlags.StringVar(&cfg.Network.NetworkConfigDir, "cni-config-dir", cfg.Network.NetworkConfigDir, "Path of the configuration directory for CNI networks") -	pFlags.StringVar(&cfg.Containers.DefaultMountsFile, "default-mounts-file", cfg.Containers.DefaultMountsFile, "Path to default mounts file") -	pFlags.StringVar(&cfg.Engine.EventsLogger, "events-backend", cfg.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) -	pFlags.StringSliceVar(&cfg.Engine.HooksDir, "hooks-dir", cfg.Engine.HooksDir, "Set the OCI hooks directory path (may be set multiple times)") -	pFlags.IntVar(&opts.MaxWorks, "max-workers", (runtime.NumCPU()*3)+1, "The maximum number of workers for parallel operations") -	pFlags.StringVar(&cfg.Engine.Namespace, "namespace", cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") -	pFlags.StringVar(&cfg.Engine.StaticDir, "root", "", "Path to the root directory in which data, including images, is stored") -	pFlags.StringVar(&opts.RegistriesConf, "registries-conf", "", "Path to a registries.conf to use for image processing") -	pFlags.StringVar(&opts.Runroot, "runroot", "", "Path to the 'run directory' where all state information is stored") -	pFlags.StringVar(&opts.RuntimePath, "runtime", "", "Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc") -	// -s is deprecated due to conflict with -s on subcommands -	pFlags.StringVar(&opts.StorageDriver, "storage-driver", "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") -	pFlags.StringArrayVar(&opts.StorageOpts, "storage-opt", []string{}, "Used to pass an option to the storage driver") - -	pFlags.StringVar(&opts.Engine.TmpDir, "tmpdir", "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") -	pFlags.BoolVar(&opts.Trace, "trace", false, "Enable opentracing output (default false)") - +	if registry.IsRemote() { +		if err := lFlags.MarkHidden("remote"); err != nil { +			logrus.Warnf("unable to mark --remote flag as hidden: %s", err.Error()) +		} +		opts.Remote = true +	} else { +		pFlags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") +		pFlags.StringVar(&opts.CPUProfile, "cpu-profile", "", "Path for the cpu profiling results") +		pFlags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") +		pFlags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") +		pFlags.StringVar(&cfg.Network.NetworkConfigDir, "cni-config-dir", cfg.Network.NetworkConfigDir, "Path of the configuration directory for CNI networks") +		pFlags.StringVar(&cfg.Containers.DefaultMountsFile, "default-mounts-file", cfg.Containers.DefaultMountsFile, "Path to default mounts file") +		pFlags.StringVar(&cfg.Engine.EventsLogger, "events-backend", cfg.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) +		pFlags.StringSliceVar(&cfg.Engine.HooksDir, "hooks-dir", cfg.Engine.HooksDir, "Set the OCI hooks directory path (may be set multiple times)") +		pFlags.IntVar(&opts.MaxWorks, "max-workers", (runtime.NumCPU()*3)+1, "The maximum number of workers for parallel operations") +		pFlags.StringVar(&cfg.Engine.Namespace, "namespace", cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") +		pFlags.StringVar(&cfg.Engine.StaticDir, "root", "", "Path to the root directory in which data, including images, is stored") +		pFlags.StringVar(&opts.RegistriesConf, "registries-conf", "", "Path to a registries.conf to use for image processing") +		pFlags.StringVar(&opts.Runroot, "runroot", "", "Path to the 'run directory' where all state information is stored") +		pFlags.StringVar(&opts.RuntimePath, "runtime", "", "Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc") +		// -s is deprecated due to conflict with -s on subcommands +		pFlags.StringVar(&opts.StorageDriver, "storage-driver", "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") +		pFlags.StringArrayVar(&opts.StorageOpts, "storage-opt", []string{}, "Used to pass an option to the storage driver") + +		pFlags.StringVar(&opts.Engine.TmpDir, "tmpdir", "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") +		pFlags.BoolVar(&opts.Trace, "trace", false, "Enable opentracing output (default false)") + +		// Hide these flags for both ABI and Tunneling +		for _, f := range []string{ +			"cpu-profile", +			"default-mounts-file", +			"max-workers", +			"registries-conf", +			"trace", +		} { +			if err := pFlags.MarkHidden(f); err != nil { +				logrus.Warnf("unable to mark %s flag as hidden: %s", f, err.Error()) +			} +		} +	}  	// Override default --help information of `--help` global flag  	var dummyHelp bool  	pFlags.BoolVar(&dummyHelp, "help", false, "Help for podman")  	pFlags.StringVar(&logLevel, "log-level", logLevel, fmt.Sprintf("Log messages above specified level (%s)", strings.Join(logLevels, ", "))) -	// Hide these flags for both ABI and Tunneling -	for _, f := range []string{ -		"cpu-profile", -		"default-mounts-file", -		"max-workers", -		"registries-conf", -		"trace", -	} { -		if err := pFlags.MarkHidden(f); err != nil { -			logrus.Warnf("unable to mark %s flag as hidden: %s", f, err.Error()) -		} -	} -  	// Only create these flags for ABI connections  	if !registry.IsRemote() {  		pFlags.StringArrayVar(&opts.RuntimeFlags, "runtime-flag", []string{}, "add global flags for the container runtime") 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/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 555486562..87337fa3c 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -160,6 +160,14 @@ Print the version  Podman can set up environment variables from env of [engine] table in containers.conf. These variables can be overridden by passing  environment variables before the `podman` commands. +## Remote Access + +The Podman command can be used with remote services using the `--remote` flag. Connections can +be made using local unix domain sockets, ssh or directly to tcp sockets. When specifying the +podman --remote flag, only the global options `--url`, `--identity`, `--log-level`, `--connection` are used. + +Connection information can also be managed using the containers.conf file. +  ## Exit Status  The exit code from `podman` gives information about why the container 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/kube.go b/libpod/kube.go index f83e99d82..6df79e394 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -303,12 +303,24 @@ func containerToV1Container(c *Container) (v1.Container, []v1.Volume, error) {  	// This should not be applicable  	//container.EnvFromSource =  	kubeContainer.Env = envVariables -	// TODO enable resources when we can support naming conventions -	//container.Resources  	kubeContainer.SecurityContext = kubeSec  	kubeContainer.StdinOnce = false  	kubeContainer.TTY = c.config.Spec.Process.Terminal +	// TODO add CPU limit support. +	if c.config.Spec.Linux != nil && +		c.config.Spec.Linux.Resources != nil && +		c.config.Spec.Linux.Resources.Memory != nil && +		c.config.Spec.Linux.Resources.Memory.Limit != nil { +		if kubeContainer.Resources.Limits == nil { +			kubeContainer.Resources.Limits = v1.ResourceList{} +		} + +		qty := kubeContainer.Resources.Limits.Memory() +		qty.Set(*c.config.Spec.Linux.Resources.Memory.Limit) +		kubeContainer.Resources.Limits[v1.ResourceMemory] = *qty +	} +  	return kubeContainer, kubeVolumes, nil  } diff --git a/libpod/pod.go b/libpod/pod.go index a5a0532be..c8f62ca18 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -327,3 +327,21 @@ func (p *Pod) GetPodStats(previousContainerStats map[string]*define.ContainerSta  	}  	return newContainerStats, nil  } + +// ProcessLabel returns the SELinux label associated with the pod +func (p *Pod) ProcessLabel() (string, error) { +	if !p.HasInfraContainer() { +		return "", nil +	} + +	id, err := p.InfraContainerID() +	if err != nil { +		return "", err +	} + +	ctr, err := p.runtime.state.Container(id) +	if err != nil { +		return "", err +	} +	return ctr.ProcessLabel(), nil +} diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 940b57343..cc67ebcd1 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -20,9 +20,25 @@ import (  	"github.com/containers/podman/v2/pkg/domain/entities"  	"github.com/docker/docker/api/types"  	"github.com/gorilla/schema" +	"github.com/opencontainers/go-digest"  	"github.com/pkg/errors"  ) +// mergeNameAndTagOrDigest creates an image reference as string from the +// provided image name and tagOrDigest which can be a tag, a digest or empty. +func mergeNameAndTagOrDigest(name, tagOrDigest string) string { +	if len(tagOrDigest) == 0 { +		return name +	} + +	separator := ":" // default to tag +	if _, err := digest.Parse(tagOrDigest); err == nil { +		// We have a digest, so let's change the separator. +		separator = "@" +	} +	return fmt.Sprintf("%s%s%s", name, separator, tagOrDigest) +} +  func ExportImage(w http.ResponseWriter, r *http.Request) {  	// 200 ok  	// 500 server @@ -252,10 +268,7 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {  		return  	} -	fromImage := query.FromImage -	if len(query.Tag) >= 1 { -		fromImage = fmt.Sprintf("%s:%s", fromImage, query.Tag) -	} +	fromImage := mergeNameAndTagOrDigest(query.FromImage, query.Tag)  	authConf, authfile, key, err := auth.GetCredentials(r)  	if err != nil { diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 46e4df1d2..708ad06cb 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -25,7 +25,7 @@ var (  // the most recent number of containers.  The pod and size booleans indicate that pod information and rootfs  // size information should also be included.  Finally, the sync bool synchronizes the OCI runtime and  // container state. -func List(ctx context.Context, filters map[string][]string, all *bool, last *int, size, sync *bool) ([]entities.ListContainer, error) { // nolint:typecheck +func List(ctx context.Context, filters map[string][]string, all *bool, last *int, namespace, size, sync *bool) ([]entities.ListContainer, error) { // nolint:typecheck  	conn, err := bindings.GetClient(ctx)  	if err != nil {  		return nil, err @@ -44,6 +44,9 @@ func List(ctx context.Context, filters map[string][]string, all *bool, last *int  	if sync != nil {  		params.Set("sync", strconv.FormatBool(*sync))  	} +	if namespace != nil { +		params.Set("namespace", strconv.FormatBool(*namespace)) +	}  	if filters != nil {  		filterString, err := bindings.FiltersToString(filters)  		if err != nil { diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index bf2ceab2a..408b4769d 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -499,7 +499,7 @@ var _ = Describe("Podman containers ", func() {  		Expect(err).To(BeNil())  		_, err = bt.RunTopContainer(&name2, bindings.PFalse, nil)  		Expect(err).To(BeNil()) -		containerLatestList, err := containers.List(bt.conn, nil, nil, &latestContainers, nil, nil) +		containerLatestList, err := containers.List(bt.conn, nil, nil, &latestContainers, nil, nil, nil)  		Expect(err).To(BeNil())  		err = containers.Kill(bt.conn, containerLatestList[0].Names[0], "SIGTERM")  		Expect(err).To(BeNil()) @@ -744,7 +744,7 @@ var _ = Describe("Podman containers ", func() {  		// Validate list container with id filter  		filters := make(map[string][]string)  		filters["id"] = []string{cid} -		c, err := containers.List(bt.conn, filters, bindings.PTrue, nil, nil, nil) +		c, err := containers.List(bt.conn, filters, bindings.PTrue, nil, nil, nil, nil)  		Expect(err).To(BeNil())  		Expect(len(c)).To(Equal(1))  	}) @@ -758,7 +758,7 @@ var _ = Describe("Podman containers ", func() {  		lastNum := 1 -		c, err := containers.List(bt.conn, nil, bindings.PTrue, &lastNum, nil, nil) +		c, err := containers.List(bt.conn, nil, bindings.PTrue, &lastNum, nil, nil, nil)  		Expect(err).To(BeNil())  		Expect(len(c)).To(Equal(1))  		Expect(c[0].PodName).To(Equal(podName)) diff --git a/pkg/channel/writer.go b/pkg/channel/writer.go index dbb38e416..28c9d7de5 100644 --- a/pkg/channel/writer.go +++ b/pkg/channel/writer.go @@ -1,6 +1,7 @@  package channel  import ( +	"fmt"  	"io"  	"sync" @@ -32,16 +33,24 @@ func (w *writeCloser) Chan() <-chan []byte {  }  // Write method for WriteCloser -func (w *writeCloser) Write(b []byte) (int, error) { +func (w *writeCloser) Write(b []byte) (bLen int, err error) { +	// https://github.com/containers/podman/issues/7896 +	// when podman-remote pull image, if it was killed, the server will panic: send on closed channel +	// so handle it +	defer func() { +		if rErr := recover(); rErr != nil { +			err = fmt.Errorf("%s", rErr) +		} +	}()  	if w == nil || w.ch == nil {  		return 0, errors.New("use channel.NewWriter() to initialize a WriteCloser")  	}  	w.mux.Lock() +	defer w.mux.Unlock()  	buf := make([]byte, len(b))  	copy(buf, b)  	w.ch <- buf -	w.mux.Unlock()  	return len(b), nil  } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 9b03503c6..194bb4b48 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -548,7 +548,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri  }  func (ic *ContainerEngine) ContainerList(ctx context.Context, options entities.ContainerListOptions) ([]entities.ListContainer, error) { -	return containers.List(ic.ClientCxt, options.Filters, &options.All, &options.Last, &options.Size, &options.Sync) +	return containers.List(ic.ClientCxt, options.Filters, &options.All, &options.Last, &options.Namespace, &options.Size, &options.Sync)  }  func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.ContainerRunOptions) (*entities.ContainerRunReport, error) { diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go index 7d28f1bd3..63f9546be 100644 --- a/pkg/domain/infra/tunnel/helpers.go +++ b/pkg/domain/infra/tunnel/helpers.go @@ -19,7 +19,7 @@ func getContainersByContext(contextWithConnection context.Context, all, ignore b  		return nil, errors.New("cannot lookup containers and all")  	} -	allContainers, err := containers.List(contextWithConnection, nil, bindings.PTrue, nil, nil, bindings.PTrue) +	allContainers, err := containers.List(contextWithConnection, nil, bindings.PTrue, nil, nil, nil, bindings.PTrue)  	if err != nil {  		return nil, err  	} diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index 8087507e2..96b2d754f 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -134,15 +134,16 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities  			logrus.Errorf("error getting exited time for %q: %v", c.ID(), err)  		} +		pid, err = c.PID() +		if err != nil { +			return errors.Wrapf(err, "unable to obtain container pid") +		} +  		if !opts.Size && !opts.Namespace {  			return nil  		}  		if opts.Namespace { -			pid, err = c.PID() -			if err != nil { -				return errors.Wrapf(err, "unable to obtain container pid") -			}  			ctrPID := strconv.Itoa(pid)  			cgroup, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup"))  			ipc, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc")) 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/container_create.go b/pkg/specgen/generate/container_create.go index 2ac3b376f..147450703 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -11,6 +11,7 @@ import (  	"github.com/containers/podman/v2/pkg/specgen"  	"github.com/containers/podman/v2/pkg/util"  	"github.com/containers/storage" +	"github.com/opencontainers/selinux/go-selinux/label"  	"github.com/pkg/errors"  	"github.com/sirupsen/logrus"  ) @@ -272,6 +273,21 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.  	// Security options  	if len(s.SelinuxOpts) > 0 {  		options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) +	} else { +		if pod != nil { +			// duplicate the security options from the pod +			processLabel, err := pod.ProcessLabel() +			if err != nil { +				return nil, err +			} +			if processLabel != "" { +				selinuxOpts, err := label.DupSecOpt(processLabel) +				if err != nil { +					return nil, err +				} +				options = append(options, libpod.WithSecLabels(selinuxOpts)) +			} +		}  	}  	options = append(options, libpod.WithPrivileged(s.Privileged)) 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/apiv2/10-images.at b/test/apiv2/10-images.at index a204df65c..bdc298ae3 100644 --- a/test/apiv2/10-images.at +++ b/test/apiv2/10-images.at @@ -39,7 +39,11 @@ t GET images/$iid/json 200 \    .Id=sha256:$iid \    .RepoTags[0]=$IMAGE -#t POST images/create fromImage=alpine 201 foo +t POST "images/create?fromImage=alpine" '' 200 + +t POST "images/create?fromImage=alpine&tag=latest" '' 200 + +t POST "images/create?fromImage=docker.io/library/alpine&tag=sha256:acd3ca9941a85e8ed16515bfc5328e4e2f8c128caa72959a58a127b7801ee01f" '' 200  # Display the image history  t GET libpod/images/nonesuch/history 404 diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index a3a841dc6..05a7f4ddf 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -235,6 +235,31 @@ var _ = Describe("Podman generate kube", func() {  		}  	}) +	It("podman generate kube on pod with memory limit", func() { +		podName := "testMemoryLimit" +		podSession := podmanTest.Podman([]string{"pod", "create", "--name", podName}) +		podSession.WaitWithDefaultTimeout() +		Expect(podSession.ExitCode()).To(Equal(0)) + +		ctr1Name := "ctr1" +		ctr1Session := podmanTest.Podman([]string{"create", "--name", ctr1Name, "--pod", podName, "--memory", "10Mi", ALPINE, "top"}) +		ctr1Session.WaitWithDefaultTimeout() +		Expect(ctr1Session.ExitCode()).To(Equal(0)) + +		kube := podmanTest.Podman([]string{"generate", "kube", podName}) +		kube.WaitWithDefaultTimeout() +		Expect(kube.ExitCode()).To(Equal(0)) + +		pod := new(v1.Pod) +		err := yaml.Unmarshal(kube.Out.Contents(), pod) +		Expect(err).To(BeNil()) + +		for _, ctr := range pod.Spec.Containers { +			memoryLimit, _ := ctr.Resources.Limits.Memory().AsInt64() +			Expect(memoryLimit).To(Equal(int64(10 * 1024 * 1024))) +		} +	}) +  	It("podman generate kube on pod with ports", func() {  		podName := "test"  		podSession := podmanTest.Podman([]string{"pod", "create", "--name", podName, "-p", "4000:4000", "-p", "5000:5000"}) 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/pod_infra_container_test.go b/test/e2e/pod_infra_container_test.go index 515391f92..063c71b9f 100644 --- a/test/e2e/pod_infra_container_test.go +++ b/test/e2e/pod_infra_container_test.go @@ -225,7 +225,6 @@ var _ = Describe("Podman pod create", func() {  	})  	It("podman pod container can override pod pid NS", func() { -		SkipIfRemote("FIXME This should work on podman-remote")  		session := podmanTest.Podman([]string{"pod", "create", "--share", "pid"})  		session.WaitWithDefaultTimeout()  		Expect(session.ExitCode()).To(Equal(0)) @@ -257,7 +256,6 @@ var _ = Describe("Podman pod create", func() {  	})  	It("podman pod container can override pod not sharing pid", func() { -		SkipIfRemote("FIXME This should work on podman-remote")  		session := podmanTest.Podman([]string{"pod", "create", "--share", "net"})  		session.WaitWithDefaultTimeout()  		Expect(session.ExitCode()).To(Equal(0)) @@ -283,7 +281,6 @@ var _ = Describe("Podman pod create", func() {  	})  	It("podman pod container can override pod ipc NS", func() { -		SkipIfRemote("FIXME This should work on podman-remote")  		session := podmanTest.Podman([]string{"pod", "create", "--share", "ipc"})  		session.WaitWithDefaultTimeout()  		Expect(session.ExitCode()).To(Equal(0)) diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index 82a842146..0f2ce2d46 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -173,6 +173,18 @@ var _ = Describe("Podman ps", func() {  		Expect(len(result.OutputToStringArray())).Should(BeNumerically(">", 0))  	}) +	It("podman ps namespace flag even for remote", func() { +		session := podmanTest.RunTopContainer("test1") +		session.WaitWithDefaultTimeout() + +		result := podmanTest.Podman([]string{"ps", "-a", "--namespace", "--format", +			"{{with .Namespaces}}{{.Cgroup}}:{{.IPC}}:{{.MNT}}:{{.NET}}:{{.PIDNS}}:{{.User}}:{{.UTS}}{{end}}"}) +		result.WaitWithDefaultTimeout() +		Expect(result.ExitCode()).To(Equal(0)) +		// it must contains `::` when some ns is null. If it works normally, it should be "$num1:$num2:$num3" +		Expect(result.OutputToString()).To(Not(ContainSubstring(`::`))) +	}) +  	It("podman ps with no containers is valid json format", func() {  		result := podmanTest.Podman([]string{"ps", "--format", "json"})  		result.WaitWithDefaultTimeout() diff --git a/test/e2e/run_selinux_test.go b/test/e2e/run_selinux_test.go index 219750bcb..3294f6d3b 100644 --- a/test/e2e/run_selinux_test.go +++ b/test/e2e/run_selinux_test.go @@ -182,4 +182,115 @@ var _ = Describe("Podman run", func() {  		match2, _ := session.GrepString("s0:c1,c2")  		Expect(match2).To(BeTrue())  	}) + +	It("podman pod container share SELinux labels", func() { +		session := podmanTest.Podman([]string{"pod", "create"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		podID := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--pod", podID, ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		label1 := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--pod", podID, ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(Equal(label1)) + +		session = podmanTest.Podman([]string{"pod", "rm", podID, "--force"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +	}) + +	It("podman pod container --infra=false doesn't share SELinux labels", func() { +		session := podmanTest.Podman([]string{"pod", "create", "--infra=false"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		podID := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--pod", podID, ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		label1 := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--pod", podID, ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(Not(Equal(label1))) + +		session = podmanTest.Podman([]string{"pod", "rm", podID, "--force"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +	}) + +	It("podman shared IPC NS container share SELinux labels", func() { +		session := podmanTest.RunTopContainer("test1") +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) + +		session = podmanTest.Podman([]string{"exec", "test1", "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		label1 := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--ipc", "container:test1", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(Equal(label1)) +	}) + +	It("podman shared PID NS container share SELinux labels", func() { +		session := podmanTest.RunTopContainer("test1") +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) + +		session = podmanTest.Podman([]string{"exec", "test1", "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		label1 := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--pid", "container:test1", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(Equal(label1)) +	}) + +	It("podman shared NET NS container doesn't share SELinux labels", func() { +		session := podmanTest.RunTopContainer("test1") +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) + +		session = podmanTest.Podman([]string{"exec", "test1", "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		label1 := session.OutputToString() + +		session = podmanTest.Podman([]string{"run", "--net", "container:test1", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(Not(Equal(label1))) +	}) + +	It("podman test --pid=host", func() { +		session := podmanTest.Podman([]string{"run", "--pid=host", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(ContainSubstring("spc_t")) +	}) + +	It("podman test --ipc=host", func() { +		session := podmanTest.Podman([]string{"run", "--ipc=host", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(ContainSubstring("spc_t")) +	}) + +	It("podman test --ipc=net", func() { +		session := podmanTest.Podman([]string{"run", "--net=host", ALPINE, "cat", "/proc/self/attr/current"}) +		session.WaitWithDefaultTimeout() +		Expect(session.ExitCode()).To(Equal(0)) +		Expect(session.OutputToString()).To(ContainSubstring("container_t")) +	})  }) 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/001-basic.bats b/test/system/001-basic.bats index 1d5eb066b..d276cfda1 100644 --- a/test/system/001-basic.bats +++ b/test/system/001-basic.bats @@ -61,7 +61,7 @@ function setup() {      is "$output" ".*Server:" "podman --remote: contacts server"      # This was failing: "podman --foo --bar --remote". -    PODMAN="${podman_non_remote} --tmpdir /var/tmp --log-level=error ${podman_args[@]} --remote" run_podman version +    PODMAN="${podman_non_remote} --log-level=error ${podman_args[@]} --remote" run_podman version      is "$output" ".*Server:" "podman [flags] --remote: contacts server"      # ...but no matter what, --remote is never allowed after subcommand 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 | 
