diff options
270 files changed, 8765 insertions, 1758 deletions
@@ -62,10 +62,11 @@ PKG_MANAGER ?= $(shell command -v dnf yum|head -n1) PRE_COMMIT = $(shell command -v bin/venv/bin/pre-commit ~/.local/bin/pre-commit pre-commit | head -n1) # This isn't what we actually build; it's a superset, used for target -# dependencies. Basically: all *.go files, except *_test.go, and except -# anything in a dot subdirectory. If any of these files is newer than -# our target (bin/podman{,-remote}), a rebuild is triggered. -SOURCES = $(shell find . -path './.*' -prune -o \( -name '*.go' -a ! -name '*_test.go' \) -print) +# dependencies. Basically: all *.go and *.c files, except *_test.go, +# and except anything in a dot subdirectory. If any of these files is +# newer than our target (bin/podman{,-remote}), a rebuild is +# triggered. +SOURCES = $(shell find . -path './.*' -prune -o \( \( -name '*.go' -o -name '*.c' \) -a ! -name '*_test.go' \) -print) BUILDFLAGS := -mod=vendor $(BUILDFLAGS) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index ea453a331..4cb29383a 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1163,6 +1163,13 @@ func AutocompleteEventBackend(cmd *cobra.Command, args []string, toComplete stri return types, cobra.ShellCompDirectiveNoFileComp } +// AutocompleteNetworkBackend - Autocomplete network backend options. +// -> "cni", "netavark" +func AutocompleteNetworkBackend(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + types := []string{"cni", "netavark"} + return types, cobra.ShellCompDirectiveNoFileComp +} + // AutocompleteLogLevel - Autocomplete log level options. // -> "trace", "debug", "info", "warn", "error", "fatal", "panic" func AutocompleteLogLevel(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go index f309390c3..0e582ae52 100644 --- a/cmd/podman/containers/port.go +++ b/cmd/podman/containers/port.go @@ -8,7 +8,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" - "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -73,7 +72,8 @@ func port(_ *cobra.Command, args []string) error { var ( container string err error - userPort types.OCICNIPortMapping + userPort uint16 + userProto string ) if len(args) == 0 && !portOpts.Latest && !portOpts.All { @@ -101,16 +101,12 @@ func port(_ *cobra.Command, args []string) error { fields = append(fields, "tcp") } - portNum, err := strconv.Atoi(fields[0]) + portNum, err := strconv.ParseUint(fields[0], 10, 16) if err != nil { return err } - userPort = types.OCICNIPortMapping{ - HostPort: 0, - ContainerPort: int32(portNum), - Protocol: fields[1], - HostIP: "", - } + userPort = uint16(portNum) + userProto = fields[1] } reports, err := registry.ContainerEngine().ContainerPort(registry.GetContext(), container, portOpts) @@ -120,24 +116,36 @@ func port(_ *cobra.Command, args []string) error { var found bool // Iterate mappings for _, report := range reports { + allPrefix := "" + if portOpts.All { + allPrefix = report.Id[:12] + "\t" + } for _, v := range report.Ports { hostIP := v.HostIP // Set host IP to 0.0.0.0 if blank if hostIP == "" { hostIP = "0.0.0.0" } - if portOpts.All { - fmt.Printf("%s\t", report.Id[:12]) - } - // If not searching by port or port/proto, then dump what we see - if port == "" { - fmt.Printf("%d/%s -> %s:%d\n", v.ContainerPort, v.Protocol, hostIP, v.HostPort) - continue - } - if v.ContainerPort == userPort.ContainerPort { - fmt.Printf("%s:%d\n", hostIP, v.HostPort) - found = true - break + protocols := strings.Split(v.Protocol, ",") + for _, protocol := range protocols { + // If not searching by port or port/proto, then dump what we see + if port == "" { + for i := uint16(0); i < v.Range; i++ { + fmt.Printf("%s%d/%s -> %s:%d\n", allPrefix, v.ContainerPort+i, protocol, hostIP, v.HostPort+i) + } + continue + } + // check if the proto matches and if the port is in the range + // this is faster than looping over the range for no reason + if v.Protocol == userProto && + v.ContainerPort <= userPort && + v.ContainerPort+v.Range > userPort { + // we have to add the current range to the host port + hostPort := v.HostPort + userPort - v.ContainerPort + fmt.Printf("%s%s:%d\n", allPrefix, hostIP, hostPort) + found = true + break + } } } if !found && port != "" { diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 9687cd5bd..712de327c 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -3,8 +3,6 @@ package containers import ( "fmt" "os" - "sort" - "strconv" "strings" "time" @@ -477,174 +475,31 @@ func (l psReporter) UTS() string { // portsToString converts the ports used to a string of the from "port1, port2" // and also groups a continuous list of ports into a readable format. -func portsToString(ports []types.OCICNIPortMapping) string { +// The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto +func portsToString(ports []types.PortMapping) string { if len(ports) == 0 { return "" } - // Sort the ports, so grouping continuous ports become easy. - sort.Slice(ports, func(i, j int) bool { - return comparePorts(ports[i], ports[j]) - }) - - portGroups := [][]types.OCICNIPortMapping{} - currentGroup := []types.OCICNIPortMapping{} - for i, v := range ports { - var prevPort, nextPort *int32 - if i > 0 { - prevPort = &ports[i-1].ContainerPort - } - if i+1 < len(ports) { - nextPort = &ports[i+1].ContainerPort - } - - port := v.ContainerPort - - // Helper functions - addToCurrentGroup := func(x types.OCICNIPortMapping) { - currentGroup = append(currentGroup, x) - } - - addToPortGroup := func(x types.OCICNIPortMapping) { - portGroups = append(portGroups, []types.OCICNIPortMapping{x}) - } - - finishCurrentGroup := func() { - portGroups = append(portGroups, currentGroup) - currentGroup = []types.OCICNIPortMapping{} - } - - // Single entry slice - if prevPort == nil && nextPort == nil { - addToPortGroup(v) - } - - // Start of the slice with len > 0 - if prevPort == nil && nextPort != nil { - isGroup := *nextPort-1 == port - - if isGroup { - // Start with a group - addToCurrentGroup(v) - } else { - // Start with single item - addToPortGroup(v) - } - - continue - } - - // Middle of the slice with len > 0 - if prevPort != nil && nextPort != nil { - currentIsGroup := *prevPort+1 == port - nextIsGroup := *nextPort-1 == port - - if currentIsGroup { - // Maybe in the middle of a group - addToCurrentGroup(v) - - if !nextIsGroup { - // End of a group - finishCurrentGroup() - } - } else if nextIsGroup { - // Start of a new group - addToCurrentGroup(v) - } else { - // No group at all - addToPortGroup(v) - } - - continue - } - - // End of the slice with len > 0 - if prevPort != nil && nextPort == nil { - isGroup := *prevPort+1 == port - - if isGroup { - // End group - addToCurrentGroup(v) - finishCurrentGroup() - } else { - // End single item - addToPortGroup(v) - } - } - } - - portDisplay := []string{} - for _, group := range portGroups { - if len(group) == 0 { - // Usually should not happen, but better do not crash. - continue - } - - first := group[0] - - hostIP := first.HostIP + sb := &strings.Builder{} + for _, port := range ports { + hostIP := port.HostIP if hostIP == "" { hostIP = "0.0.0.0" } - - // Single mappings - if len(group) == 1 { - portDisplay = append(portDisplay, - fmt.Sprintf( - "%s:%d->%d/%s", - hostIP, first.HostPort, first.ContainerPort, first.Protocol, - ), - ) - continue - } - - // Group mappings - last := group[len(group)-1] - portDisplay = append(portDisplay, formatGroup( - fmt.Sprintf("%s/%s", hostIP, first.Protocol), - first.HostPort, last.HostPort, - first.ContainerPort, last.ContainerPort, - )) - } - return strings.Join(portDisplay, ", ") -} - -func comparePorts(i, j types.OCICNIPortMapping) bool { - if i.ContainerPort != j.ContainerPort { - return i.ContainerPort < j.ContainerPort - } - - if i.HostIP != j.HostIP { - return i.HostIP < j.HostIP - } - - if i.HostPort != j.HostPort { - return i.HostPort < j.HostPort - } - - return i.Protocol < j.Protocol -} - -// formatGroup returns the group in the format: -// <IP:firstHost:lastHost->firstCtr:lastCtr/Proto> -// e.g 0.0.0.0:1000-1006->2000-2006/tcp. -func formatGroup(key string, firstHost, lastHost, firstCtr, lastCtr int32) string { - parts := strings.Split(key, "/") - groupType := parts[0] - var ip string - if len(parts) > 1 { - ip = parts[0] - groupType = parts[1] - } - - group := func(first, last int32) string { - group := strconv.Itoa(int(first)) - if first != last { - group = fmt.Sprintf("%s-%d", group, last) + protocols := strings.Split(port.Protocol, ",") + for _, protocol := range protocols { + if port.Range > 1 { + fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ", + hostIP, port.HostPort, port.HostPort+port.Range-1, + port.ContainerPort, port.ContainerPort+port.Range-1, protocol) + } else { + fmt.Fprintf(sb, "%s:%d->%d/%s, ", + hostIP, port.HostPort, + port.ContainerPort, protocol) + } } - return group } - hostGroup := group(firstHost, lastHost) - ctrGroup := group(firstCtr, lastCtr) - - return fmt.Sprintf("%s:%s->%s/%s", ip, hostGroup, ctrGroup, groupType) + display := sb.String() + // make sure to trim the last ", " of the string + return display[:len(display)-2] } diff --git a/cmd/podman/images/load.go b/cmd/podman/images/load.go index c7b7d6b3f..c39ae624e 100644 --- a/cmd/podman/images/load.go +++ b/cmd/podman/images/load.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/containers/common/pkg/completion" + "github.com/containers/common/pkg/download" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/pkg/domain/entities" @@ -69,6 +70,20 @@ func loadFlags(cmd *cobra.Command) { func load(cmd *cobra.Command, args []string) error { if len(loadOpts.Input) > 0 { + // Download the input file if needed. + if strings.HasPrefix(loadOpts.Input, "https://") || strings.HasPrefix(loadOpts.Input, "http://") { + tmpdir, err := util.DefaultContainerConfig().ImageCopyTmpDir() + if err != nil { + return err + } + tmpfile, err := download.FromURL(tmpdir, loadOpts.Input) + if err != nil { + return err + } + defer os.Remove(tmpfile) + loadOpts.Input = tmpfile + } + if _, err := os.Stat(loadOpts.Input); err != nil { return err } diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go index c89a090bf..67a531e6b 100644 --- a/cmd/podman/images/scp.go +++ b/cmd/podman/images/scp.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v3/cmd/podman/system/connection" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/rootless" "github.com/docker/distribution/reference" scpD "github.com/dtylman/scp" "github.com/pkg/errors" @@ -125,6 +126,11 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { fmt.Println(rep) // TODO: Add podman remote support default: // else native load + scpOpts.Save.Format = "oci-archive" + _, err := os.Open(scpOpts.Save.Output) + if err != nil { + return err + } if scpOpts.Tag != "" { return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") } @@ -133,12 +139,20 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { if abiErr != nil { errors.Wrapf(abiErr, "could not save image as specified") } - rep, err := abiEng.Load(context.Background(), scpOpts.Load) - if err != nil { - return err + if !rootless.IsRootless() && scpOpts.Rootless { + err := abiEng.Transfer(context.Background(), scpOpts) + if err != nil { + return err + } + } else { + rep, err := abiEng.Load(context.Background(), scpOpts.Load) + if err != nil { + return err + } + fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) } - fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) } + return nil } @@ -154,7 +168,7 @@ func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (str n, err := scpD.CopyTo(dial, localFile, remoteFile) if err != nil { - errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" return " ", errors.Wrapf(err, errOut) } run := "" @@ -167,7 +181,7 @@ func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (str if err != nil { return "", err } - return strings.TrimSuffix(out, "\n"), nil + return strings.TrimSuffix(string(out), "\n"), nil } // saveToRemote takes image information and remote connection information. it connects to the specified client @@ -193,7 +207,7 @@ func saveToRemote(image, localFile string, tag string, uri *urlP.URL, iden strin n, err := scpD.CopyFrom(dial, remoteFile, localFile) connection.ExecRemoteCommand(dial, "rm "+remoteFile) if err != nil { - errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" return errors.Wrapf(err, errOut) } return nil @@ -207,11 +221,7 @@ func makeRemoteFile(dial *ssh.Client) (string, error) { if err != nil { return "", err } - remoteFile = strings.TrimSuffix(remoteFile, "\n") - if err != nil { - return "", err - } - return remoteFile, nil + return strings.TrimSuffix(string(remoteFile), "\n"), nil } // createConnections takes a boolean determining which ssh client to dial @@ -271,7 +281,14 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination scpOpts.SourceImageName = args[0] } case 2: - if strings.Contains(args[0], "::") { + if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment + scpOpts.Rootless = true + scpOpts.User = strings.Split(args[1], "@")[0] + scpOpts.SourceImageName = strings.Split(args[0], "::")[1] + if strings.Split(args[0], "@")[0] != "root" { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") + } + } else if strings.Contains(args[0], "::") { if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client cliConnections = append(cliConnections, args[0]) scpOpts.ToRemote = true diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 0a0d43b53..0d759a586 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/containers/common/pkg/completion" - "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/sysinfo" "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/containers" @@ -48,6 +47,7 @@ var ( var ( createOptions entities.PodCreateOptions infraOptions entities.ContainerCreateOptions + infraImage string labels, labelFile []string podIDFile string replace bool @@ -72,7 +72,7 @@ func init() { _ = createCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone) infraImageFlagName := "infra-image" - flags.String(infraImageFlagName, containerConfig.Engine.InfraImage, "The image of the infra container to associate with the pod") + flags.StringVar(&infraImage, infraImageFlagName, containerConfig.Engine.InfraImage, "The image of the infra container to associate with the pod") _ = createCommand.RegisterFlagCompletionFunc(infraImageFlagName, common.AutocompleteImages) podIDFileFlagName := "pod-id-file" @@ -110,14 +110,14 @@ func create(cmd *cobra.Command, args []string) error { return errors.Wrapf(err, "unable to process labels") } - imageName = config.DefaultInfraImage + imageName = infraImage img := imageName if !createOptions.Infra { if cmd.Flag("no-hosts").Changed { return fmt.Errorf("cannot specify no-hosts without an infra container") } flags := cmd.Flags() - createOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, false) + createOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, createOptions.Infra) if err != nil { return err } @@ -134,7 +134,7 @@ func create(cmd *cobra.Command, args []string) error { } else { // reassign certain options for lbpod api, these need to be populated in spec flags := cmd.Flags() - infraOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, false) + infraOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, createOptions.Infra) if err != nil { return err } @@ -151,14 +151,6 @@ func create(cmd *cobra.Command, args []string) error { return err } } - if cmd.Flag("infra-image").Changed { - // Only send content to server side if user changed defaults - img, err = cmd.Flags().GetString("infra-image") - imageName = img - if err != nil { - return err - } - } podName = createOptions.Name err = common.ContainerToPodOptions(&infraOptions, &createOptions) if err != nil { diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 6da34050e..418a70e1e 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -357,6 +357,11 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { pFlags.StringVar(&cfg.Engine.Namespace, namespaceFlagName, cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") _ = cmd.RegisterFlagCompletionFunc(namespaceFlagName, completion.AutocompleteNone) + networkBackendFlagName := "network-backend" + pFlags.StringVar(&cfg.Network.NetworkBackend, networkBackendFlagName, cfg.Network.NetworkBackend, `Network backend to use ("cni"|"netavark")`) + _ = cmd.RegisterFlagCompletionFunc(networkBackendFlagName, common.AutocompleteNetworkBackend) + pFlags.MarkHidden(networkBackendFlagName) + rootFlagName := "root" pFlags.StringVar(&cfg.Engine.StaticDir, rootFlagName, "", "Path to the root directory in which data, including images, is stored") _ = cmd.RegisterFlagCompletionFunc(rootFlagName, completion.AutocompleteDefault) diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go index 290b3c245..ee237d7d0 100644 --- a/cmd/podman/system/connection/add.go +++ b/cmd/podman/system/connection/add.go @@ -226,12 +226,7 @@ func getUDS(cmd *cobra.Command, uri *url.URL, iden string) (string, error) { if v, found := os.LookupEnv("PODMAN_BINARY"); found { podman = v } - run := podman + " info --format=json" - out, err := ExecRemoteCommand(dial, run) - if err != nil { - return "", err - } - infoJSON, err := json.Marshal(out) + infoJSON, err := ExecRemoteCommand(dial, podman+" info --format=json") if err != nil { return "", err } diff --git a/cmd/podman/system/connection/list.go b/cmd/podman/system/connection/list.go index a3290e3d6..f1f7657ad 100644 --- a/cmd/podman/system/connection/list.go +++ b/cmd/podman/system/connection/list.go @@ -53,10 +53,6 @@ func list(cmd *cobra.Command, _ []string) error { return err } - if len(cfg.Engine.ServiceDestinations) == 0 { - return nil - } - hdrs := []map[string]string{{ "Identity": "Identity", "Name": "Name", diff --git a/cmd/podman/system/connection/remove.go b/cmd/podman/system/connection/remove.go index 73bae4994..ffbea76c5 100644 --- a/cmd/podman/system/connection/remove.go +++ b/cmd/podman/system/connection/remove.go @@ -5,14 +5,14 @@ import ( "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/system" + "github.com/pkg/errors" "github.com/spf13/cobra" ) var ( // Skip creating engines since this command will obtain connection information to said engines rmCmd = &cobra.Command{ - Use: "remove NAME", - Args: cobra.ExactArgs(1), + Use: "remove [options] NAME", Aliases: []string{"rm"}, Long: `Delete named destination from podman configuration`, Short: "Delete named destination", @@ -21,6 +21,10 @@ var ( Example: `podman system connection remove devl podman system connection rm devl`, } + + rmOpts = struct { + All bool + }{} ) func init() { @@ -28,14 +32,31 @@ func init() { Command: rmCmd, Parent: system.ConnectionCmd, }) + + flags := rmCmd.Flags() + flags.BoolVarP(&rmOpts.All, "all", "a", false, "Remove all connections") } -func rm(_ *cobra.Command, args []string) error { +func rm(cmd *cobra.Command, args []string) error { cfg, err := config.ReadCustomConfig() if err != nil { return err } + if rmOpts.All { + if cfg.Engine.ServiceDestinations != nil { + for k := range cfg.Engine.ServiceDestinations { + delete(cfg.Engine.ServiceDestinations, k) + } + } + cfg.Engine.ActiveService = "" + return cfg.Write() + } + + if len(args) != 1 { + return errors.New("accepts 1 arg(s), received 0") + } + if cfg.Engine.ServiceDestinations != nil { delete(cfg.Engine.ServiceDestinations, args[0]) } diff --git a/cmd/podman/system/connection/shared.go b/cmd/podman/system/connection/shared.go index 3fd7c59fb..714ae827d 100644 --- a/cmd/podman/system/connection/shared.go +++ b/cmd/podman/system/connection/shared.go @@ -9,10 +9,10 @@ import ( // ExecRemoteCommand takes a ssh client connection and a command to run and executes the // command on the specified client. The function returns the Stdout from the client or the Stderr -func ExecRemoteCommand(dial *ssh.Client, run string) (string, error) { +func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) { sess, err := dial.NewSession() // new ssh client session if err != nil { - return "", err + return nil, err } defer sess.Close() @@ -21,8 +21,7 @@ func ExecRemoteCommand(dial *ssh.Client, run string) (string, error) { sess.Stdout = &buffer // output from client funneled into buffer sess.Stderr = &bufferErr // err form client funneled into buffer if err := sess.Run(run); err != nil { // run the command on the ssh client - return "", errors.Wrapf(err, bufferErr.String()) + return nil, errors.Wrapf(err, bufferErr.String()) } - out := buffer.String() // output from command - return out, nil + return buffer.Bytes(), nil } diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go index fc07a6acc..6b5425a69 100644 --- a/cmd/podman/validate/args.go +++ b/cmd/podman/validate/args.go @@ -27,7 +27,8 @@ func SubCommandExists(cmd *cobra.Command, args []string) error { } return errors.Errorf("unrecognized command `%[1]s %[2]s`\n\nDid you mean this?\n\t%[3]s\n\nTry '%[1]s --help' for more information.", cmd.CommandPath(), args[0], strings.Join(suggestions, "\n\t")) } - return errors.Errorf("missing command '%[1]s COMMAND'\nTry '%[1]s --help' for more information.", cmd.CommandPath()) + cmd.Help() + return errors.Errorf("missing command '%[1]s COMMAND'", cmd.CommandPath()) } // IDOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag diff --git a/cmd/rootlessport/main.go b/cmd/rootlessport/main.go index feb9f5c06..e691ce2fc 100644 --- a/cmd/rootlessport/main.go +++ b/cmd/rootlessport/main.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v3/libpod/network/types" @@ -289,25 +290,30 @@ func handler(ctx context.Context, conn io.Reader, pm rkport.Manager) error { return nil } -func exposePorts(pm rkport.Manager, portMappings []types.OCICNIPortMapping, childIP string) error { +func exposePorts(pm rkport.Manager, portMappings []types.PortMapping, childIP string) error { ctx := context.TODO() - for _, i := range portMappings { - hostIP := i.HostIP - if hostIP == "" { - hostIP = "0.0.0.0" - } - spec := rkport.Spec{ - Proto: i.Protocol, - ParentIP: hostIP, - ParentPort: int(i.HostPort), - ChildPort: int(i.ContainerPort), - ChildIP: childIP, - } - if err := rkportutil.ValidatePortSpec(spec, nil); err != nil { - return err - } - if _, err := pm.AddPort(ctx, spec); err != nil { - return err + for _, port := range portMappings { + protocols := strings.Split(port.Protocol, ",") + for _, protocol := range protocols { + hostIP := port.HostIP + if hostIP == "" { + hostIP = "0.0.0.0" + } + for i := uint16(0); i < port.Range; i++ { + spec := rkport.Spec{ + Proto: protocol, + ParentIP: hostIP, + ParentPort: int(port.HostPort + i), + ChildPort: int(port.ContainerPort + i), + ChildIP: childIP, + } + if err := rkportutil.ValidatePortSpec(spec, nil); err != nil { + return err + } + if _, err := pm.AddPort(ctx, spec); err != nil { + return err + } + } } } return nil diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh index 9b7c613f5..cff8f4b3f 100644 --- a/contrib/cirrus/lib.sh +++ b/contrib/cirrus/lib.sh @@ -166,30 +166,42 @@ setup_rootless() { useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" - msg "creating ssh key pair for $USER" + mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh" + + msg "Creating ssh key pairs" [[ -r "$HOME/.ssh/id_rsa" ]] || \ - ssh-keygen -P "" -f "$HOME/.ssh/id_rsa" + ssh-keygen -t rsa -P "" -f "$HOME/.ssh/id_rsa" + ssh-keygen -t ed25519 -P "" -f "/home/$ROOTLESS_USER/.ssh/id_ed25519" + ssh-keygen -t rsa -P "" -f "/home/$ROOTLESS_USER/.ssh/id_rsa" - msg "Allowing ssh key for $ROOTLESS_USER" - akfilepath="/home/$ROOTLESS_USER/.ssh/authorized_keys" - (umask 077 && mkdir "/home/$ROOTLESS_USER/.ssh") - chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" - install -o $ROOTLESS_USER -g $ROOTLESS_USER -m 0600 \ - "$HOME/.ssh/id_rsa.pub" "$akfilepath" - # Makes debugging easier - cat /root/.ssh/authorized_keys >> "$akfilepath" + msg "Setup authorized_keys" + cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> $HOME/.ssh/authorized_keys + cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> /home/$ROOTLESS_USER/.ssh/authorized_keys msg "Ensure the ssh daemon is up and running within 5 minutes" systemctl start sshd - sshcmd="ssh $ROOTLESS_USER@localhost - -o UserKnownHostsFile=/dev/null - -o StrictHostKeyChecking=no - -o CheckHostIP=no" - lilto $sshcmd true # retry until sshd is up - - msg "Configuring rootless user self-access to ssh to localhost" - $sshcmd ssh-keygen -P '""' -f "/home/$ROOTLESS_USER/.ssh/id_rsa" - cat "/home/$ROOTLESS_USER/.ssh/id_rsa" >> "$akfilepath" + lilto systemctl is-active sshd + + msg "Configure ssh file permissions" + chmod -R 700 "$HOME/.ssh" + chmod -R 700 "/home/$ROOTLESS_USER/.ssh" + chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" + + msg " setup known_hosts for $USER" + ssh -q root@localhost \ + -o UserKnownHostsFile=/root/.ssh/known_hosts \ + -o UpdateHostKeys=yes \ + -o StrictHostKeyChecking=no \ + -o CheckHostIP=no \ + true + + msg " setup known_hosts for $ROOTLESS_USER" + su $ROOTLESS_USER -c "ssh -q $ROOTLESS_USER@localhost \ + -o UserKnownHostsFile=/home/$ROOTLESS_USER/.ssh/known_hosts \ + -o UpdateHostKeys=yes \ + -o StrictHostKeyChecking=no \ + -o CheckHostIP=no \ + true" } install_test_configs() { diff --git a/contrib/cirrus/pr-should-include-tests b/contrib/cirrus/pr-should-include-tests index 4b6329311..8103df41d 100755 --- a/contrib/cirrus/pr-should-include-tests +++ b/contrib/cirrus/pr-should-include-tests @@ -12,9 +12,6 @@ fi if [[ "${CIRRUS_CHANGE_MESSAGE}" =~ NO.NEW.TESTS.NEEDED ]]; then exit 0 fi -if [[ "${CIRRUS_CHANGE_MESSAGE}" =~ NO.TESTS.NEEDED ]]; then - exit 0 -fi # HEAD should be good enough, but the CIRRUS envariable allows us to test head=${CIRRUS_CHANGE_IN_REPO:-HEAD} @@ -52,14 +49,11 @@ if [[ -z "$filtered_changes" ]]; then exit 0 fi -# One last chance: perhaps the developer included the magic '[NO (NEW) TESTS NEEDED]' +# One last chance: perhaps the developer included the magic '[NO NEW TESTS NEEDED]' # string in an amended commit. if git log --format=%B ${base}..${head} | fgrep '[NO NEW TESTS NEEDED]'; then exit 0 fi -if git log --format=%B ${base}..${head} | fgrep '[NO TESTS NEEDED]'; then - exit 0 -fi cat <<EOF $(basename $0): PR does not include changes in the 'tests' directory diff --git a/contrib/podmanimage/stable/Dockerfile b/contrib/podmanimage/stable/Dockerfile index f980d15ed..7950269d2 100644 --- a/contrib/podmanimage/stable/Dockerfile +++ b/contrib/podmanimage/stable/Dockerfile @@ -19,14 +19,15 @@ RUN useradd podman; \ echo podman:10000:5000 > /etc/subuid; \ echo podman:10000:5000 > /etc/subgid; -VOLUME /var/lib/containers -VOLUME /home/podman/.local/share/containers -RUN mkdir -p /home/podman/.local/share/containers +RUN mkdir -p /home/podman/.local/share/containers; chown podman:podman -R /home/podman ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/containers.conf /etc/containers/containers.conf ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/podman-containers.conf /home/podman/.config/containers/containers.conf -RUN chown podman:podman -R /home/podman +# Note VOLUME options must always happen after the chown call above +# RUN commands can not modify existing volumes +VOLUME /var/lib/containers +VOLUME /home/podman/.local/share/containers # chmod containers.conf and adjust storage.conf to enable Fuse storage. RUN chmod 644 /etc/containers/containers.conf; sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' /etc/containers/storage.conf diff --git a/contrib/podmanimage/testing/Dockerfile b/contrib/podmanimage/testing/Dockerfile index 58b11a20b..03da05806 100644 --- a/contrib/podmanimage/testing/Dockerfile +++ b/contrib/podmanimage/testing/Dockerfile @@ -19,14 +19,15 @@ RUN useradd podman; \ echo podman:10000:5000 > /etc/subuid; \ echo podman:10000:5000 > /etc/subgid; -VOLUME /var/lib/containers -VOLUME /home/podman/.local/share/containers -RUN mkdir -p /home/podman/.local/share/containers +RUN mkdir -p /home/podman/.local/share/containers; chown podman:podman -R /home/podman ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/containers.conf /etc/containers/containers.conf ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/podman-containers.conf /home/podman/.config/containers/containers.conf -RUN chown podman:podman -R /home/podman +# Note VOLUME options must always happen after the chown call above +# RUN commands can not modify existing volumes +VOLUME /var/lib/containers +VOLUME /home/podman/.local/share/containers # chmod containers.conf and adjust storage.conf to enable Fuse storage. RUN chmod 644 /etc/containers/containers.conf; sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' /etc/containers/storage.conf diff --git a/contrib/podmanimage/upstream/Dockerfile b/contrib/podmanimage/upstream/Dockerfile index 75de947ea..89ec6e39b 100644 --- a/contrib/podmanimage/upstream/Dockerfile +++ b/contrib/podmanimage/upstream/Dockerfile @@ -68,14 +68,15 @@ RUN useradd podman; \ echo podman:10000:5000 > /etc/subuid; \ echo podman:10000:5000 > /etc/subgid; -VOLUME /var/lib/containers -VOLUME /home/podman/.local/share/containers -RUN mkdir -p /home/podman/.local/share/containers +RUN mkdir -p /home/podman/.local/share/containers; chown podman:podman -R /home/podman ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/containers.conf /etc/containers/containers.conf ADD https://raw.githubusercontent.com/containers/libpod/master/contrib/podmanimage/stable/podman-containers.conf /home/podman/.config/containers/containers.conf -RUN chown podman:podman -R /home/podman +# Note VOLUME options must always happen after the chown call above +# RUN commands can not modify existing volumes +VOLUME /var/lib/containers +VOLUME /home/podman/.local/share/containers # chmod containers.conf and adjust storage.conf to enable Fuse storage. RUN chmod 644 /etc/containers/containers.conf; sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' /etc/containers/storage.conf diff --git a/docs/source/markdown/podman-auto-update.1.md b/docs/source/markdown/podman-auto-update.1.md index 4952e09dc..755f46d70 100644 --- a/docs/source/markdown/podman-auto-update.1.md +++ b/docs/source/markdown/podman-auto-update.1.md @@ -141,4 +141,4 @@ $ podman auto-update ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-generate-systemd(1)](podman-generate-systemd.1.md)**, **[podman-run(1)](podman-run.1.md)**, sd_notify(3), systemd.unit(5) +**[podman(1)](podman.1.md)**, **[podman-generate-systemd(1)](podman-generate-systemd.1.md)**, **[podman-run(1)](podman-run.1.md)**, **sd_notify(3)**, **[systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)** diff --git a/docs/source/markdown/podman-build.1.md b/docs/source/markdown/podman-build.1.md index 5a867c574..40bd7d8aa 100644 --- a/docs/source/markdown/podman-build.1.md +++ b/docs/source/markdown/podman-build.1.md @@ -241,7 +241,7 @@ Note: if _host_device_ is a symbolic link then it will be resolved first. The container will only store the major and minor numbers of the host device. Note: if the user only has access rights via a group, accessing the device -from inside a rootless container will fail. The **crun**(1) runtime offers a +from inside a rootless container will fail. The **[crun(1)](https://github.com/containers/crun/tree/main/crun.1.md)** runtime offers a workaround for this by adding the option #### **--annotation run.oci.keep_original_groups=1**. @@ -774,6 +774,14 @@ content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Podman to label the content with a private unshared label. Only the current container can use a private volume. +Note: Do not relabel system files and directories. Relabeling system content +might cause other confined services on your machine to fail. For these types +of containers, disabling SELinux separation is recommended. The option +`--security-opt label=disable` disables SELinux separation for the container. +For example, if a user wanted to volume mount their entire home directory into the build containers, they need to disable SELinux separation. + + $ podman build --security-opt label=disable -v $HOME:/home/user . + `Overlay Volume Mounts` The `:O` flag tells Podman to mount the directory from the host as a @@ -1013,7 +1021,7 @@ If you are using `useradd` within your build script, you should pass the useradd to stop creating the lastlog file. ## SEE ALSO -podman(1), buildah(1), containers-certs.d(5), containers-registries.conf(5), crun(8), runc(8), useradd(8), podman-ps(1), podman-rm(1), Containerfile(5), containerignore(5) +**[podman(1)](podman.1.md)**, **[buildah(1)](https://github.com/containers/buildah/blob/main/docs/buildah.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)**, **[crun(1)](https://github.com/containers/crun/blob/main/crun.1.md)**, **[runc(8)](https://github.com/opencontainers/runc/blob/master/man/runc.8.md)**, **[useradd(8)](https://www.unix.com/man-page/redhat/8/useradd)**, **[podman-ps(1)](podman-ps.1.md)**, **[podman-rm(1)](podman-rm.1.md)**, **[Containerfile(5)](https://github.com/containers/buildah/blob/main/docs/Containerfile.5.md)**, **[containerignore(5)](https://github.com/containers/buildah/blob/main/docs/containerignore.5.md)** ## HISTORY Aug 2020, Additional options and .containerignore added by Dan Walsh `<dwalsh@redhat.com>` diff --git a/docs/source/markdown/podman-completion.1.md b/docs/source/markdown/podman-completion.1.md index 639332365..538bb9e60 100644 --- a/docs/source/markdown/podman-completion.1.md +++ b/docs/source/markdown/podman-completion.1.md @@ -40,7 +40,7 @@ Shell completion needs to be already enabled in the environment. The following c **echo "autoload -U compinit; compinit" >> ~/.zshrc** To make it available for all zsh sessions run:\ -**podman completion -f "${fpath[1]}/_podman zsh"** +**podman completion -f "${fpath[1]}/_podman" zsh** Once the shell is reloaded the auto-completion should be working. @@ -61,4 +61,4 @@ completion output to a file and source that to the user's powershell profile. More information about profiles is available with **Get-Help about_Profiles**. ## SEE ALSO -**[podman(1)](podman.1.md)**, zsh(1), fish(1), powershell(1) +**[podman(1)](podman.1.md)**, **zsh(1)**, **fish(1)**, **powershell(1)** diff --git a/docs/source/markdown/podman-container-cleanup.1.md b/docs/source/markdown/podman-container-cleanup.1.md index 29e34f1cf..e58519ac3 100644 --- a/docs/source/markdown/podman-container-cleanup.1.md +++ b/docs/source/markdown/podman-container-cleanup.1.md @@ -51,7 +51,7 @@ $ podman container cleanup mywebserver myflaskserver 860a4b23 ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, conmon(8) +**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[conmon(8)](https://github.com/containers/conmon/blob/main/docs/conmon.8.md)** ## HISTORY Jun 2018, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-container-inspect.1.md b/docs/source/markdown/podman-container-inspect.1.md index 72b7cef3b..54b3cb2ae 100644 --- a/docs/source/markdown/podman-container-inspect.1.md +++ b/docs/source/markdown/podman-container-inspect.1.md @@ -312,7 +312,7 @@ $ podman container inspect --latest --format {{.EffectiveCaps}} ``` ## SEE ALSO -**[podman(1)](podman.1.md)**,**[podman-container(1)](podman-container.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** +**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** ## HISTORY Sep 2021, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-container-prune.1.md b/docs/source/markdown/podman-container-prune.1.md index 0f679ceff..494500ca1 100644 --- a/docs/source/markdown/podman-container-prune.1.md +++ b/docs/source/markdown/podman-container-prune.1.md @@ -71,7 +71,7 @@ Are you sure you want to continue? [y/N] y ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-ps](podman-ps.1.md)** +**[podman(1)](podman.1.md)**, **[podman-ps(1)](podman-ps.1.md)** ## HISTORY December 2018, Originally compiled by Brent Baude <bbaude@redhat.com>\ diff --git a/docs/source/markdown/podman-container-runlabel.1.md b/docs/source/markdown/podman-container-runlabel.1.md index ec67a2687..7078b07e4 100644 --- a/docs/source/markdown/podman-container-runlabel.1.md +++ b/docs/source/markdown/podman-container-runlabel.1.md @@ -84,7 +84,7 @@ $ podman container runlabel --display run foobar ``` ## SEE ALSO -podman(1), crun(1), runc(1), containers-auth.json(5), containers-certs.d(5), containers-registries.conf(5) +**[podman(1)](podman.1.md)**, **[crun(1)](https://github.com/containers/crun/blob/main/crun.1.md)**, **[runc(8)](https://github.com/opencontainers/runc/blob/master/man/runc.8.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-auth.json(5)](https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)** ## HISTORY August 2021, Refinements by Valentin Rothberg (rothberg at redhat dot com) diff --git a/docs/source/markdown/podman-container.1.md b/docs/source/markdown/podman-container.1.md index c950347a9..540ebc723 100644 --- a/docs/source/markdown/podman-container.1.md +++ b/docs/source/markdown/podman-container.1.md @@ -48,4 +48,4 @@ The container command allows you to manage containers | wait | [podman-wait(1)](podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes. | ## SEE ALSO -podman, podman-exec, podman-run +**[podman(1)](podman.1.md)**, **[podman-exec(1)](podman-exec.1.md)**, **[podman-run(1)](podman-run.1.md)** diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 3ff736adb..b0d7b8f12 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -1249,6 +1249,15 @@ content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Podman to label the content with a private unshared label. Only the current container can use a private volume. +Note: Do not relabel system files and directories. Relabeling system content +might cause other confined services on your machine to fail. For these types +of containers we recommend that disable SELinux separation. The option +`--security-opt label=disable` disables SELinux separation for containers used in the build. +For example if a user wanted to volume mount their entire home directory into a +container, they need to disable SELinux separation. + + $ podman create --security-opt label=disable -v $HOME:/home/user fedora touch /home/user/file + `Overlay Volume Mounts` The `:O` flag tells Podman to mount the directory from the host as a @@ -1528,8 +1537,7 @@ page. NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`. ## SEE ALSO -**podman**(1), **podman-secret**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-start*(1), **podman-kill**(1), **podman-stop**(1), -**podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5), **conmon**(8), **personality**(2). +**[podman(1)](podman.1.md)**, **[podman-save(1)](podman-save.1.md)**, **[podman-ps(1)](podman-ps.1.md)**, **[podman-attach(1)](podman-attach.1.md)**, **[podman-pod-create(1)](podman-pod-create.1.md)**, **[podman-port(1)](podman-port.1.md)**, **[podman-start(1)](podman-start.1.md)**, **[podman-kill(1)](podman-kill.1.md)**, **[podman-stop(1)](podman-stop.1.md)**, **[podman-generate-systemd(1)](podman-generate-systemd.1.md)**, **[podman-rm(1)](podman-rm.1.md)**, **[subgid(5)](https://www.unix.com/man-page/linux/5/subgid)**, **[subuid(5)](https://www.unix.com/man-page/linux/5/subuid)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)**, **[setsebool(8)](https://man7.org/linux/man-pages/man8/setsebool.8.html)**, **[slirp4netns(1)](https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md)**, **[fuse-overlayfs(1)](https://github.com/containers/fuse-overlayfs/blob/main/fuse-overlayfs.1.md)**, **proc(5)**, **[conmon(8)](https://github.com/containers/conmon/blob/main/docs/conmon.8.md)**, **personality(2)** ## HISTORY October 2017, converted from Docker documentation to Podman by Dan Walsh for Podman `<dwalsh@redhat.com>` diff --git a/docs/source/markdown/podman-events.1.md b/docs/source/markdown/podman-events.1.md index 3c8b46884..7ca52e3e9 100644 --- a/docs/source/markdown/podman-events.1.md +++ b/docs/source/markdown/podman-events.1.md @@ -155,7 +155,7 @@ $ podman events --format json ``` ## SEE ALSO -podman(1), containers.conf(5) +**[podman(1)](podman.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)** ## HISTORY March 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-exec.1.md b/docs/source/markdown/podman-exec.1.md index d739e1f5d..c539e987b 100644 --- a/docs/source/markdown/podman-exec.1.md +++ b/docs/source/markdown/podman-exec.1.md @@ -114,7 +114,7 @@ $ podman exec --user root ctrID ls ``` ## SEE ALSO -podman(1), podman-run(1) +**[podman(1)](podman.1.md)**, **[podman-run(1)](podman-run.1.md)** ## HISTORY December 2017, Originally compiled by Brent Baude<bbaude@redhat.com> diff --git a/docs/source/markdown/podman-export.1.md b/docs/source/markdown/podman-export.1.md index 63989a4db..b2ad4e907 100644 --- a/docs/source/markdown/podman-export.1.md +++ b/docs/source/markdown/podman-export.1.md @@ -41,7 +41,7 @@ $ podman export 883504668ec465463bc0fe7e63d53154ac3b696ea8d7b233748918664ea90e57 ``` ## SEE ALSO -podman(1), podman-import(1) +**[podman(1)](podman.1.md)**, **[podman-import(1)](podman-import.1.md)** ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index a583afcf9..41545455e 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -206,7 +206,7 @@ status: ``` ## SEE ALSO -podman(1), podman-container(1), podman-pod(1), podman-play-kube(1) +**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-play-kube(1)](podman-play-kube.1.md)** ## HISTORY December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/source/markdown/podman-generate-systemd.1.md b/docs/source/markdown/podman-generate-systemd.1.md index 8393aec11..0acbb9d8c 100644 --- a/docs/source/markdown/podman-generate-systemd.1.md +++ b/docs/source/markdown/podman-generate-systemd.1.md @@ -235,7 +235,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS bb310a0780ae docker.io/library/alpine:latest /bin/sh 3 minutes ago Created busy_moser ``` ## SEE ALSO -[**podman**(1)](podman.1.md), [**podman-container**(1)](podman-container.1.md), **systemctl**(1), **systemd.unit**(5), **systemd.service**(5), **conmon**(8). +**[podman(1)](podman.1.md)**, **[podman-container(1)](podman-container.1.md)**, **systemctl(1)**, **systemd.unit(5)**, **systemd.service(5)**, **[conmon(8)](https://github.com/containers/conmon/blob/main/docs/conmon.8.md)** ## HISTORY April 2020, Updated details and added use case to use generated .service files as root and non-root, by Sujil Shah (sushah at redhat dot com) diff --git a/docs/source/markdown/podman-generate.1.md b/docs/source/markdown/podman-generate.1.md index c2060d1a4..415a53875 100644 --- a/docs/source/markdown/podman-generate.1.md +++ b/docs/source/markdown/podman-generate.1.md @@ -18,4 +18,4 @@ The generate command will create structured output (like YAML) based on a contai ## SEE ALSO -podman, podman-pod, podman-container +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)** diff --git a/docs/source/markdown/podman-healthcheck-run.1.md b/docs/source/markdown/podman-healthcheck-run.1.md index fa10d5dca..dbaf87a0b 100644 --- a/docs/source/markdown/podman-healthcheck-run.1.md +++ b/docs/source/markdown/podman-healthcheck-run.1.md @@ -33,7 +33,7 @@ $ podman healthcheck run mywebapp ``` ## SEE ALSO -podman-healthcheck(1) +**[podman(1)](podman.1.md)**, **[podman-healthcheck(1)](podman-healthcheck.1.md)** ## HISTORY Feb 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-healthcheck.1.md b/docs/source/markdown/podman-healthcheck.1.md index 91c3e4345..eae71eba9 100644 --- a/docs/source/markdown/podman-healthcheck.1.md +++ b/docs/source/markdown/podman-healthcheck.1.md @@ -16,7 +16,7 @@ podman healthcheck is a set of subcommands that manage container healthchecks | run | [podman-healthcheck-run(1)](podman-healthcheck-run.1.md) | Run a container healthcheck | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY Feb 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-history.1.md b/docs/source/markdown/podman-history.1.md index c9d01f105..4ab2547ea 100644 --- a/docs/source/markdown/podman-history.1.md +++ b/docs/source/markdown/podman-history.1.md @@ -91,7 +91,7 @@ $ podman history --format json debian ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY July 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-image-diff.1.md b/docs/source/markdown/podman-image-diff.1.md index 933cd0f9a..c5c62c751 100644 --- a/docs/source/markdown/podman-image-diff.1.md +++ b/docs/source/markdown/podman-image-diff.1.md @@ -48,7 +48,7 @@ $ podman diff --format json redis:old redis:alpine ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)**, **[podman-image(1)](podman-image.1.md)** ## HISTORY August 2017, Originally compiled by Ryan Cole <rycole@redhat.com> diff --git a/docs/source/markdown/podman-image-exists.1.md b/docs/source/markdown/podman-image-exists.1.md index 5d5cb1647..b585d16f1 100644 --- a/docs/source/markdown/podman-image-exists.1.md +++ b/docs/source/markdown/podman-image-exists.1.md @@ -37,7 +37,7 @@ $ ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)**, **[podman-image(1)](podman-image.1.md)** ## HISTORY November 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/source/markdown/podman-image-inspect.1.md b/docs/source/markdown/podman-image-inspect.1.md index a4f81dfc0..eb8d75786 100644 --- a/docs/source/markdown/podman-image-inspect.1.md +++ b/docs/source/markdown/podman-image-inspect.1.md @@ -99,7 +99,7 @@ $ podman image inspect --format '{{ .Id }}' fedora ``` ## SEE ALSO -**[podman(1)](podman.1.md)**,**[podman-image(1)](podman-image.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** +**[podman(1)](podman.1.md)**, **[podman-image(1)](podman-image.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** ## HISTORY Sep 2021, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-image-mount.1.md b/docs/source/markdown/podman-image-mount.1.md index 8cb9684a7..453266f8c 100644 --- a/docs/source/markdown/podman-image-mount.1.md +++ b/docs/source/markdown/podman-image-mount.1.md @@ -73,4 +73,4 @@ podman image mount --format json ``` ## SEE ALSO -podman(1), podman-image-umount(1), mount(8), podman-unshare(1) +**[podman(1)](podman.1.md)**, **[podman-image(1)](podman-image.1.md)**, **[podman-image-umount(1)](podman-image-umount.1.md)**, **[podman-unshare(1)](podman-unshare.1.md)**, **mount(8)** diff --git a/docs/source/markdown/podman-image-prune.1.md b/docs/source/markdown/podman-image-prune.1.md index 493332ec0..153e3bfe8 100644 --- a/docs/source/markdown/podman-image-prune.1.md +++ b/docs/source/markdown/podman-image-prune.1.md @@ -103,7 +103,7 @@ f9f0a8a58c9e02a2b3250b88cc5c95b1e10245ca2c4161d19376580aaa90f55c ``` ## SEE ALSO -podman(1), podman-images +**[podman(1)](podman.1.md)**, **[podman-images(1)](podman-images.1.md)** ## HISTORY December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/source/markdown/podman-image-scp.1.md b/docs/source/markdown/podman-image-scp.1.md index 420452a4d..b4caf353a 100644 --- a/docs/source/markdown/podman-image-scp.1.md +++ b/docs/source/markdown/podman-image-scp.1.md @@ -8,7 +8,7 @@ podman-image-scp - Securely copy an image from one host to another ## DESCRIPTION **podman image scp** copies container images between hosts on a network. You can load to the remote host or from the remote host as well as in between two remote hosts. -Note: `::` is used to specify the image name depending on if you are saving or loading. +Note: `::` is used to specify the image name depending on if you are saving or loading. Images can also be transferred from rootful to rootless storage on the same machine without using sshd. This feature is not supported on the remote client. **podman image scp [GLOBAL OPTIONS]** @@ -62,8 +62,24 @@ Storing signatures Loaded image(s): docker.io/library/alpine:latest ``` +``` +$ sudo podman image scp root@localhost::alpine username@localhost:: +Copying blob e2eb06d8af82 done +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Run Directory Obtained: /run/user/1000/ +[Run Root: /var/tmp/containers-user-1000/containers Graph Root: /root/.local/share/containers/storage DB Path: /root/.local/share/containers/storage/libpod/bolt_state.db] +Getting image source signatures +Copying blob 5eb901baf107 skipped: already exists +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Loaded image(s): docker.io/library/alpine:latest +``` + ## SEE ALSO -podman(1), podman-load(1), podman-save(1), podman-remote(1), podman-system-connection-add(1), containers.conf(5), containers-transports(5) +**[podman(1)](podman.1.md)**, **[podman-load(1)](podman-load.1.md)**, **[podman-save(1)](podman-save.1.md)**, **[podman-remote(1)](podman-remote.1.md)**, **[podman-system-connection-add(1)](podman-system-connection-add.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** ## HISTORY July 2021, Originally written by Charlie Doern <cdoern@redhat.com> diff --git a/docs/source/markdown/podman-image-sign.1.md b/docs/source/markdown/podman-image-sign.1.md index 616aab8ff..e284955a2 100644 --- a/docs/source/markdown/podman-image-sign.1.md +++ b/docs/source/markdown/podman-image-sign.1.md @@ -61,7 +61,7 @@ the signature will be written into sub-directories of the signature will be 'read' from that same location on a pull-related function. ## SEE ALSO -containers-certs.d(5), containers-registries.d(5) +**[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.d(5)](https://github.com/containers/image/blob/main/docs/containers-registries.d.5.md)** ## HISTORY November 2018, Originally compiled by Qi Wang (qiwan at redhat dot com) diff --git a/docs/source/markdown/podman-image-tree.1.md b/docs/source/markdown/podman-image-tree.1.md index fe36929cc..78f1b1004 100644 --- a/docs/source/markdown/podman-image-tree.1.md +++ b/docs/source/markdown/podman-image-tree.1.md @@ -82,7 +82,7 @@ Image Layers ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY Feb 2019, Originally compiled by Kunal Kushwaha `<kushwaha_kunal_v7@lab.ntt.co.jp>` diff --git a/docs/source/markdown/podman-image-trust.1.md b/docs/source/markdown/podman-image-trust.1.md index 6e2c2287a..61b8966a7 100644 --- a/docs/source/markdown/podman-image-trust.1.md +++ b/docs/source/markdown/podman-image-trust.1.md @@ -85,8 +85,7 @@ Display trust as JSON sudo podman image trust show --json ## SEE ALSO - -containers-policy.json(5) +**[containers-policy.json(5)](https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md)** ## HISTORY January 2019, updated by Tom Sweeney (tsweeney at redhat dot com) diff --git a/docs/source/markdown/podman-image-unmount.1.md b/docs/source/markdown/podman-image-unmount.1.md index 2fedb8f58..71ec5488f 100644 --- a/docs/source/markdown/podman-image-unmount.1.md +++ b/docs/source/markdown/podman-image-unmount.1.md @@ -40,4 +40,4 @@ podman image unmount imageID1 imageID2 imageID3 podman image unmount --all ## SEE ALSO -podman(1), podman-image-mount(1), podman-container-mount(1) +**[podman(1)](podman.1.md)**, **[podman-image-mount(1)](podman-image-mount.1.md)**, **[podman-container-mount(1)](podman-container-mount.1.md)** diff --git a/docs/source/markdown/podman-image.1.md b/docs/source/markdown/podman-image.1.md index 01024519f..356369007 100644 --- a/docs/source/markdown/podman-image.1.md +++ b/docs/source/markdown/podman-image.1.md @@ -37,4 +37,4 @@ The image command allows you to manage images | untag | [podman-untag(1)](podman-untag.1.md) | Removes one or more names from a locally-stored image. | ## SEE ALSO -podman +**[podman(1)](podman.1.md)** diff --git a/docs/source/markdown/podman-images.1.md b/docs/source/markdown/podman-images.1.md index a346b7810..8db291fb4 100644 --- a/docs/source/markdown/podman-images.1.md +++ b/docs/source/markdown/podman-images.1.md @@ -188,7 +188,7 @@ docker.io/library/alpine latest 3fd9065eaf02 5 months ago 4.41 MB ``` ## SEE ALSO -podman(1), containers-storage.conf(5) +**[podman(1)](podman.1.md)**, **[containers-storage.conf(5)](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md)** ## HISTORY March 2017, Originally compiled by Dan Walsh `<dwalsh@redhat.com>` diff --git a/docs/source/markdown/podman-import.1.md b/docs/source/markdown/podman-import.1.md index 989e23b74..462e9eacf 100644 --- a/docs/source/markdown/podman-import.1.md +++ b/docs/source/markdown/podman-import.1.md @@ -107,7 +107,7 @@ db65d991f3bbf7f31ed1064db9a6ced7652e3f8166c4736aa9133dadd3c7acb3 ``` ## SEE ALSO -podman(1), podman-export(1) +**[podman(1)](podman.1.md)**, **[podman-export(1)](podman-export.1.md)** ## HISTORY November 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-info.1.md b/docs/source/markdown/podman-info.1.md index 4f873248d..4f09913b8 100644 --- a/docs/source/markdown/podman-info.1.md +++ b/docs/source/markdown/podman-info.1.md @@ -290,4 +290,4 @@ map[registries:[docker.io quay.io registry.fedoraproject.org registry.access.red ``` ## SEE ALSO -podman(1), containers-registries.conf(5), containers-storage.conf(5) +**[podman(1)](podman.1.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)**, **[containers-storage.conf(5)](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md)** diff --git a/docs/source/markdown/podman-init.1.md b/docs/source/markdown/podman-init.1.md index 5f3636686..6d1d92e04 100644 --- a/docs/source/markdown/podman-init.1.md +++ b/docs/source/markdown/podman-init.1.md @@ -36,7 +36,7 @@ podman init test1 podman init --latest ## SEE ALSO -podman(1), podman-start(1) +**[podman(1)](podman.1.md)**, **[podman-start(1)](podman-start.1.md)** ## HISTORY April 2019, Originally compiled by Matthew Heon <mheon@redhat.com> diff --git a/docs/source/markdown/podman-inspect.1.md b/docs/source/markdown/podman-inspect.1.md index 83dc5cbbe..259a6c992 100644 --- a/docs/source/markdown/podman-inspect.1.md +++ b/docs/source/markdown/podman-inspect.1.md @@ -162,7 +162,7 @@ myNetwork ``` ## SEE ALSO -**[podman(1)](podman.1.md)**,**[podman-container-inspect(1)](podman-container-inspect.1.md)**,**[podman-image-inspect(1)](podman-image-inspect.1.md)**,**[podman-network-inspect(1)](podman-network-inspect.1.md)**,**[podman-pod-inspect(1)](podman-pod-inspect.1.md)**,**[podman-volume-inspect(1)](podman-volume-inspect.1.md)**. +**[podman(1)](podman.1.md)**, **[podman-container-inspect(1)](podman-container-inspect.1.md)**, **[podman-image-inspect(1)](podman-image-inspect.1.md)**, **[podman-network-inspect(1)](podman-network-inspect.1.md)**, **[podman-pod-inspect(1)](podman-pod-inspect.1.md)**, **[podman-volume-inspect(1)](podman-volume-inspect.1.md)** ## HISTORY July 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-kill.1.md b/docs/source/markdown/podman-kill.1.md index e5f1d4bbe..dc79a44dc 100644 --- a/docs/source/markdown/podman-kill.1.md +++ b/docs/source/markdown/podman-kill.1.md @@ -47,7 +47,7 @@ podman kill --cidfile /home/user/cidfile-1 podman kill --cidfile /home/user/cidfile-1 --cidfile ./cidfile-2 ## SEE ALSO -podman(1), podman-stop(1) +**[podman(1)](podman.1.md)**, **[podman-stop(1)](podman-stop.1.md)** ## HISTORY September 2017, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-load.1.md b/docs/source/markdown/podman-load.1.md index 3ff03e9e7..d1735750f 100644 --- a/docs/source/markdown/podman-load.1.md +++ b/docs/source/markdown/podman-load.1.md @@ -28,7 +28,7 @@ Note: `:` is a restricted character and cannot be part of the file name. #### **--input**, **-i**=*input* -Read from archive file, default is STDIN. +Load the specified input file instead of from stdin. The file can be on the local file system or on a server (e.g., https://server.com/archive.tar) The remote client requires the use of this option. @@ -49,7 +49,7 @@ $ podman load --quiet -i fedora.tar ``` ``` -$ podman load -q -i fedora.tar +$ podman load -q -i https://server.com/archive.tar ``` ``` @@ -77,7 +77,7 @@ Loaded image: registry.fedoraproject.org/fedora:latest ``` ## SEE ALSO -podman(1), podman-save(1) +**[podman(1)](podman.1.md)**, **[podman-save(1)](podman-save.1.md)** ## HISTORY July 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-login.1.md b/docs/source/markdown/podman-login.1.md index 2559f4b77..d7e0152a8 100644 --- a/docs/source/markdown/podman-login.1.md +++ b/docs/source/markdown/podman-login.1.md @@ -120,7 +120,7 @@ Login Succeeded! ``` ## SEE ALSO -podman(1), podman-logout(1), containers-auth.json(5), containers-certs.d(5), containers-registries.conf(5) +**[podman(1)](podman.1.md)**, **[podman-logout(1)](podman-logout.1.md)**, **[containers-auth.json(5)](https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)** ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-logout.1.md b/docs/source/markdown/podman-logout.1.md index c35c4856b..e34c80e95 100644 --- a/docs/source/markdown/podman-logout.1.md +++ b/docs/source/markdown/podman-logout.1.md @@ -54,7 +54,7 @@ Remove login credentials for all registries ``` ## SEE ALSO -podman(1), podman-login(1), containers-auth.json(5) +**[podman(1)](podman.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-auth.json(5)](https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md)** ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-logs.1.md b/docs/source/markdown/podman-logs.1.md index 6b3ef79d7..888279990 100644 --- a/docs/source/markdown/podman-logs.1.md +++ b/docs/source/markdown/podman-logs.1.md @@ -123,7 +123,7 @@ AH00558: httpd: Could not reliably determine the server's fully qualified domain ``` ## SEE ALSO -podman(1), podman-run(1), podman-container-rm(1) +**[podman(1)](podman.1.md)**, **[podman-run(1)](podman-run.1.md)**, **[podman-container-rm(1)](podman-container-rm.1.md)** ## HISTORY February 2018, Updated by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-machine-init.1.md b/docs/source/markdown/podman-machine-init.1.md index 45f798cd6..3abc7c9ea 100644 --- a/docs/source/markdown/podman-machine-init.1.md +++ b/docs/source/markdown/podman-machine-init.1.md @@ -69,7 +69,7 @@ $ podman machine init --memory=1024 myvm ``` ## SEE ALSO -podman-machine(1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine-list.1.md b/docs/source/markdown/podman-machine-list.1.md index cad7e614b..accdd760c 100644 --- a/docs/source/markdown/podman-machine-list.1.md +++ b/docs/source/markdown/podman-machine-list.1.md @@ -48,7 +48,7 @@ $ podman machine ls --format {{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\n ``` ## SEE ALSO -podman-machine(1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine-rm.1.md b/docs/source/markdown/podman-machine-rm.1.md index 4da17fdcb..7c9eb65c5 100644 --- a/docs/source/markdown/podman-machine-rm.1.md +++ b/docs/source/markdown/podman-machine-rm.1.md @@ -59,7 +59,7 @@ Are you sure you want to continue? [y/N] y ``` ## SEE ALSO -podman-machine (1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine-ssh.1.md b/docs/source/markdown/podman-machine-ssh.1.md index dd592622f..db0350961 100644 --- a/docs/source/markdown/podman-machine-ssh.1.md +++ b/docs/source/markdown/podman-machine-ssh.1.md @@ -49,7 +49,7 @@ $ podman machine ssh myvm rpm -q podman ``` ## SEE ALSO -podman-machine (1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine-start.1.md b/docs/source/markdown/podman-machine-start.1.md index 3ab5e96ba..e55dcab13 100644 --- a/docs/source/markdown/podman-machine-start.1.md +++ b/docs/source/markdown/podman-machine-start.1.md @@ -32,7 +32,7 @@ $ podman machine start myvm ``` ## SEE ALSO -podman-machine (1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine-stop.1.md b/docs/source/markdown/podman-machine-stop.1.md index fc31e0bee..9aa781561 100644 --- a/docs/source/markdown/podman-machine-stop.1.md +++ b/docs/source/markdown/podman-machine-stop.1.md @@ -29,7 +29,7 @@ $ podman machine stop myvm ``` ## SEE ALSO -podman-machine (1) +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-machine.1.md b/docs/source/markdown/podman-machine.1.md index 40f69c0ba..8d9e77ea5 100644 --- a/docs/source/markdown/podman-machine.1.md +++ b/docs/source/markdown/podman-machine.1.md @@ -21,7 +21,7 @@ podman\-machine - Manage Podman's virtual machine | stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine | ## SEE ALSO -podman(1), podman-machine-init(1), podman-machine-list(1), podman-machine-rm(1), podman-machine-ssh(1), podman-machine-start(1), podman-machine-stop(1) +**[podman(1)](podman.1.md)**, **[podman-machine-init(1)](podman-machine-init.1.md)**, **[podman-machine-list(1)](podman-machine-list.1.md)**, **[podman-machine-rm(1)](podman-machine-rm.1.md)**, **[podman-machine-ssh(1)](podman-machine-ssh.1.md)**, **[podman-machine-start(1)](podman-machine-start.1.md)**, **[podman-machine-stop(1)](podman-machine-stop.1.md)** ## HISTORY March 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-manifest-add.1.md b/docs/source/markdown/podman-manifest-add.1.md index 376301589..47f3fa25f 100644 --- a/docs/source/markdown/podman-manifest-add.1.md +++ b/docs/source/markdown/podman-manifest-add.1.md @@ -132,4 +132,4 @@ podman manifest add --arch arm64 --variant v8 mylist:v1.11 docker://71c201d10fff ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-manifest-push(1), podman-manifest-remove(1), podman-rmi(1), containers-certs.d(5) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-annotate.1.md b/docs/source/markdown/podman-manifest-annotate.1.md index 2126b71ec..2b8b70aa4 100644 --- a/docs/source/markdown/podman-manifest-annotate.1.md +++ b/docs/source/markdown/podman-manifest-annotate.1.md @@ -58,4 +58,4 @@ podman manifest annotate --arch arm64 --variant v8 mylist:v1.11 sha256:59eec8837 ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-add(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-rmi(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-create.1.md b/docs/source/markdown/podman-manifest-create.1.md index 3a3d5e4b0..77a4b9db6 100644 --- a/docs/source/markdown/podman-manifest-create.1.md +++ b/docs/source/markdown/podman-manifest-create.1.md @@ -40,4 +40,4 @@ podman manifest create --all mylist:v1.11 docker://fedora ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-add(1), podman-manifest-inspect(1), podman-manifest-push(1), podman-manifest-remove(1), podman-rmi(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-exists.1.md b/docs/source/markdown/podman-manifest-exists.1.md index e15110126..dd344046d 100644 --- a/docs/source/markdown/podman-manifest-exists.1.md +++ b/docs/source/markdown/podman-manifest-exists.1.md @@ -37,7 +37,7 @@ $ ``` ## SEE ALSO -podman(1), podman-manifest-create(1), podman-manifest-remove(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** ## HISTORY January 2021, Originally compiled by Paul Holzinger `<paul.holzinger@web.de>` diff --git a/docs/source/markdown/podman-manifest-inspect.1.md b/docs/source/markdown/podman-manifest-inspect.1.md index a4c58bd13..b2c6b1935 100644 --- a/docs/source/markdown/podman-manifest-inspect.1.md +++ b/docs/source/markdown/podman-manifest-inspect.1.md @@ -21,4 +21,4 @@ podman manifest inspect mylist:v1.11 ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-create(1), podman-manifest-add(1), podman-manifest-push(1), podman-manifest-remove(1), podman-rmi(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-push.1.md b/docs/source/markdown/podman-manifest-push.1.md index 2cf1cc375..b50b8748a 100644 --- a/docs/source/markdown/podman-manifest-push.1.md +++ b/docs/source/markdown/podman-manifest-push.1.md @@ -107,4 +107,4 @@ podman manifest push mylist:v1.11 docker://registry.example.org/mylist:v1.11 ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-add(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-manifest-remove(1), podman-rmi(1), containers-certs.d(5) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-remove.1.md b/docs/source/markdown/podman-manifest-remove.1.md index c13714195..256d5a5b5 100644 --- a/docs/source/markdown/podman-manifest-remove.1.md +++ b/docs/source/markdown/podman-manifest-remove.1.md @@ -20,4 +20,4 @@ e604eabaaee4858232761b4fef84e2316ed8f93e15eceafce845966ee3400036 :sha256:cb8a924 ``` ## SEE ALSO -podman(1), podman-manifest(1), podman-manifest-add(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-manifest-push(1), podman-rmi(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)** diff --git a/docs/source/markdown/podman-manifest-rm.1.md b/docs/source/markdown/podman-manifest-rm.1.md index a65f32936..af7fedefe 100644 --- a/docs/source/markdown/podman-manifest-rm.1.md +++ b/docs/source/markdown/podman-manifest-rm.1.md @@ -22,4 +22,4 @@ storage.conf is the storage configuration file for all tools using containers/st The storage configuration file specifies all of the available container storage options for tools using shared container storage. ## SEE ALSO -podman(1), containers-storage.conf(5), podman-manifest(1) +**[podman(1)](podman.1.md)**, **[podman-manifest(1)](podman-manifest.1.md)**, **[containers-storage.conf(5)](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md)** diff --git a/docs/source/markdown/podman-manifest.1.md b/docs/source/markdown/podman-manifest.1.md index 964f89afe..5ff44c1d5 100644 --- a/docs/source/markdown/podman-manifest.1.md +++ b/docs/source/markdown/podman-manifest.1.md @@ -69,4 +69,4 @@ be performed like this: ## SEE ALSO -podman(1), podman-manifest-add(1), podman-manifest-annotate(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-manifest-push(1), podman-manifest-remove(1) +**[podman(1)](podman.1.md)**, **[podman-manifest-add(1)](podman-manifest-add.1.md)**, **[podman-manifest-annotate(1)](podman-manifest-annotate.1.md)**, **[podman-manifest-create(1)](podman-manifest-create.1.md)**, **[podman-manifest-inspect(1)](podman-manifest-inspect.1.md)**, **[podman-manifest-push(1)](podman-manifest-push.1.md)**, **[podman-manifest-remove(1)](podman-manifest-remove.1.md)** diff --git a/docs/source/markdown/podman-mount.1.md b/docs/source/markdown/podman-mount.1.md index 110fb0500..a4ce25bce 100644 --- a/docs/source/markdown/podman-mount.1.md +++ b/docs/source/markdown/podman-mount.1.md @@ -73,4 +73,4 @@ a7060253093b /var/lib/containers/storage/overlay/0ff7d7ca68bed1ace424f9df154d2dd ``` ## SEE ALSO -podman(1), podman-umount(1), mount(8), podman-unshare(1) +**[podman(1)](podman.1.md)**, **[podman-umount(1)](podman-umount.1.md)**, **[podman-unshare(1)](podman-unshare.1.md)**, **mount(8)** diff --git a/docs/source/markdown/podman-network-connect.1.md b/docs/source/markdown/podman-network-connect.1.md index 39893c676..b998d4b7e 100644 --- a/docs/source/markdown/podman-network-connect.1.md +++ b/docs/source/markdown/podman-network-connect.1.md @@ -30,7 +30,7 @@ podman network connect --alias web1 --alias web2 test web ``` ## SEE ALSO -podman(1), podman-network(1), podman-network-disconnect(1), podman-network-inspect(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)**, **[podman-network-disconnect(1)](podman-network-disconnect.1.md)** ## HISTORY November 2020, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network-disconnect.1.md b/docs/source/markdown/podman-network-disconnect.1.md index a13aa6088..f3ca059a1 100644 --- a/docs/source/markdown/podman-network-disconnect.1.md +++ b/docs/source/markdown/podman-network-disconnect.1.md @@ -26,7 +26,7 @@ podman network disconnect test web ## SEE ALSO -podman(1), podman-network(1), podman-network-connect(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)**, **[podman-network-connect(1)](podman-network-connect.1.md)** ## HISTORY November 2020, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network-exists.1.md b/docs/source/markdown/podman-network-exists.1.md index 3ad3ccedb..ee62a830e 100644 --- a/docs/source/markdown/podman-network-exists.1.md +++ b/docs/source/markdown/podman-network-exists.1.md @@ -38,7 +38,7 @@ $ ``` ## SEE ALSO -podman(1), podman-network-create(1), podman-network-rm(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)** ## HISTORY January 2021, Originally compiled by Paul Holzinger `<paul.holzinger@web.de>` diff --git a/docs/source/markdown/podman-network-prune.1.md b/docs/source/markdown/podman-network-prune.1.md index e18ffaaea..d35decb1b 100644 --- a/docs/source/markdown/podman-network-prune.1.md +++ b/docs/source/markdown/podman-network-prune.1.md @@ -39,7 +39,7 @@ podman network prune ## SEE ALSO -podman(1), podman-network(1), podman-network-remove(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)**, **[podman-network-rm(1)](podman-network-rm.1.md)** ## HISTORY February 2021, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network-reload.1.md b/docs/source/markdown/podman-network-reload.1.md index 593265df6..f091c34dc 100644 --- a/docs/source/markdown/podman-network-reload.1.md +++ b/docs/source/markdown/podman-network-reload.1.md @@ -52,7 +52,7 @@ fe7e8eca56f844ec33af10f0aa3b31b44a172776e3277b9550a623ed5d96e72b ## SEE ALSO -podman(1), podman-network(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)** ## HISTORY December 2020, Originally compiled by Paul Holzinger <paul.holzinger@web.de> diff --git a/docs/source/markdown/podman-network-rm.1.md b/docs/source/markdown/podman-network-rm.1.md index b12e81810..12102ba5a 100644 --- a/docs/source/markdown/podman-network-rm.1.md +++ b/docs/source/markdown/podman-network-rm.1.md @@ -45,7 +45,7 @@ Deleted: fred **125** The command fails for any other reason ## SEE ALSO -podman(1), podman-network(1), podman-network-inspect(1) +**[podman(1)](podman.1.md)**, **[podman-network(1)](podman-network.1.md)** ## HISTORY August 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network.1.md b/docs/source/markdown/podman-network.1.md index 885c957b6..29ee70139 100644 --- a/docs/source/markdown/podman-network.1.md +++ b/docs/source/markdown/podman-network.1.md @@ -24,4 +24,4 @@ The network command manages CNI networks for Podman. | rm | [podman-network-rm(1)](podman-network-rm.1.md) | Remove one or more CNI networks | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** diff --git a/docs/source/markdown/podman-pause.1.md b/docs/source/markdown/podman-pause.1.md index 90ea1e32e..863be9ed4 100644 --- a/docs/source/markdown/podman-pause.1.md +++ b/docs/source/markdown/podman-pause.1.md @@ -35,7 +35,7 @@ podman pause -a ``` ## SEE ALSO -podman(1), podman-unpause(1) +**[podman(1)](podman.1.md)**, **[podman-unpause(1)](podman-unpause.1.md)** ## HISTORY September 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 4b825ef95..2438f7ad8 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -227,7 +227,7 @@ $ podman play kube demo.yml --network cni1,cni2 Please take into account that CNI networks must be created first using podman-network-create(1). ## SEE ALSO -podman(1), podman-container(1), podman-pod(1), podman-generate-kube(1), podman-play(1), podman-network-create(1), containers-certs.d(5) +**[podman(1)](podman.1.md)**, **[podman-play(1)](podman-play.1.md)**, **[podman-network-create(1)](podman-network-create.1.md)**, **[podman-generate-kube(1)](podman-generate-kube.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)** ## HISTORY December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/source/markdown/podman-play.1.md b/docs/source/markdown/podman-play.1.md index 39101ad31..e5f0d315e 100644 --- a/docs/source/markdown/podman-play.1.md +++ b/docs/source/markdown/podman-play.1.md @@ -17,4 +17,4 @@ file input. Containers will be automatically started. | kube | [podman-play-kube(1)](podman-play-kube.1.md) | Create containers, pods or volumes based on Kubernetes YAML. | ## SEE ALSO -podman, podman-pod(1), podman-container(1), podman-generate(1), podman-play(1), podman-play-kube(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-generate(1)](podman-generate.1.md)**, **[podman-play-kube(1)](podman-play-kube.1.md)** diff --git a/docs/source/markdown/podman-pod-create.1.md b/docs/source/markdown/podman-pod-create.1.md index 061727559..cca90c942 100644 --- a/docs/source/markdown/podman-pod-create.1.md +++ b/docs/source/markdown/podman-pod-create.1.md @@ -432,7 +432,8 @@ $ podman pod create --network slirp4netns:cidr=192.168.0.0/24 ``` ## SEE ALSO -podman-pod(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** + ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-exists.1.md b/docs/source/markdown/podman-pod-exists.1.md index 4c8714341..201dfbefd 100644 --- a/docs/source/markdown/podman-pod-exists.1.md +++ b/docs/source/markdown/podman-pod-exists.1.md @@ -31,7 +31,7 @@ $ ``` ## SEE ALSO -podman-pod(1), podman(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** ## HISTORY December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/source/markdown/podman-pod-inspect.1.md b/docs/source/markdown/podman-pod-inspect.1.md index 0c58b099e..5147a2559 100644 --- a/docs/source/markdown/podman-pod-inspect.1.md +++ b/docs/source/markdown/podman-pod-inspect.1.md @@ -70,7 +70,7 @@ Valid placeholders for the Go template are listed below: ``` ## SEE ALSO -**[podman(1)](podman.1.md)**,**[podman-pod(1)](podman-pod.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** ## HISTORY August 2018, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-pod-kill.1.md b/docs/source/markdown/podman-pod-kill.1.md index d4eef2307..973d8da88 100644 --- a/docs/source/markdown/podman-pod-kill.1.md +++ b/docs/source/markdown/podman-pod-kill.1.md @@ -37,7 +37,7 @@ podman pod kill --latest podman pod kill --all ## SEE ALSO -podman-pod(1), podman-pod-stop(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-stop(1)](podman-pod-stop.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-logs.1.md b/docs/source/markdown/podman-pod-logs.1.md index 5ccc69bb9..5adcd9df5 100644 --- a/docs/source/markdown/podman-pod-logs.1.md +++ b/docs/source/markdown/podman-pod-logs.1.md @@ -85,4 +85,4 @@ podman pod logs --until 30m myserver-pod-1 ``` ## SEE ALSO -podman(1), podman-pod-start(1), podman-pod-rm(1), podman-logs(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-rm(1)](podman-pod-rm.1.md)**, **[podman-logs(1)](podman-logs.1.md)** diff --git a/docs/source/markdown/podman-pod-pause.1.md b/docs/source/markdown/podman-pod-pause.1.md index 2c849a932..0e53b52be 100644 --- a/docs/source/markdown/podman-pod-pause.1.md +++ b/docs/source/markdown/podman-pod-pause.1.md @@ -26,7 +26,7 @@ podman pod pause mywebserverpod podman pod pause 860a4b23 ## SEE ALSO -podman-pod(1), podman-pod-unpause(1), podman-pause(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-unpause(1)](podman-pod-unpause.1.md)**, **[podman-pause(1)](podman-pause.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-prune.1.md b/docs/source/markdown/podman-pod-prune.1.md index e90164c15..6cc9babf1 100644 --- a/docs/source/markdown/podman-pod-prune.1.md +++ b/docs/source/markdown/podman-pod-prune.1.md @@ -26,7 +26,7 @@ $ sudo podman pod prune ``` ## SEE ALSO -podman-pod(1), podman-pod-ps(1), podman-pod-rm(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** ## HISTORY April 2019, Originally compiled by Peter Hunt (pehunt at redhat dot com) diff --git a/docs/source/markdown/podman-pod-ps.1.md b/docs/source/markdown/podman-pod-ps.1.md index 41e06347e..b61f12e90 100644 --- a/docs/source/markdown/podman-pod-ps.1.md +++ b/docs/source/markdown/podman-pod-ps.1.md @@ -169,7 +169,7 @@ POD ID NAME STATUS CREATED INFRA ID NAMES Print a list of pods ## SEE ALSO -podman-pod(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-restart.1.md b/docs/source/markdown/podman-pod-restart.1.md index 65d05f977..c63daa419 100644 --- a/docs/source/markdown/podman-pod-restart.1.md +++ b/docs/source/markdown/podman-pod-restart.1.md @@ -44,7 +44,7 @@ cc8f0bea67b1a1a11aec1ecd38102a1be4b145577f21fc843c7c83b77fc28907 ``` ## SEE ALSO -podman-pod(1), podman-pod-start(1), podman-restart(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-restart(1)](podman-restart.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-rm.1.md b/docs/source/markdown/podman-pod-rm.1.md index fc834a69c..00c82a8f7 100644 --- a/docs/source/markdown/podman-pod-rm.1.md +++ b/docs/source/markdown/podman-pod-rm.1.md @@ -61,7 +61,7 @@ podman pod rm --pod-id-file /path/to/id/file **125** The command fails for any other reason ## SEE ALSO -podman-pod(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-start.1.md b/docs/source/markdown/podman-pod-start.1.md index 40a71d879..7f3adc8ff 100644 --- a/docs/source/markdown/podman-pod-start.1.md +++ b/docs/source/markdown/podman-pod-start.1.md @@ -37,7 +37,7 @@ podman pod start --all podman pod start --pod-id-file /path/to/id/file ## SEE ALSO -podman-pod(1), podman-pod-stop(1), podman-start(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-stop(1)](podman-pod-stop.1.md)** ## HISTORY July 2018, Adapted from podman start man page by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-stats.1.md b/docs/source/markdown/podman-pod-stats.1.md index 3158ffc1c..47d95983a 100644 --- a/docs/source/markdown/podman-pod-stats.1.md +++ b/docs/source/markdown/podman-pod-stats.1.md @@ -85,7 +85,7 @@ ID NAME MEM USAGE / LIMIT ``` ## SEE ALSO -podman-pod(1), podman(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** ## HISTORY February 2019, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-pod-stop.1.md b/docs/source/markdown/podman-pod-stop.1.md index 57abf608d..f2887b5a5 100644 --- a/docs/source/markdown/podman-pod-stop.1.md +++ b/docs/source/markdown/podman-pod-stop.1.md @@ -80,7 +80,7 @@ $ podman pod stop -a -t 1 ``` ## SEE ALSO -podman-pod(1), podman-pod-start(1), podman-stop(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-start(1)](podman-pod-start.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-top.1.md b/docs/source/markdown/podman-pod-top.1.md index d374a0dca..42198c7fa 100644 --- a/docs/source/markdown/podman-pod-top.1.md +++ b/docs/source/markdown/podman-pod-top.1.md @@ -44,7 +44,7 @@ PID SECCOMP COMMAND %CPU ``` ## SEE ALSO -podman-pod(1), podman-top(1), ps(1), seccomp(2), proc(5), capabilities(7) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **ps(1)**, **seccomp(2)**, **proc(5)**, **capabilities(7)** ## HISTORY August 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod-unpause.1.md b/docs/source/markdown/podman-pod-unpause.1.md index 98022a50b..cd91a661b 100644 --- a/docs/source/markdown/podman-pod-unpause.1.md +++ b/docs/source/markdown/podman-pod-unpause.1.md @@ -26,7 +26,7 @@ podman pod unpause mywebserverpod podman pod unpause 860a4b23 ## SEE ALSO -podman-pod(1), podman-pod-pause(1), podman-unpause(1) +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-pod-pause(1)](podman-pod-pause.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-pod.1.md b/docs/source/markdown/podman-pod.1.md index 9de2442bd..71e4dcb59 100644 --- a/docs/source/markdown/podman-pod.1.md +++ b/docs/source/markdown/podman-pod.1.md @@ -30,7 +30,7 @@ podman pod is a set of subcommands that manage pods, or groups of containers. | unpause | [podman-pod-unpause(1)](podman-pod-unpause.1.md) | Unpause one or more pods. | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY July 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/docs/source/markdown/podman-port.1.md b/docs/source/markdown/podman-port.1.md index 7a84d181a..2881cbb7b 100644 --- a/docs/source/markdown/podman-port.1.md +++ b/docs/source/markdown/podman-port.1.md @@ -55,7 +55,7 @@ List the port mappings for a specific container for port 80 and the tcp protocol # ``` ## SEE ALSO -podman(1), podman-inspect(1) +**[podman(1)](podman.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** ## HISTORY January 2018, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-ps.1.md b/docs/source/markdown/podman-ps.1.md index b1a944942..4bae3e6bd 100644 --- a/docs/source/markdown/podman-ps.1.md +++ b/docs/source/markdown/podman-ps.1.md @@ -194,7 +194,7 @@ f78620804e00 scratch buildah 2 hours ago storage Print a list of containers ## SEE ALSO -podman(1), buildah(1), crio(8) +**[podman(1)](podman.1.md)**, **[buildah(1)](https://github.com/containers/buildah/blob/main/docs/buildah.1.md)**, **[crio(8)](https://github.com/cri-o/cri-o/blob/main/docs/crio.8.md)** ## HISTORY August 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-pull.1.md b/docs/source/markdown/podman-pull.1.md index 7fd9732d6..d91571799 100644 --- a/docs/source/markdown/podman-pull.1.md +++ b/docs/source/markdown/podman-pull.1.md @@ -234,7 +234,7 @@ Storing signatures ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-push(1)](podman-push.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.d.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** +**[podman(1)](podman.1.md)**, **[podman-push(1)](podman-push.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** ## HISTORY July 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-push.1.md b/docs/source/markdown/podman-push.1.md index 089d169fb..a145e3194 100644 --- a/docs/source/markdown/podman-push.1.md +++ b/docs/source/markdown/podman-push.1.md @@ -154,4 +154,4 @@ Storing signatures ``` ## SEE ALSO -podman(1), podman-pull(1), podman-login(1), containers-certs.d(5), containers-transports(5) +**[podman(1)](podman.1.md)**, **[podman-pull(1)](podman-pull.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** diff --git a/docs/source/markdown/podman-remote.1.md b/docs/source/markdown/podman-remote.1.md index fb77f3300..72c8d7226 100644 --- a/docs/source/markdown/podman-remote.1.md +++ b/docs/source/markdown/podman-remote.1.md @@ -177,4 +177,4 @@ Users can modify defaults by creating the `$HOME/.config/containers/containers.c Podman uses builtin defaults if no containers.conf file is found. ## SEE ALSO -`containers.conf(5)` +**[podman(1)](podman.1.md)**, **[podman-system-service(1)](podman-system-service.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)** diff --git a/docs/source/markdown/podman-rename.1.md b/docs/source/markdown/podman-rename.1.md index fdb0dac89..4017db505 100644 --- a/docs/source/markdown/podman-rename.1.md +++ b/docs/source/markdown/podman-rename.1.md @@ -35,4 +35,4 @@ $ podman container rename 6e7514b47180 databaseCtr ``` ## SEE ALSO -podman(1), podman-create(1), podman-run(1) +**[podman(1)](podman.1.md)** diff --git a/docs/source/markdown/podman-restart.1.md b/docs/source/markdown/podman-restart.1.md index 9ffca7b58..bb8f13a92 100644 --- a/docs/source/markdown/podman-restart.1.md +++ b/docs/source/markdown/podman-restart.1.md @@ -60,7 +60,7 @@ $ podman restart --all ``` ## SEE ALSO -podman(1), podman-run(1), podman-start(1), podman-create(1) +**[podman(1)](podman.1.md)** ## HISTORY March 2018, Originally compiled by Matt Heon <mheon@redhat.com> diff --git a/docs/source/markdown/podman-rm.1.md b/docs/source/markdown/podman-rm.1.md index ad44fd894..c599c8687 100644 --- a/docs/source/markdown/podman-rm.1.md +++ b/docs/source/markdown/podman-rm.1.md @@ -91,7 +91,7 @@ $ podman rm -f --latest **125** The command fails for any other reason ## SEE ALSO -podman(1), podman-image-rm(1), podman-ps(1), podman-build(1), buildah(1), cri-o(1) +**[podman(1)](podman.1.md)**, **[crio(8)](https://github.com/cri-o/cri-o/blob/main/docs/crio.8.md)** ## HISTORY August 2017, Originally compiled by Ryan Cole <rycole@redhat.com> diff --git a/docs/source/markdown/podman-rmi.1.md b/docs/source/markdown/podman-rmi.1.md index 4f3ec5541..5fe0efa18 100644 --- a/docs/source/markdown/podman-rmi.1.md +++ b/docs/source/markdown/podman-rmi.1.md @@ -53,7 +53,7 @@ $ podman rmi -a -f **125** The command fails for any other reason ## SEE ALSO -podman(1), skopeo-delete(1) +**[podman(1)](podman.1.md)**, **[skopeo-delete(1)](https://github.com/containers/skopeo/blob/main/docs/skopeo-delete.1.md)** ## HISTORY March 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index a1170253f..0fdd47a78 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -1314,6 +1314,15 @@ share the volume content. As a result, Podman labels the content with a shared content label. Shared volume labels allow all containers to read/write content. The **Z** option tells Podman to label the content with a private unshared label. +Note: Do not relabel system files and directories. Relabeling system content +might cause other confined services on your machine to fail. For these types +of containers we recommend that disable SELinux separation. The option +`--security-opt label=disable` disables SELinux separation for the container. +For example if a user wanted to volume mount their entire home directory into a +container, they need to disable SELinux separation. + + $ podman run --security-opt label=disable -v $HOME:/home/user fedora touch /home/user/file + `Overlay Volume Mounts` The `:O` flag tells Podman to mount the directory from the host as a @@ -1882,8 +1891,7 @@ page. NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`. ## SEE ALSO -**podman**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-start**(1), **podman-kill**(1), **podman-stop**(1), -**podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5), **conmon**(8), **personality**(2). +**[podman(1)](podman.1.md)**, **[podman-save(1)](podman-save.1.md)**, **[podman-ps(1)](podman-ps.1.md)**, **[podman-attach(1)](podman-attach.1.md)**, **[podman-pod-create(1)](podman-pod-create.1.md)**, **[podman-port(1)](podman-port.1.md)**, **[podman-start(1)](podman-start.1.md)**, **[podman-kill(1)](podman-kill.1.md)**, **[podman-stop(1)](podman-stop.1.md)**, **[podman-generate-systemd(1)](podman-generate-systemd.1.md)**, **[podman-rm(1)](podman-rm.1.md)**, **[subgid(5)](https://www.unix.com/man-page/linux/5/subgid)**, **[subuid(5)](https://www.unix.com/man-page/linux/5/subuid)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)**, **[setsebool(8)](https://man7.org/linux/man-pages/man8/setsebool.8.html)**, **[slirp4netns(1)](https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md)**, **[fuse-overlayfs(1)](https://github.com/containers/fuse-overlayfs/blob/main/fuse-overlayfs.1.md)**, **proc(5)**, **[conmon(8)](https://github.com/containers/conmon/blob/main/docs/conmon.8.md)**, **personality(2)** ## HISTORY September 2018, updated by Kunal Kushwaha `<kushwaha_kunal_v7@lab.ntt.co.jp>` diff --git a/docs/source/markdown/podman-save.1.md b/docs/source/markdown/podman-save.1.md index 842bc8b41..3fe99e911 100644 --- a/docs/source/markdown/podman-save.1.md +++ b/docs/source/markdown/podman-save.1.md @@ -105,7 +105,7 @@ Storing signatures ``` ## SEE ALSO -podman(1), podman-load(1), containers.conf(5), containers-transports(5) +**[podman(1)](podman.1.md)**, **[podman-load(1)](podman-load.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** ## HISTORY July 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-search.1.md b/docs/source/markdown/podman-search.1.md index 911bbcb4b..9e166fcc2 100644 --- a/docs/source/markdown/podman-search.1.md +++ b/docs/source/markdown/podman-search.1.md @@ -144,7 +144,7 @@ Note: This works only with registries that implement the v2 API. If tried with a registries.conf is the configuration file which specifies which container registries should be consulted when completing image names which do not include a registry or domain portion. ## SEE ALSO -podman(1), containers-registries.conf(5) +**[podman(1)](podman.1.md)**, **[containers-registries(5)](https://github.com/containers/image/blob/main/docs/containers-registries.5.md)** ## HISTORY January 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-secret-create.1.md b/docs/source/markdown/podman-secret-create.1.md index 712ca4aaa..2d504c0ad 100644 --- a/docs/source/markdown/podman-secret-create.1.md +++ b/docs/source/markdown/podman-secret-create.1.md @@ -45,7 +45,7 @@ $ printf <secret> | podman secret create my_secret - ``` ## SEE ALSO -podman-secret (1) +**[podman(1)](podman.1.md)**, **[podman-secret(1)](podman-secret.1.md)** ## HISTORY January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-inspect.1.md b/docs/source/markdown/podman-secret-inspect.1.md index 383db8375..df16ba6fa 100644 --- a/docs/source/markdown/podman-secret-inspect.1.md +++ b/docs/source/markdown/podman-secret-inspect.1.md @@ -32,7 +32,7 @@ $ podman secret inspect --format "{{.Name} {{.Scope}}" mysecret ``` ## SEE ALSO -podman-secret(1) +**[podman(1)](podman.1.md)**, **[podman-secret(1)](podman-secret.1.md)** ## HISTORY January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-ls.1.md b/docs/source/markdown/podman-secret-ls.1.md index 6adbeca08..27576f026 100644 --- a/docs/source/markdown/podman-secret-ls.1.md +++ b/docs/source/markdown/podman-secret-ls.1.md @@ -28,7 +28,7 @@ $ podman secret ls --format "{{.Name}}" ``` ## SEE ALSO -podman-secret(1) +**[podman(1)](podman.1.md)**, **[podman-secret(1)](podman-secret.1.md)** ## HISTORY January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-rm.1.md b/docs/source/markdown/podman-secret-rm.1.md index c6f5079ba..ceab3df60 100644 --- a/docs/source/markdown/podman-secret-rm.1.md +++ b/docs/source/markdown/podman-secret-rm.1.md @@ -33,7 +33,7 @@ $ podman secret rm mysecret1 mysecret2 ``` ## SEE ALSO -podman-secret(1) +**[podman(1)](podman.1.md)**, **[podman-secret(1)](podman-secret.1.md)** ## HISTORY January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret.1.md b/docs/source/markdown/podman-secret.1.md index 125888cff..671a0c43b 100644 --- a/docs/source/markdown/podman-secret.1.md +++ b/docs/source/markdown/podman-secret.1.md @@ -19,7 +19,7 @@ podman secret is a set of subcommands that manage secrets. | rm | [podman-secret-rm(1)](podman-secret-rm.1.md) | Remove one or more secrets | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-start.1.md b/docs/source/markdown/podman-start.1.md index a751a098b..b8ed181e0 100644 --- a/docs/source/markdown/podman-start.1.md +++ b/docs/source/markdown/podman-start.1.md @@ -78,7 +78,7 @@ podman start --interactive --attach 860a4b231279 podman start -i -l ## SEE ALSO -podman(1), podman-create(1) +**[podman(1)](podman.1.md)** ## HISTORY November 2018, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-stats.1.md b/docs/source/markdown/podman-stats.1.md index abd8fd530..bbb4bcb06 100644 --- a/docs/source/markdown/podman-stats.1.md +++ b/docs/source/markdown/podman-stats.1.md @@ -99,7 +99,7 @@ ID NAME MEM USAGE / LIMIT ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY July 2017, Originally compiled by Ryan Cole <rycole@redhat.com> diff --git a/docs/source/markdown/podman-stop.1.md b/docs/source/markdown/podman-stop.1.md index 2a3f60844..9a852cbae 100644 --- a/docs/source/markdown/podman-stop.1.md +++ b/docs/source/markdown/podman-stop.1.md @@ -59,7 +59,7 @@ $ podman stop -a $ podman stop --latest ## SEE ALSO -podman(1), podman-rm(1) +**[podman(1)](podman.1.md)**, **[podman-rm(1)](podman-rm.1.md)** ## HISTORY September 2018, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-system-connection-add.1.md b/docs/source/markdown/podman-system-connection-add.1.md index fa763d7cc..76f222f2b 100644 --- a/docs/source/markdown/podman-system-connection-add.1.md +++ b/docs/source/markdown/podman-system-connection-add.1.md @@ -46,7 +46,8 @@ $ podman system connection add testing unix:///run/podman/podman.sock $ podman system connection add debug tcp://localhost:8080 ``` ## SEE ALSO -podman-system(1) , podman-system-connection(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** + ## HISTORY June 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-connection-default.1.md b/docs/source/markdown/podman-system-connection-default.1.md index f324f8c01..49f165e81 100644 --- a/docs/source/markdown/podman-system-connection-default.1.md +++ b/docs/source/markdown/podman-system-connection-default.1.md @@ -14,7 +14,7 @@ Set named ssh destination as default destination for the Podman service. $ podman system connection default production ``` ## SEE ALSO -podman-system(1) , podman-system-connection(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** ## HISTORY July 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-connection-list.1.md b/docs/source/markdown/podman-system-connection-list.1.md index 4dc85dd98..cc457860a 100644 --- a/docs/source/markdown/podman-system-connection-list.1.md +++ b/docs/source/markdown/podman-system-connection-list.1.md @@ -33,7 +33,7 @@ devl ssh://root@example.com:/run/podman/podman.sock ~/.ssh/id_rsa True devl ssh://user@example.com:/run/user/1000/podman/podman.sock ~/.ssh/id_rsa False ``` ## SEE ALSO -podman-system(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** ## HISTORY July 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-connection-remove.1.md b/docs/source/markdown/podman-system-connection-remove.1.md index faa767176..4914ac9e9 100644 --- a/docs/source/markdown/podman-system-connection-remove.1.md +++ b/docs/source/markdown/podman-system-connection-remove.1.md @@ -4,17 +4,23 @@ podman\-system\-connection\-remove - Delete named destination ## SYNOPSIS -**podman system connection remove** *name* +**podman system connection remove** [*options*] *name* ## DESCRIPTION Delete named ssh destination. +## OPTIONS + +#### **--all**=*false*, **-a** + +Remove all connections. + ## EXAMPLE ``` $ podman system connection remove production ``` ## SEE ALSO -podman-system(1) , podman-system-connection(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** ## HISTORY July 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-connection-rename.1.md b/docs/source/markdown/podman-system-connection-rename.1.md index 819cb697f..5b5930738 100644 --- a/docs/source/markdown/podman-system-connection-rename.1.md +++ b/docs/source/markdown/podman-system-connection-rename.1.md @@ -14,7 +14,7 @@ Rename ssh destination from *old* to *new*. $ podman system connection rename laptop devel ``` ## SEE ALSO -podman-system(1) , podman-system-connection(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[podman-system-connection(1)](podman-system-connection.1.md)** ## HISTORY July 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-connection.1.md b/docs/source/markdown/podman-system-connection.1.md index b00a2aec3..df9d8d248 100644 --- a/docs/source/markdown/podman-system-connection.1.md +++ b/docs/source/markdown/podman-system-connection.1.md @@ -28,7 +28,7 @@ Name URI Identity Default devl ssh://root@example.com/run/podman/podman.sock ~/.ssh/id_rsa true ``` ## SEE ALSO -podman-system(1) , containers.conf(5) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)** ## HISTORY June 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system-df.1.md b/docs/source/markdown/podman-system-df.1.md index f3db2d020..eb2c79b96 100644 --- a/docs/source/markdown/podman-system-df.1.md +++ b/docs/source/markdown/podman-system-df.1.md @@ -51,7 +51,7 @@ Containers 5 Local Volumes 1 ``` ## SEE ALSO -podman-system(1) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)** ## HISTORY March 2019, Originally compiled by Qi Wang (qiwan at redhat dot com) diff --git a/docs/source/markdown/podman-system-migrate.1.md b/docs/source/markdown/podman-system-migrate.1.md index 913f119b0..dbdd24132 100644 --- a/docs/source/markdown/podman-system-migrate.1.md +++ b/docs/source/markdown/podman-system-migrate.1.md @@ -33,7 +33,7 @@ This can be used after a system upgrade which changes the default OCI runtime to There are no guarantees that the containers will continue to work under the new runtime, as some runtimes support differing options and configurations. ## SEE ALSO -`podman(1)`, `containers.conf(5)`, `usermod(8)` +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **usermod(8)** ## HISTORY April 2019, Originally compiled by Giuseppe Scrivano (gscrivan at redhat dot com) diff --git a/docs/source/markdown/podman-system-prune.1.md b/docs/source/markdown/podman-system-prune.1.md index 791503a2c..d8b218db2 100644 --- a/docs/source/markdown/podman-system-prune.1.md +++ b/docs/source/markdown/podman-system-prune.1.md @@ -46,7 +46,7 @@ Print usage statement Prune volumes currently unused by any container ## SEE ALSO -podman(1), podman-image-prune(1), podman-container-prune(1), podman-pod-prune(1), podman-volume-prune(1) +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)** ## HISTORY February 2019, Originally compiled by Dan Walsh (dwalsh at redhat dot com) diff --git a/docs/source/markdown/podman-system-renumber.1.md b/docs/source/markdown/podman-system-renumber.1.md index 51c085606..61e26242b 100644 --- a/docs/source/markdown/podman-system-renumber.1.md +++ b/docs/source/markdown/podman-system-renumber.1.md @@ -20,7 +20,7 @@ When all available locks are exhausted, no further containers and pods can be cr If possible, avoid calling **podman system renumber** while there are other Podman processes running. ## SEE ALSO -`podman(1)`, `containers.conf(5)` +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)** ## HISTORY February 2019, Originally compiled by Matt Heon (mheon at redhat dot com) diff --git a/docs/source/markdown/podman-system-reset.1.md b/docs/source/markdown/podman-system-reset.1.md index 744f917df..90bcb5f53 100644 --- a/docs/source/markdown/podman-system-reset.1.md +++ b/docs/source/markdown/podman-system-reset.1.md @@ -43,7 +43,7 @@ if the program does not exist. Users can run `podman info` to ensure Podman is using fuse-overlayfs and the overlay driver. ## SEE ALSO -`podman(1)`, `podman-system(1)`, `fuse-overlayfs(1)`, `containers-storage.conf(5)` +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)**, **[fuse-overlayfs(1)](https://github.com/containers/fuse-overlayfs/blob/main/fuse-overlayfs.1.md)**, **[containers-storage.conf(5)](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md)** ## HISTORY November 2019, Originally compiled by Dan Walsh (dwalsh at redhat dot com) diff --git a/docs/source/markdown/podman-system.1.md b/docs/source/markdown/podman-system.1.md index 7b9081b84..ae18aca88 100644 --- a/docs/source/markdown/podman-system.1.md +++ b/docs/source/markdown/podman-system.1.md @@ -23,4 +23,4 @@ The system command allows you to manage the podman systems | service | [podman-system-service(1)](podman-system-service.1.md) | Run an API service | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** diff --git a/docs/source/markdown/podman-tag.1.md b/docs/source/markdown/podman-tag.1.md index 7eab62028..23dd3b60b 100644 --- a/docs/source/markdown/podman-tag.1.md +++ b/docs/source/markdown/podman-tag.1.md @@ -30,7 +30,7 @@ $ podman tag httpd myregistryhost:5000/fedora/httpd:v2 ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY December 2019, Update description to refer to 'name' instead of 'alias' by Sascha Grunert <sgrunert@suse.com> diff --git a/docs/source/markdown/podman-top.1.md b/docs/source/markdown/podman-top.1.md index 09e8f7423..d385cde28 100644 --- a/docs/source/markdown/podman-top.1.md +++ b/docs/source/markdown/podman-top.1.md @@ -102,7 +102,7 @@ root 1 0 0.000 1h2m12.497061672s ? 0s sleep 100000 ``` ## SEE ALSO -podman(1), ps(1), seccomp(2), proc(5), capabilities(7) +**[podman(1)](podman.1.md)**, **ps(1)**, **seccomp(2)**, **proc(5)**, **capabilities(7)** ## HISTORY July 2018, Introduce format descriptors by Valentin Rothberg <vrothberg@suse.com> diff --git a/docs/source/markdown/podman-unmount.1.md b/docs/source/markdown/podman-unmount.1.md index 2832e5fdb..045ea5456 100644 --- a/docs/source/markdown/podman-unmount.1.md +++ b/docs/source/markdown/podman-unmount.1.md @@ -54,4 +54,4 @@ podman unmount containerID1 containerID2 containerID3 podman unmount --all ## SEE ALSO -podman(1), podman-container-mount(1), podman-image-mount(1) +**[podman(1)](podman.1.md)**, **[podman-container-mount(1)](podman-container-mount.1.md)**, **[podman-image-mount(1)](podman-image-mount.1.md)** diff --git a/docs/source/markdown/podman-unpause.1.md b/docs/source/markdown/podman-unpause.1.md index ae178647a..0eb1f1bb8 100644 --- a/docs/source/markdown/podman-unpause.1.md +++ b/docs/source/markdown/podman-unpause.1.md @@ -36,7 +36,7 @@ podman unpause -a ``` ## SEE ALSO -podman(1), podman-pause(1) +**[podman(1)](podman.1.md)**, **[podman-pause(1)](podman-pause.1.md)** ## HISTORY September 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com> diff --git a/docs/source/markdown/podman-unshare.1.md b/docs/source/markdown/podman-unshare.1.md index fa5259ae1..5676b1be7 100644 --- a/docs/source/markdown/podman-unshare.1.md +++ b/docs/source/markdown/podman-unshare.1.md @@ -105,4 +105,4 @@ $ podman unshare --rootless-netns ip addr ## SEE ALSO -podman(1), podman-mount(1), namespaces(7), newuidmap(1), newgidmap(1), user\_namespaces(7) +**[podman(1)](podman.1.md)**, **[podman-mount(1)](podman-mount.1.md)**, **namespaces(7)**, **newuidmap(1)**, **newgidmap(1)**, **user\_namespaces(7)** diff --git a/docs/source/markdown/podman-untag.1.md b/docs/source/markdown/podman-untag.1.md index 70053c940..8314e5f9b 100644 --- a/docs/source/markdown/podman-untag.1.md +++ b/docs/source/markdown/podman-untag.1.md @@ -29,7 +29,7 @@ $ podman untag httpd myregistryhost:5000/fedora/httpd:v2 ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY December 2019, Originally compiled by Sascha Grunert <sgrunert@suse.com> diff --git a/docs/source/markdown/podman-version.1.md b/docs/source/markdown/podman-version.1.md index a54e66372..809ce64a6 100644 --- a/docs/source/markdown/podman-version.1.md +++ b/docs/source/markdown/podman-version.1.md @@ -40,7 +40,7 @@ $ podman version --format '{{.Client.Version}}' ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY November 2018, Added --format flag by Tomas Tomecek <ttomecek@redhat.com> diff --git a/docs/source/markdown/podman-volume-create.1.md b/docs/source/markdown/podman-volume-create.1.md index 9bf5a3d81..365a5acac 100644 --- a/docs/source/markdown/podman-volume-create.1.md +++ b/docs/source/markdown/podman-volume-create.1.md @@ -17,7 +17,7 @@ driver options can be set using the **--opt** flag. #### **--driver**=*driver* -Specify the volume driver name (default **local**). Setting this to a value other than **local** Podman attempts to create the volume using a volume plugin with the given name. Such plugins must be defined in the **volume_plugins** section of the **containers.conf**(5) configuration file. +Specify the volume driver name (default **local**). Setting this to a value other than **local** Podman attempts to create the volume using a volume plugin with the given name. Such plugins must be defined in the **volume_plugins** section of the **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)** configuration file. #### **--help** @@ -86,7 +86,7 @@ All volume assigned project ids larger project ids starting with 200000. This prevents xfs_quota management conflicts with containers/storage. ## SEE ALSO -**podman-volume**(1), **mount**(8), **containers.conf**(5), **xfs_quota**(8), `xfs_quota(8)`, `projects(5)`, `projid(5)` +**[podman(1)](podman.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[podman-volume(1)](podman-volume.1.md)**, **mount(8)**, **xfs_quota(8)**, **xfs_quota(8)**, **projects(5)**, **projid(5)** ## HISTORY January 2020, updated with information on volume plugins by Matthew Heon <mheon@redhat.com> diff --git a/docs/source/markdown/podman-volume-exists.1.md b/docs/source/markdown/podman-volume-exists.1.md index ead806bd9..dbdf0985c 100644 --- a/docs/source/markdown/podman-volume-exists.1.md +++ b/docs/source/markdown/podman-volume-exists.1.md @@ -37,7 +37,7 @@ $ ``` ## SEE ALSO -podman(1), podman-volume-create(1), podman-volume-rm(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** ## HISTORY January 2021, Originally compiled by Paul Holzinger `<paul.holzinger@web.de>` diff --git a/docs/source/markdown/podman-volume-export.1.md b/docs/source/markdown/podman-volume-export.1.md index 7db1e421d..ed73e86d0 100644 --- a/docs/source/markdown/podman-volume-export.1.md +++ b/docs/source/markdown/podman-volume-export.1.md @@ -35,4 +35,4 @@ $ podman volume export myvol --output myvol.tar ``` ## SEE ALSO -podman-volume(1), podman-volume-import(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-import(1)](podman-volume-import.1.md)** diff --git a/docs/source/markdown/podman-volume-import.1.md b/docs/source/markdown/podman-volume-import.1.md index 88b7b1b5b..71956f43a 100644 --- a/docs/source/markdown/podman-volume-import.1.md +++ b/docs/source/markdown/podman-volume-import.1.md @@ -32,4 +32,4 @@ $ podman volume export myvol | podman volume import oldmyvol - ``` ## SEE ALSO -podman-volume(1), podman-volume-export(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-export(1)](podman-volume-export.1.md)** diff --git a/docs/source/markdown/podman-volume-inspect.1.md b/docs/source/markdown/podman-volume-inspect.1.md index 4595ccda6..dda83e558 100644 --- a/docs/source/markdown/podman-volume-inspect.1.md +++ b/docs/source/markdown/podman-volume-inspect.1.md @@ -40,7 +40,7 @@ $ podman volume inspect --format "{{.Driver}} {{.Scope}}" myvol ``` ## SEE ALSO -**[podman(1)](podman.1.md)**,**[podman-volume(1)](podman-volume.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-inspect(1)](podman-inspect.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-volume-ls.1.md b/docs/source/markdown/podman-volume-ls.1.md index 6c80ec152..4f5ce6888 100644 --- a/docs/source/markdown/podman-volume-ls.1.md +++ b/docs/source/markdown/podman-volume-ls.1.md @@ -59,7 +59,7 @@ $ podman volume ls --filter label=key=value ``` ## SEE ALSO -podman-volume(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-volume-prune.1.md b/docs/source/markdown/podman-volume-prune.1.md index cfdfc1a44..012567957 100644 --- a/docs/source/markdown/podman-volume-prune.1.md +++ b/docs/source/markdown/podman-volume-prune.1.md @@ -44,7 +44,7 @@ $ podman volume prune --filter label=mylabel=mylabelvalue ``` ## SEE ALSO -podman-volume(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-volume-rm.1.md b/docs/source/markdown/podman-volume-rm.1.md index 26cc605a2..8274eaaf3 100644 --- a/docs/source/markdown/podman-volume-rm.1.md +++ b/docs/source/markdown/podman-volume-rm.1.md @@ -52,7 +52,7 @@ $ podman volume rm --force myvol **125** The command fails for any other reason ## SEE ALSO -podman-volume(1) +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-volume.1.md b/docs/source/markdown/podman-volume.1.md index 64b37c28c..d8bc11aad 100644 --- a/docs/source/markdown/podman-volume.1.md +++ b/docs/source/markdown/podman-volume.1.md @@ -23,7 +23,7 @@ podman volume is a set of subcommands that manage volumes. | rm | [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. | ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/source/markdown/podman-wait.1.md b/docs/source/markdown/podman-wait.1.md index fac017fe6..7ee53c57e 100644 --- a/docs/source/markdown/podman-wait.1.md +++ b/docs/source/markdown/podman-wait.1.md @@ -53,7 +53,7 @@ $ podman wait mywebserver myftpserver ``` ## SEE ALSO -podman(1) +**[podman(1)](podman.1.md)** ## HISTORY September 2017, Originally compiled by Brent Baude<bbaude@redhat.com> diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 573ff20e7..daa8212c5 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -400,7 +400,7 @@ The Network File System (NFS) and other distributed file systems (for example: L For more information, please refer to the [Podman Troubleshooting Page](https://github.com/containers/podman/blob/master/troubleshooting.md). ## SEE ALSO -**containers-mounts.conf**(5), **containers-registries.conf**(5), **containers-storage.conf**(5), **buildah**(1), **containers.conf**(5), **oci-hooks**(5), **containers-policy.json**(5), **crun**(8), **runc**(8), **subuid**(5), **subgid**(5), **slirp4netns**(1), **conmon**(8). +**[containers-mounts.conf(5)](https://github.com/containers/common/blob/main/docs/containers-mounts.conf.5.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)**, **[containers-storage.conf(5)](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md)**, **[buildah(1)](https://github.com/containers/buildah/blob/main/docs/buildah.1.md)**, **oci-hooks(5)**, **[containers-policy.json(5)](https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md)**, **[crun(1)](https://github.com/containers/crun/blob/main/crun.1.md)**, **[runc(8)](https://github.com/opencontainers/runc/blob/master/man/runc.8.md)**, **[subuid(5)](https://www.unix.com/man-page/linux/5/subuid)**, **[subgid(5)](https://www.unix.com/man-page/linux/5/subgid)**, **[slirp4netns(1)](https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md)**, **[conmon(8)](https://github.com/containers/conmon/blob/main/docs/conmon.8.md)** ## HISTORY Dec 2016, Originally compiled by Dan Walsh <dwalsh@redhat.com> @@ -12,7 +12,7 @@ require ( github.com/containernetworking/cni v1.0.1 github.com/containernetworking/plugins v1.0.1 github.com/containers/buildah v1.23.1 - github.com/containers/common v0.46.1-0.20211026130826-7abfd453c86f + github.com/containers/common v0.46.1-0.20211110143743-73e7b462c358 github.com/containers/conmon v2.0.20+incompatible github.com/containers/image/v5 v5.16.1 github.com/containers/ocicrypt v1.1.2 @@ -45,7 +45,7 @@ require ( github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/mrunalp/fileutils v0.5.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.16.0 + github.com/onsi/gomega v1.17.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.2-0.20210819154149-5ad6f50d6283 github.com/opencontainers/runc v1.0.2 @@ -67,7 +67,10 @@ require ( golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 + gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.22.3 k8s.io/apimachinery v0.22.3 ) + +replace github.com/onsi/gomega => github.com/onsi/gomega v1.16.0 @@ -258,8 +258,8 @@ github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNB github.com/containers/buildah v1.23.1 h1:Tpc9DsRuU+0Oofewpxb6OJVNQjCu7yloN/obUqzfDTY= github.com/containers/buildah v1.23.1/go.mod h1:4WnrN0yrA7ab0ppgunixu2WM1rlD2rG8QLJAKbEkZlQ= github.com/containers/common v0.44.2/go.mod h1:7sdP4vmI5Bm6FPFxb3lvAh1Iktb6tiO1MzjUzhxdoGo= -github.com/containers/common v0.46.1-0.20211026130826-7abfd453c86f h1:jFFIV8QvsPgwkJHh3tjfREFRwSeMq5M8lt8vklkZaOk= -github.com/containers/common v0.46.1-0.20211026130826-7abfd453c86f/go.mod h1:pVvmLTLCOZE300e4rex/QDmpnRmEM/5aZ/YfCkkjgZo= +github.com/containers/common v0.46.1-0.20211110143743-73e7b462c358 h1:dK2AgGBdWspdQNw28Wc4peY25QeyYV4H9ViQaFaQ9XQ= +github.com/containers/common v0.46.1-0.20211110143743-73e7b462c358/go.mod h1:bu8gizEkgAz6gXHvUw2cMtI5ErxB+fn/hv49RWk5N1A= 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.16.0/go.mod h1:XgTpfAPLRGOd1XYyCU5cISFr777bLmOerCSpt/v7+Q4= @@ -735,7 +735,6 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -747,15 +746,6 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1099,12 +1089,10 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= diff --git a/hack/install_catatonit.sh b/hack/install_catatonit.sh index 8837db3a8..0a02b75ab 100755 --- a/hack/install_catatonit.sh +++ b/hack/install_catatonit.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash BASE_PATH="/usr/libexec/podman" CATATONIT_PATH="${BASE_PATH}/catatonit" -CATATONIT_VERSION="v0.1.4" +CATATONIT_VERSION="v0.1.7" set -e if [ -f $CATATONIT_PATH ]; then diff --git a/hack/xref-helpmsgs-manpages b/hack/xref-helpmsgs-manpages index af54f05f3..6a2d627bb 100755 --- a/hack/xref-helpmsgs-manpages +++ b/hack/xref-helpmsgs-manpages @@ -54,6 +54,11 @@ option is listed in the appropriate man page and vice-versa. $ME invokes '\$PODMAN' (default: $Default_Podman). +In the spirit of shoehorning functionality where it wasn't intended, +$ME also checks the SEE ALSO section of each man page +to ensure that references and links are properly formatted +and valid. + Exit status is zero if no inconsistencies found, one otherwise OPTIONS: @@ -286,6 +291,9 @@ sub podman_man { elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) { $section = 'commands'; } + elsif ($line =~ /^\#\#\s+SEE\s+ALSO/) { + $section = 'see-also'; + } elsif ($line =~ /^\#\#[^#]/) { $section = ''; } @@ -340,6 +348,11 @@ sub podman_man { } } } + + # It's easy to make mistakes in the SEE ALSO elements. + elsif ($section eq 'see-also') { + _check_seealso_links( "$subpath:$.", $line ); + } } close $fh; @@ -427,5 +440,119 @@ sub podman_rst { # END data gathering ############################################################################### +# BEGIN sanity checking of SEE ALSO links + +########################## +# _check_seealso_links # Check formatting and link validity. +########################## +sub _check_seealso_links { + my $path = shift; + my $line = shift; + + return if ! $line; + + # Line must be a comma-separated list of man page references, e.g. + # **foo(1)**, **[podman-bar(1)](podman-bar.1.md)**, **[xxx(8)](http...)** + TOKEN: + for my $token (split /,\s+/, $line) { + # Elements must be separated by comma and space. (We don't do further + # checks here, so it's possible for the dev to add the space and then + # have us fail on the next iteration. I choose not to address that.) + if ($token =~ /,/) { + warn "$ME: $path: please add space after comma: '$token'\n"; + ++$Errs; + next TOKEN; + } + + # Each token must be of the form '**something**' + if ($token !~ s/^\*\*(.*)\*\*$/$1/) { + if ($token =~ /\*\*/) { + warn "$ME: $path: '$token' has asterisks in the wrong place\n"; + } + else { + warn "$ME: $path: '$token' should be bracketed by '**'\n"; + } + ++$Errs; + next TOKEN; + } + + # Is it a markdown link? + if ($token =~ /^\[(\S+)\]\((\S+)\)$/) { + my ($name, $link) = ($1, $2); + if ($name =~ /^(.*)\((\d)\)$/) { + my ($base, $section) = ($1, $2); + if (-e "$Markdown_Path/$base.$section.md" || -e "$Markdown_Path/links/$base.$section") { + if ($link ne "$base.$section.md") { + warn "$ME: $path: inconsistent link $name -> $link, expected $base.$section.md\n"; + ++$Errs; + } + } + else { + if (! _is_valid_external_link($base, $section, $link)) { + warn "$ME: $path: invalid link $name -> $link\n"; + ++$Errs; + } + } + } + else { + warn "$ME: $path: could not parse '$name' as 'manpage(N)'\n"; + ++$Errs; + } + } + + # Not a markdown link; it must be a plain man reference, e.g. 'foo(5)' + elsif ($token =~ m!^(\S+)\((\d+)\)$!) { + my ($base, $section) = ($1, $2); + + # Unadorned 'podman-foo(1)' must be a link. + if (-e "$Markdown_Path/$base.$section.md" || -e "$Markdown_Path/links/$base.$section") { + warn "$ME: $path: '$token' should be '[$token]($base.$section.md)'\n"; + ++$Errs; + } + + # Link to man page foo(5) but without a link. This is not an error + # but Ed may sometimes want to see those on a manual test run. + warn "$ME: $path: plain '$token' would be so much nicer as a link\n" + if $verbose; + } + else { + warn "$ME: $path: invalid token '$token'\n"; + ++$Errs; + } + } +} + +############################# +# _is_valid_external_link # Tries to validate links to external man pages +############################# +# +# This performs no actual fetches, so we can't actually check for 404. +# All we do is ensure that links conform to standard patterns. This is +# good for catching things like 'conmon(8)' pointing to a .5 URL, or +# linking to .md instead of .html. +# +# FIXME: we could actually rewrite this so as to offer hints on what to fix. +# That's a lot of work, and a lot of convoluted code, for questionable ROI. +# +sub _is_valid_external_link { + my ($base, $section, $link) = @_; + + return 1 if $link =~ m!^https://github\.com/\S+/blob/(main|master)(/.*)?/\Q$base\E\.$section\.md!; + + return 1 if $link =~ m!^https://.*unix\.com/man-page/(linux|redhat)/$section/$base$!; + return 1 if $link eq "https://man7\.org/linux/man-pages/man$section/$base\.$section\.html"; + + if ($base =~ /systemd/) { + return 1 if $link eq "https://www.freedesktop.org/software/systemd/man/$base.html"; + } + + return; +} + + + + +# END sanity checking of SEE ALSO links +############################################################################### 1; diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index 3e3c17a9e..e71d82736 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -384,6 +384,15 @@ func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig, return errors.Wrapf(err, "error unmarshalling container %s config", string(id)) } + // convert ports to the new format if needed + if len(config.ContainerNetworkConfig.OldPortMappings) > 0 && len(config.ContainerNetworkConfig.PortMappings) == 0 { + config.ContainerNetworkConfig.PortMappings = ocicniPortsToNetTypesPorts(config.ContainerNetworkConfig.OldPortMappings) + // keep the OldPortMappings in case an user has to downgrade podman + + // indicate the the config was modified and should be written back to the db when possible + config.rewrite = true + } + return nil } diff --git a/libpod/common_test.go b/libpod/common_test.go index 4662a33bd..67e29c265 100644 --- a/libpod/common_test.go +++ b/libpod/common_test.go @@ -41,18 +41,20 @@ func getTestContainer(id, name string, manager lock.Manager) (*Container, error) ContainerNetworkConfig: ContainerNetworkConfig{ DNSServer: []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.2.2")}, DNSSearch: []string{"example.com", "example.example.com"}, - PortMappings: []types.OCICNIPortMapping{ + PortMappings: []types.PortMapping{ { HostPort: 80, ContainerPort: 90, Protocol: "tcp", HostIP: "192.168.3.3", + Range: 1, }, { HostPort: 100, ContainerPort: 110, Protocol: "udp", HostIP: "192.168.4.4", + Range: 1, }, }, }, diff --git a/libpod/container.go b/libpod/container.go index 4d15c04c5..86989a02f 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -465,7 +465,7 @@ func (c *Container) NewNetNS() bool { // PortMappings returns the ports that will be mapped into a container if // a new network namespace is created // If NewNetNS() is false, this value is unused -func (c *Container) PortMappings() ([]types.OCICNIPortMapping, error) { +func (c *Container) PortMappings() ([]types.PortMapping, error) { // First check if the container belongs to a network namespace (like a pod) if len(c.config.NetNsCtr) > 0 { netNsCtr, err := c.runtime.GetContainer(c.config.NetNsCtr) diff --git a/libpod/container_config.go b/libpod/container_config.go index 8f803d744..412be835f 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -78,6 +78,11 @@ type ContainerConfig struct { // These containers must be started before this container is started. Dependencies []string + // rewrite is an internal bool to indicate that the config was modified after + // a read from the db, e.g. to migrate config fields after an upgrade. + // This field should never be written to the db, the json tag ensures this. + rewrite bool `json:"-"` + // embedded sub-configs ContainerRootFSConfig ContainerSecurityConfig @@ -232,7 +237,12 @@ type ContainerNetworkConfig struct { // PortMappings are the ports forwarded to the container's network // namespace // These are not used unless CreateNetNS is true - PortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"` + PortMappings []types.PortMapping `json:"newPortMappings,omitempty"` + // OldPortMappings are the ports forwarded to the container's network + // namespace. As of podman 4.0 this field is deprecated, use PortMappings + // instead. The db will convert the old ports to the new structure for you. + // These are not used unless CreateNetNS is true + OldPortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"` // ExposedPorts are the ports which are exposed but not forwarded // into the container. // The map key is the port and the string slice contains the protocols, diff --git a/libpod/container_internal.go b/libpod/container_internal.go index b9805faa3..de23a4aeb 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -39,6 +39,7 @@ import ( "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) const ( @@ -290,7 +291,7 @@ func (c *Container) handleRestartPolicy(ctx context.Context) (_ bool, retErr err // setup slirp4netns again because slirp4netns will die when conmon exits if c.config.NetMode.IsSlirp4netns() { - err := c.runtime.setupSlirp4netns(c) + err := c.runtime.setupSlirp4netns(c, c.state.NetNS) if err != nil { return false, err } @@ -299,7 +300,7 @@ func (c *Container) handleRestartPolicy(ctx context.Context) (_ bool, retErr err // setup rootlesskit port forwarder again since it dies when conmon exits // we use rootlesskit port forwarder only as rootless and when bridge network is used if rootless.IsRootless() && c.config.NetMode.IsBridge() && len(c.config.PortMappings) > 0 { - err := c.runtime.setupRootlessPortMappingViaRLK(c, c.state.NetNS.Path()) + err := c.runtime.setupRootlessPortMappingViaRLK(c, c.state.NetNS.Path(), c.state.NetworkStatus) if err != nil { return false, err } @@ -496,9 +497,27 @@ func (c *Container) setupStorage(ctx context.Context) error { c.setupStorageMapping(&options.IDMappingOptions, &c.config.IDMappings) - containerInfo, err := c.runtime.storageService.CreateContainerStorage(ctx, c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, options) - if err != nil { - return errors.Wrapf(err, "error creating container storage") + // Unless the user has specified a name, use a randomly generated one. + // Note that name conflicts may occur (see #11735), so we need to loop. + generateName := c.config.Name == "" + var containerInfo ContainerInfo + var containerInfoErr error + for { + if generateName { + name, err := c.runtime.generateName() + if err != nil { + return err + } + c.config.Name = name + } + containerInfo, containerInfoErr = c.runtime.storageService.CreateContainerStorage(ctx, c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, options) + + if !generateName || errors.Cause(containerInfoErr) != storage.ErrDuplicateName { + break + } + } + if containerInfoErr != nil { + return errors.Wrapf(containerInfoErr, "error creating container storage") } // only reconfig IDMappings if layer was mounted from storage @@ -667,6 +686,19 @@ func (c *Container) refresh() error { c.state.NetworkStatus = nil c.state.NetworkStatusOld = nil + // Rewrite the config if necessary. + // Podman 4.0 uses a new port format in the config. + // getContainerConfigFromDB() already converted the old ports to the new one + // but it did not write the config to the db back for performance reasons. + // If a rewrite must happen the config.rewrite field is set to true. + if c.config.rewrite { + // SafeRewriteContainerConfig must be used with care. Make sure to not change config fields by accident. + if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil { + return errors.Wrapf(err, "failed to rewrite the config for container %s", c.config.ID) + } + c.config.rewrite = false + } + if err := c.save(); err != nil { return errors.Wrapf(err, "error refreshing state for container %s", c.ID()) } @@ -968,9 +1000,6 @@ func (c *Container) completeNetworkSetup() error { if err := c.syncContainer(); err != nil { return err } - if c.config.NetMode.IsSlirp4netns() { - return c.runtime.setupSlirp4netns(c) - } if err := c.runtime.setupNetNS(c); err != nil { return err } @@ -1564,14 +1593,49 @@ func (c *Container) mountStorage() (_ string, deferredErr error) { }() } + rootUID, rootGID := c.RootUID(), c.RootGID() + + dirfd, err := unix.Open(mountPoint, unix.O_RDONLY|unix.O_PATH, 0) + if err != nil { + return "", errors.Wrap(err, "open mount point") + } + defer unix.Close(dirfd) + + err = unix.Mkdirat(dirfd, "etc", 0755) + if err != nil && !os.IsExist(err) { + return "", errors.Wrap(err, "create /etc") + } + // If the etc directory was created, chown it to root in the container + if err == nil && (rootUID != 0 || rootGID != 0) { + err = unix.Fchownat(dirfd, "etc", rootUID, rootGID, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return "", errors.Wrap(err, "chown /etc") + } + } + + etcInTheContainerPath, err := securejoin.SecureJoin(mountPoint, "etc") + if err != nil { + return "", errors.Wrap(err, "resolve /etc in the container") + } + + etcInTheContainerFd, err := unix.Open(etcInTheContainerPath, unix.O_RDONLY|unix.O_PATH, 0) + if err != nil { + return "", errors.Wrap(err, "open /etc in the container") + } + defer unix.Close(etcInTheContainerFd) + // If /etc/mtab does not exist in container image, then we need to // create it, so that mount command within the container will work. - mtab := filepath.Join(mountPoint, "/etc/mtab") - if err := idtools.MkdirAllAs(filepath.Dir(mtab), 0755, c.RootUID(), c.RootGID()); err != nil { - return "", errors.Wrap(err, "error creating mtab directory") + err = unix.Symlinkat("/proc/mounts", etcInTheContainerFd, "mtab") + if err != nil && !os.IsExist(err) { + return "", errors.Wrap(err, "creating /etc/mtab symlink") } - if err = os.Symlink("/proc/mounts", mtab); err != nil && !os.IsExist(err) { - return "", err + // If the symlink was created, then also chown it to root in the container + if err == nil && (rootUID != 0 || rootGID != 0) { + err = unix.Fchownat(etcInTheContainerFd, "mtab", rootUID, rootGID, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return "", errors.Wrap(err, "chown /etc/mtab") + } } // Request a mount of all named volumes diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 2fd519990..91453574e 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -21,6 +21,7 @@ import ( "time" metadata "github.com/checkpoint-restore/checkpointctl/lib" + "github.com/checkpoint-restore/go-criu/v5/stats" cdi "github.com/container-orchestrated-devices/container-device-interface/pkg" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/buildah/pkg/chrootuser" @@ -107,15 +108,6 @@ func (c *Container) prepare() error { c.state.NetNS = netNS c.state.NetworkStatus = networkStatus } - - // handle rootless network namespace setup - if noNetNS && !c.config.PostConfigureNetNS { - if rootless.IsRootless() { - createNetNSErr = c.runtime.setupRootlessNetNS(c) - } else if c.config.NetMode.IsSlirp4netns() { - createNetNSErr = c.runtime.setupSlirp4netns(c) - } - } }() // Mount storage if not mounted go func() { @@ -1013,6 +1005,7 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error { metadata.ConfigDumpFile, metadata.SpecDumpFile, metadata.NetworkStatusFile, + stats.StatsDump, } if c.LogDriver() == define.KubernetesLogging || @@ -1197,7 +1190,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO if !options.Keep && !options.PreCheckPoint { cleanup := []string{ "dump.log", - "stats-dump", + stats.StatsDump, metadata.ConfigDumpFile, metadata.SpecDumpFile, } @@ -1564,8 +1557,8 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti cleanup := [...]string{ "restore.log", "dump.log", - "stats-dump", - "stats-restore", + stats.StatsDump, + stats.StatsRestore, metadata.NetworkStatusFile, metadata.RootFsDiffTar, metadata.DeletedFilesFile, diff --git a/libpod/define/info.go b/libpod/define/info.go index 61f2f4c75..15400991f 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -39,6 +39,7 @@ type HostInfo struct { LogDriver string `json:"logDriver"` MemFree int64 `json:"memFree"` MemTotal int64 `json:"memTotal"` + NetworkBackend string `json:"networkBackend"` OCIRuntime *OCIRuntimeInfo `json:"ociRuntime"` OS string `json:"os"` // RemoteSocket returns the UNIX domain socket the Podman service is listening on diff --git a/libpod/info.go b/libpod/info.go index 8d0b19f23..a1db5763a 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -131,6 +131,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { Kernel: kv, MemFree: mi.MemFree, MemTotal: mi.MemTotal, + NetworkBackend: r.config.Network.NetworkBackend, OS: runtime.GOOS, Security: define.SecurityInfo{ AppArmorEnabled: apparmor.IsEnabled(), diff --git a/libpod/kube.go b/libpod/kube.go index ad5acea78..850f2493c 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -75,7 +75,7 @@ func (p *Pod) GenerateForKube(ctx context.Context) (*v1.Pod, []v1.ServicePort, e Hostnames: []string{hostSli[0]}, }) } - ports, err = ocicniPortMappingToContainerPort(infraContainer.config.PortMappings) + ports, err = portMappingToContainerPort(infraContainer.config.PortMappings) if err != nil { return nil, servicePorts, err } @@ -167,9 +167,83 @@ func (v *Volume) GenerateForKube() *v1.PersistentVolumeClaim { } } +// YAMLPodSpec represents the same k8s API core PodSpec struct with a small +// change and that is having Containers as a pointer to YAMLContainer. +// Because Go doesn't omit empty struct and we want to omit Status in YAML +// if it's empty. Fixes: GH-11998 +type YAMLPodSpec struct { + v1.PodSpec + Containers []*YAMLContainer `json:"containers"` +} + +// YAMLPod represents the same k8s API core Pod struct with a small +// change and that is having Spec as a pointer to YAMLPodSpec and +// Status as a pointer to k8s API core PodStatus. +// Because Go doesn't omit empty struct and we want to omit Status in YAML +// if it's empty. Fixes: GH-11998 +type YAMLPod struct { + v1.Pod + Spec *YAMLPodSpec `json:"spec,omitempty"` + Status *v1.PodStatus `json:"status,omitempty"` +} + +// YAMLService represents the same k8s API core Service struct with a small +// change and that is having Status as a pointer to k8s API core ServiceStatus. +// Because Go doesn't omit empty struct and we want to omit Status in YAML +// if it's empty. Fixes: GH-11998 +type YAMLService struct { + v1.Service + Status *v1.ServiceStatus `json:"status,omitempty"` +} + +// YAMLContainer represents the same k8s API core Container struct with a small +// change and that is having Resources as a pointer to k8s API core ResourceRequirements. +// Because Go doesn't omit empty struct and we want to omit Status in YAML +// if it's empty. Fixes: GH-11998 +type YAMLContainer struct { + v1.Container + Resources *v1.ResourceRequirements `json:"resources,omitempty"` +} + +// ConvertV1PodToYAMLPod takes k8s API core Pod and returns a pointer to YAMLPod +func ConvertV1PodToYAMLPod(pod *v1.Pod) *YAMLPod { + cs := []*YAMLContainer{} + for _, cc := range pod.Spec.Containers { + var res *v1.ResourceRequirements = nil + if len(cc.Resources.Limits) > 0 || len(cc.Resources.Requests) > 0 { + res = &cc.Resources + } + cs = append(cs, &YAMLContainer{Container: cc, Resources: res}) + } + mpo := &YAMLPod{Pod: *pod} + mpo.Spec = &YAMLPodSpec{PodSpec: (*pod).Spec, Containers: cs} + for _, ctr := range pod.Spec.Containers { + if ctr.SecurityContext == nil || ctr.SecurityContext.SELinuxOptions == nil { + continue + } + selinuxOpts := ctr.SecurityContext.SELinuxOptions + if selinuxOpts.User == "" && selinuxOpts.Role == "" && selinuxOpts.Type == "" && selinuxOpts.Level == "" { + ctr.SecurityContext.SELinuxOptions = nil + } + } + dnsCfg := pod.Spec.DNSConfig + if dnsCfg != nil && (len(dnsCfg.Nameservers)+len(dnsCfg.Searches)+len(dnsCfg.Options) > 0) { + mpo.Spec.DNSConfig = dnsCfg + } + status := pod.Status + if status.Phase != "" || len(status.Conditions) > 0 || + status.Message != "" || status.Reason != "" || + status.NominatedNodeName != "" || status.HostIP != "" || + status.PodIP != "" || status.StartTime != nil || + len(status.InitContainerStatuses) > 0 || len(status.ContainerStatuses) > 0 || status.QOSClass != "" || len(status.EphemeralContainerStatuses) > 0 { + mpo.Status = &status + } + return mpo +} + // GenerateKubeServiceFromV1Pod creates a v1 service object from a v1 pod object -func GenerateKubeServiceFromV1Pod(pod *v1.Pod, servicePorts []v1.ServicePort) v1.Service { - service := v1.Service{} +func GenerateKubeServiceFromV1Pod(pod *v1.Pod, servicePorts []v1.ServicePort) YAMLService { + service := YAMLService{} selector := make(map[string]string) selector["app"] = pod.Labels["app"] ports := servicePorts @@ -452,7 +526,7 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, [] if err != nil { return kubeContainer, kubeVolumes, nil, annotations, err } - ports, err := ocicniPortMappingToContainerPort(portmappings) + ports, err := portMappingToContainerPort(portmappings) if err != nil { return kubeContainer, kubeVolumes, nil, annotations, err } @@ -588,29 +662,36 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, [] return kubeContainer, kubeVolumes, &dns, annotations, nil } -// ocicniPortMappingToContainerPort takes an ocicni portmapping and converts +// portMappingToContainerPort takes an portmapping and converts // it to a v1.ContainerPort format for kube output -func ocicniPortMappingToContainerPort(portMappings []types.OCICNIPortMapping) ([]v1.ContainerPort, error) { +func portMappingToContainerPort(portMappings []types.PortMapping) ([]v1.ContainerPort, error) { containerPorts := make([]v1.ContainerPort, 0, len(portMappings)) for _, p := range portMappings { - var protocol v1.Protocol - switch strings.ToUpper(p.Protocol) { - case "TCP": - // do nothing as it is the default protocol in k8s, there is no need to explicitly - // add it to the generated yaml - case "UDP": - protocol = v1.ProtocolUDP - default: - return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol) - } - cp := v1.ContainerPort{ - // Name will not be supported - HostPort: p.HostPort, - HostIP: p.HostIP, - ContainerPort: p.ContainerPort, - Protocol: protocol, - } - containerPorts = append(containerPorts, cp) + protocols := strings.Split(p.Protocol, ",") + for _, proto := range protocols { + var protocol v1.Protocol + switch strings.ToUpper(proto) { + case "TCP": + // do nothing as it is the default protocol in k8s, there is no need to explicitly + // add it to the generated yaml + case "UDP": + protocol = v1.ProtocolUDP + case "SCTP": + protocol = v1.ProtocolSCTP + default: + return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol) + } + for i := uint16(0); i < p.Range; i++ { + cp := v1.ContainerPort{ + // Name will not be supported + HostPort: int32(p.HostPort + i), + HostIP: p.HostIP, + ContainerPort: int32(p.ContainerPort + i), + Protocol: protocol, + } + containerPorts = append(containerPorts, cp) + } + } } return containerPorts, nil } diff --git a/libpod/lock/shm/shm_lock.go b/libpod/lock/shm/shm_lock.go index 322e92a8f..fea02a619 100644 --- a/libpod/lock/shm/shm_lock.go +++ b/libpod/lock/shm/shm_lock.go @@ -130,8 +130,17 @@ func (locks *SHMLocks) AllocateSemaphore() (uint32, error) { // semaphore indexes, and can still return error codes. retCode := C.allocate_semaphore(locks.lockStruct) if retCode < 0 { + var err = syscall.Errno(-1 * retCode) // Negative errno returned - return 0, syscall.Errno(-1 * retCode) + if errors.Is(err, syscall.ENOSPC) { + // ENOSPC expands to "no space left on device". While it is technically true + // that there's no room in the SHM inn for this lock, this tends to send normal people + // down the path of checking disk-space which is not actually their problem. + // Give a clue that it's actually due to num_locks filling up. + var errFull = errors.Errorf("allocation failed; exceeded num_locks (%d)", locks.maxLocks) + return uint32(retCode), errFull + } + return uint32(retCode), syscall.Errno(-1 * retCode) } return uint32(retCode), nil diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go index 01e149114..70d259b60 100644 --- a/libpod/network/cni/cni_conversion.go +++ b/libpod/network/cni/cni_conversion.go @@ -14,6 +14,7 @@ import ( "time" "github.com/containernetworking/cni/libcni" + internalutil "github.com/containers/podman/v3/libpod/network/internal/util" "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/network/util" pkgutil "github.com/containers/podman/v3/pkg/util" @@ -156,10 +157,7 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway) } // convert to 4 byte if ipv4 - ipv4 := gateway.To4() - if ipv4 != nil { - gateway = ipv4 - } + util.NormalizeIP(&gateway) } else if !network.Internal { // only add a gateway address if the network is not internal gateway, err = util.FirstIPInSubnet(sub) @@ -244,13 +242,13 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ for k, v := range network.Options { switch k { case "mtu": - mtu, err = parseMTU(v) + mtu, err = internalutil.ParseMTU(v) if err != nil { return nil, "", err } case "vlan": - vlan, err = parseVlan(v) + vlan, err = internalutil.ParseVlan(v) if err != nil { return nil, "", err } @@ -339,36 +337,6 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ return config, cniPathName, nil } -// parseMTU parses the mtu option -func parseMTU(mtu string) (int, error) { - if mtu == "" { - return 0, nil // default - } - m, err := strconv.Atoi(mtu) - if err != nil { - return 0, err - } - if m < 0 { - return 0, errors.Errorf("mtu %d is less than zero", m) - } - return m, nil -} - -// parseVlan parses the vlan option -func parseVlan(vlan string) (int, error) { - if vlan == "" { - return 0, nil // default - } - v, err := strconv.Atoi(vlan) - if err != nil { - return 0, err - } - if v < 0 || v > 4094 { - return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v) - } - return v, nil -} - func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) { cniPorts := make([]cniPortMapEntry, 0, len(ports)) for _, port := range ports { diff --git a/libpod/network/cni/config.go b/libpod/network/cni/config.go index 3df155637..5d587da23 100644 --- a/libpod/network/cni/config.go +++ b/libpod/network/cni/config.go @@ -7,8 +7,8 @@ import ( "os" "github.com/containers/podman/v3/libpod/define" + internalutil "github.com/containers/podman/v3/libpod/network/internal/util" "github.com/containers/podman/v3/libpod/network/types" - "github.com/containers/podman/v3/libpod/network/util" pkgutil "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -47,32 +47,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") } - if newNetwork.Labels == nil { - newNetwork.Labels = map[string]string{} - } - if newNetwork.Options == nil { - newNetwork.Options = map[string]string{} - } - if newNetwork.IPAMOptions == nil { - newNetwork.IPAMOptions = map[string]string{} - } - - var name string - var err error - // validate the name when given - if newNetwork.Name != "" { - if !define.NameRegex.MatchString(newNetwork.Name) { - return nil, errors.Wrapf(define.RegexError, "network name %s invalid", newNetwork.Name) - } - if _, ok := n.networks[newNetwork.Name]; ok { - return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", newNetwork.Name) - } - } else { - name, err = n.getFreeDeviceName() - if err != nil { - return nil, err - } - newNetwork.Name = name + err := internalutil.CommonNetworkCreate(n, &newNetwork) + if err != nil { + return nil, err } // Only get the used networks for validation if we do not create the default network. @@ -84,7 +61,7 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* // fail because it thinks the network is used on the host. var usedNetworks []*net.IPNet if !defaultNet { - usedNetworks, err = n.getUsedSubnets() + usedNetworks, err = internalutil.GetUsedSubnets(n) if err != nil { return nil, err } @@ -92,11 +69,7 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* switch newNetwork.Driver { case types.BridgeNetworkDriver: - // if the name was created with getFreeDeviceName set the interface to it as well - if name != "" && newNetwork.NetworkInterface == "" { - newNetwork.NetworkInterface = name - } - err = n.createBridge(&newNetwork, usedNetworks) + err = internalutil.CreateBridge(n, &newNetwork, usedNetworks) if err != nil { return nil, err } @@ -109,14 +82,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver) } - for i := range newNetwork.Subnets { - err := validateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks) - if err != nil { - return nil, err - } - if util.IsIPv6(newNetwork.Subnets[i].Subnet.IP) { - newNetwork.IPv6Enabled = true - } + err = internalutil.ValidateSubnets(&newNetwork, usedNetworks) + if err != nil { + return nil, err } // generate the network ID @@ -223,7 +191,7 @@ func createIPMACVLAN(network *types.Network) error { return errors.New("internal is not supported with macvlan") } if network.NetworkInterface != "" { - interfaceNames, err := util.GetLiveNetworkNames() + interfaceNames, err := internalutil.GetLiveNetworkNames() if err != nil { return err } @@ -238,107 +206,3 @@ func createIPMACVLAN(network *types.Network) error { } return nil } - -func (n *cniNetwork) createBridge(network *types.Network, usedNetworks []*net.IPNet) error { - if network.NetworkInterface != "" { - bridges := n.getBridgeInterfaceNames() - if pkgutil.StringInSlice(network.NetworkInterface, bridges) { - return errors.Errorf("bridge name %s already in use", network.NetworkInterface) - } - if !define.NameRegex.MatchString(network.NetworkInterface) { - return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface) - } - } else { - var err error - network.NetworkInterface, err = n.getFreeDeviceName() - if err != nil { - return err - } - } - - if len(network.Subnets) == 0 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - // ipv6 enabled means dual stack, check if we already have - // a ipv4 or ipv6 subnet and add one if not. - if network.IPv6Enabled { - ipv4 := false - ipv6 := false - for _, subnet := range network.Subnets { - if util.IsIPv6(subnet.Subnet.IP) { - ipv6 = true - } - if util.IsIPv4(subnet.Subnet.IP) { - ipv4 = true - } - } - if !ipv4 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - if !ipv6 { - freeSubnet, err := n.getFreeIPv6NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - } - network.IPAMOptions["driver"] = types.HostLocalIPAMDriver - return nil -} - -// validateSubnet will validate a given Subnet. It checks if the -// given gateway and lease range are part of this subnet. If the -// gateway is empty and addGateway is true it will get the first -// available ip in the subnet assigned. -func validateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) error { - if s == nil { - return errors.New("subnet is nil") - } - if s.Subnet.IP == nil { - return errors.New("subnet ip is nil") - } - - // Reparse to ensure subnet is valid. - // Do not use types.ParseCIDR() because we want the ip to be - // the network address and not a random ip in the subnet. - _, net, err := net.ParseCIDR(s.Subnet.String()) - if err != nil { - return errors.Wrap(err, "subnet invalid") - } - - // check that the new subnet does not conflict with existing ones - if util.NetworkIntersectsWithNetworks(net, usedNetworks) { - return errors.Errorf("subnet %s is already used on the host or by another config", net.String()) - } - - s.Subnet = types.IPNet{IPNet: *net} - if s.Gateway != nil { - if !s.Subnet.Contains(s.Gateway) { - return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) - } - } else if addGateway { - ip, err := util.FirstIPInSubnet(net) - if err != nil { - return err - } - s.Gateway = ip - } - if s.LeaseRange != nil { - if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) { - return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) - } - if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) { - return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) - } - } - return nil -} diff --git a/libpod/network/cni/network.go b/libpod/network/cni/network.go index a37a84373..3e9cdaa47 100644 --- a/libpod/network/cni/network.go +++ b/libpod/network/cni/network.go @@ -6,8 +6,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "fmt" - "net" "os" "strings" "time" @@ -15,8 +13,6 @@ import ( "github.com/containernetworking/cni/libcni" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/network/types" - "github.com/containers/podman/v3/libpod/network/util" - pkgutil "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage/pkg/lockfile" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -242,111 +238,29 @@ func getNetworkIDFromName(name string) string { return hex.EncodeToString(hash[:]) } -// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet -func (n *cniNetwork) getFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { - // the default podman network is 10.88.0.0/16 - // start locking for free /24 networks - network := &net.IPNet{ - IP: net.IP{10, 89, 0, 0}, - Mask: net.IPMask{255, 255, 255, 0}, - } - - // TODO: make sure to not use public subnets - for { - if intersectsConfig := util.NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig { - logrus.Debugf("found free ipv4 network subnet %s", network.String()) - return &types.Subnet{ - Subnet: types.IPNet{IPNet: *network}, - }, nil - } - var err error - network, err = util.NextSubnet(network) - if err != nil { - return nil, err - } - } -} +// Implement the NetUtil interface for easy code sharing with other network interfaces. -// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet -func (n *cniNetwork) getFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { - // FIXME: Is 10000 fine as limit? We should prevent an endless loop. - for i := 0; i < 10000; i++ { - // RFC4193: Choose the ipv6 subnet random and NOT sequentially. - network, err := util.GetRandomIPv6Subnet() - if err != nil { - return nil, err - } - if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig { - logrus.Debugf("found free ipv6 network subnet %s", network.String()) - return &types.Subnet{ - Subnet: types.IPNet{IPNet: network}, - }, nil - } - } - return nil, errors.New("failed to get random ipv6 subnet") -} - -// getUsedSubnets returns a list of all used subnets by network -// configs and interfaces on the host. -func (n *cniNetwork) getUsedSubnets() ([]*net.IPNet, error) { - // first, load all used subnets from network configs - subnets := make([]*net.IPNet, 0, len(n.networks)) +// ForEach call the given function for each network +func (n *cniNetwork) ForEach(run func(types.Network)) { for _, val := range n.networks { - for i := range val.libpodNet.Subnets { - subnets = append(subnets, &val.libpodNet.Subnets[i].Subnet.IPNet) - } + run(*val.libpodNet) } - // second, load networks from the current system - liveSubnets, err := util.GetLiveNetworkSubnets() - if err != nil { - return nil, err - } - return append(subnets, liveSubnets...), nil } -// getFreeDeviceName returns a free device name which can -// be used for new configs as name and bridge interface name -func (n *cniNetwork) getFreeDeviceName() (string, error) { - bridgeNames := n.getBridgeInterfaceNames() - netNames := n.getUsedNetworkNames() - liveInterfaces, err := util.GetLiveNetworkNames() - if err != nil { - return "", nil - } - names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces)) - names = append(names, bridgeNames...) - names = append(names, netNames...) - names = append(names, liveInterfaces...) - // FIXME: Is a limit fine? - // Start by 1, 0 is reserved for the default network - for i := 1; i < 1000000; i++ { - deviceName := fmt.Sprintf("%s%d", cniDeviceName, i) - if !pkgutil.StringInSlice(deviceName, names) { - logrus.Debugf("found free device name %s", deviceName) - return deviceName, nil - } - } - return "", errors.New("could not find free device name, to many iterations") +// Len return the number of networks +func (n *cniNetwork) Len() int { + return len(n.networks) } -// getUsedNetworkNames returns all network names already used -// by network configs -func (n *cniNetwork) getUsedNetworkNames() []string { - names := make([]string, 0, len(n.networks)) - for _, val := range n.networks { - names = append(names, val.libpodNet.Name) - } - return names +// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number. +func (n *cniNetwork) DefaultInterfaceName() string { + return cniDeviceName } -// getUsedNetworkNames returns all bridge device names already used -// by network configs -func (n *cniNetwork) getBridgeInterfaceNames() []string { - names := make([]string, 0, len(n.networks)) - for _, val := range n.networks { - if val.libpodNet.Driver == types.BridgeNetworkDriver { - names = append(names, val.libpodNet.NetworkInterface) - } +func (n *cniNetwork) Network(nameOrID string) (*types.Network, error) { + network, err := n.getNetwork(nameOrID) + if err != nil { + return nil, err } - return names + return network.libpodNet, err } diff --git a/libpod/network/cni/run.go b/libpod/network/cni/run.go index 7795dfeeb..667ed3ab1 100644 --- a/libpod/network/cni/run.go +++ b/libpod/network/cni/run.go @@ -13,6 +13,7 @@ import ( types040 "github.com/containernetworking/cni/pkg/types/040" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/internal/util" "github.com/containers/podman/v3/libpod/network/types" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -30,24 +31,9 @@ func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (ma return nil, err } - if namespacePath == "" { - return nil, errors.New("namespacePath is empty") - } - if options.ContainerID == "" { - return nil, errors.New("ContainerID is empty") - } - if len(options.Networks) == 0 { - return nil, errors.New("must specify at least one network") - } - for name, netOpts := range options.Networks { - network := n.networks[name] - if network == nil { - return nil, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name) - } - err := validatePerNetworkOpts(network, netOpts) - if err != nil { - return nil, err - } + err = util.ValidateSetupOptions(n, namespacePath, options) + if err != nil { + return nil, err } // set the loopback adapter up in the container netns @@ -172,23 +158,6 @@ func CNIResultToStatus(res cnitypes.Result) (types.StatusBlock, error) { return result, nil } -// validatePerNetworkOpts checks that all given static ips are in a subnet on this network -func validatePerNetworkOpts(network *network, netOpts types.PerNetworkOptions) error { - if netOpts.InterfaceName == "" { - return errors.Errorf("interface name on network %s is empty", network.libpodNet.Name) - } -outer: - for _, ip := range netOpts.StaticIPs { - for _, s := range network.libpodNet.Subnets { - if s.Subnet.Contains(ip) { - continue outer - } - } - return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.libpodNet.Name) - } - return nil -} - func getRuntimeConfig(netns, conName, conID, networkName string, ports []cniPortMapEntry, opts types.PerNetworkOptions) *libcni.RuntimeConf { rt := &libcni.RuntimeConf{ ContainerID: conID, diff --git a/libpod/network/cni/run_test.go b/libpod/network/cni/run_test.go index 3169cd0eb..6c54f82ef 100644 --- a/libpod/network/cni/run_test.go +++ b/libpod/network/cni/run_test.go @@ -1232,7 +1232,7 @@ var _ = Describe("run CNI", func() { } _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network somenet: network not found")) + Expect(err.Error()).To(ContainSubstring("unable to find network with name or ID somenet: network not found")) }) }) diff --git a/libpod/network/internal/util/bridge.go b/libpod/network/internal/util/bridge.go new file mode 100644 index 000000000..476557050 --- /dev/null +++ b/libpod/network/internal/util/bridge.go @@ -0,0 +1,69 @@ +package util + +import ( + "net" + + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + pkgutil "github.com/containers/podman/v3/pkg/util" + "github.com/pkg/errors" +) + +func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet) error { + if network.NetworkInterface != "" { + bridges := GetBridgeInterfaceNames(n) + if pkgutil.StringInSlice(network.NetworkInterface, bridges) { + return errors.Errorf("bridge name %s already in use", network.NetworkInterface) + } + if !define.NameRegex.MatchString(network.NetworkInterface) { + return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface) + } + } else { + var err error + network.NetworkInterface, err = GetFreeDeviceName(n) + if err != nil { + return err + } + } + + if network.IPAMOptions["driver"] != types.DHCPIPAMDriver { + if len(network.Subnets) == 0 { + freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + // ipv6 enabled means dual stack, check if we already have + // a ipv4 or ipv6 subnet and add one if not. + if network.IPv6Enabled { + ipv4 := false + ipv6 := false + for _, subnet := range network.Subnets { + if util.IsIPv6(subnet.Subnet.IP) { + ipv6 = true + } + if util.IsIPv4(subnet.Subnet.IP) { + ipv4 = true + } + } + if !ipv4 { + freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + if !ipv6 { + freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + } + network.IPAMOptions["driver"] = types.HostLocalIPAMDriver + } + return nil +} diff --git a/libpod/network/internal/util/create.go b/libpod/network/internal/util/create.go new file mode 100644 index 000000000..cecfd7133 --- /dev/null +++ b/libpod/network/internal/util/create.go @@ -0,0 +1,42 @@ +package util + +import ( + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/pkg/errors" +) + +func CommonNetworkCreate(n NetUtil, network *types.Network) error { + if network.Labels == nil { + network.Labels = map[string]string{} + } + if network.Options == nil { + network.Options = map[string]string{} + } + if network.IPAMOptions == nil { + network.IPAMOptions = map[string]string{} + } + + var name string + var err error + // validate the name when given + if network.Name != "" { + if !define.NameRegex.MatchString(network.Name) { + return errors.Wrapf(define.RegexError, "network name %s invalid", network.Name) + } + if _, err := n.Network(network.Name); err == nil { + return errors.Wrapf(define.ErrNetworkExists, "network name %s already used", network.Name) + } + } else { + name, err = GetFreeDeviceName(n) + if err != nil { + return err + } + network.Name = name + // also use the name as interface name when we create a bridge network + if network.Driver == types.BridgeNetworkDriver && network.NetworkInterface == "" { + network.NetworkInterface = name + } + } + return nil +} diff --git a/libpod/network/internal/util/interface.go b/libpod/network/internal/util/interface.go new file mode 100644 index 000000000..4b01a09b8 --- /dev/null +++ b/libpod/network/internal/util/interface.go @@ -0,0 +1,19 @@ +package util + +import "github.com/containers/podman/v3/libpod/network/types" + +// This is a helper package to allow code sharing between the different +// network interfaces. + +// NetUtil is a helper interface which all network interfaces should implement to allow easy code sharing +type NetUtil interface { + // ForEach eaxecutes the given function for each network + ForEach(func(types.Network)) + // Len returns the number of networks + Len() int + // DefaultInterfaceName return the default interface name, this will be suffixed by a number + DefaultInterfaceName() string + // Network returns the network with the given name or ID. + // It returns an error if the network is not found + Network(nameOrID string) (*types.Network, error) +} diff --git a/libpod/network/util/interfaces.go b/libpod/network/internal/util/interfaces.go index dc2bd601d..20819f756 100644 --- a/libpod/network/util/interfaces.go +++ b/libpod/network/internal/util/interfaces.go @@ -2,9 +2,9 @@ package util import "net" -// GetLiveNetworkSubnets returns a slice of subnets representing what the system +// getLiveNetworkSubnets returns a slice of subnets representing what the system // has defined as network interfaces -func GetLiveNetworkSubnets() ([]*net.IPNet, error) { +func getLiveNetworkSubnets() ([]*net.IPNet, error) { addrs, err := net.InterfaceAddrs() if err != nil { return nil, err diff --git a/libpod/network/internal/util/ip.go b/libpod/network/internal/util/ip.go new file mode 100644 index 000000000..7fe35d3d4 --- /dev/null +++ b/libpod/network/internal/util/ip.go @@ -0,0 +1,70 @@ +package util + +import ( + "crypto/rand" + "net" + + "github.com/pkg/errors" +) + +func incByte(subnet *net.IPNet, idx int, shift uint) error { + if idx < 0 { + return errors.New("no more subnets left") + } + if subnet.IP[idx] == 255 { + subnet.IP[idx] = 0 + return incByte(subnet, idx-1, 0) + } + subnet.IP[idx] += 1 << shift + return nil +} + +// NextSubnet returns subnet incremented by 1 +func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { + newSubnet := &net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + ones, bits := newSubnet.Mask.Size() + if ones == 0 { + return nil, errors.Errorf("%s has only one subnet", subnet.String()) + } + zeroes := uint(bits - ones) + shift := zeroes % 8 + idx := ones/8 - 1 + if idx < 0 { + idx = 0 + } + if err := incByte(newSubnet, idx, shift); err != nil { + return nil, err + } + return newSubnet, nil +} + +func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool { + for _, nw := range networklist { + if networkIntersect(n, nw) { + return true + } + } + return false +} + +func networkIntersect(n1, n2 *net.IPNet) bool { + return n2.Contains(n1.IP) || n1.Contains(n2.IP) +} + +// getRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879. +func getRandomIPv6Subnet() (net.IPNet, error) { + ip := make(net.IP, 8, net.IPv6len) + // read 8 random bytes + _, err := rand.Read(ip) + if err != nil { + return net.IPNet{}, nil + } + // first byte must be FD as per RFC3879 + ip[0] = 0xfd + // add 8 zero bytes + ip = append(ip, make([]byte, 8)...) + return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil +} diff --git a/libpod/network/internal/util/ip_test.go b/libpod/network/internal/util/ip_test.go new file mode 100644 index 000000000..eaed769d7 --- /dev/null +++ b/libpod/network/internal/util/ip_test.go @@ -0,0 +1,63 @@ +package util + +import ( + "fmt" + "net" + "reflect" + "testing" +) + +func parseCIDR(n string) *net.IPNet { + _, parsedNet, _ := net.ParseCIDR(n) + return parsedNet +} + +func TestNextSubnet(t *testing.T) { + type args struct { + subnet *net.IPNet + } + tests := []struct { + name string + args args + want *net.IPNet + wantErr bool + }{ + {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, + {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + got, err := NextSubnet(test.args.subnet) + if (err != nil) != test.wantErr { + t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) + return + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("NextSubnet() got = %v, want %v", got, test.want) + } + }) + } +} + +func TestGetRandomIPv6Subnet(t *testing.T) { + for i := 0; i < 1000; i++ { + t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) { + sub, err := getRandomIPv6Subnet() + if err != nil { + t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err) + return + } + if sub.IP.To4() != nil { + t.Errorf("ip %s is not an ipv6 address", sub.IP) + } + if sub.IP[0] != 0xfd { + t.Errorf("ipv6 %s does not start with fd", sub.IP) + } + ones, bytes := sub.Mask.Size() + if ones != 64 || bytes != 128 { + t.Errorf("wrong network mask %v, it should be /64", sub.Mask) + } + }) + } +} diff --git a/libpod/network/internal/util/parse.go b/libpod/network/internal/util/parse.go new file mode 100644 index 000000000..1f68df0bb --- /dev/null +++ b/libpod/network/internal/util/parse.go @@ -0,0 +1,37 @@ +package util + +import ( + "strconv" + + "github.com/pkg/errors" +) + +// ParseMTU parses the mtu option +func ParseMTU(mtu string) (int, error) { + if mtu == "" { + return 0, nil // default + } + m, err := strconv.Atoi(mtu) + if err != nil { + return 0, err + } + if m < 0 { + return 0, errors.Errorf("mtu %d is less than zero", m) + } + return m, nil +} + +// ParseVlan parses the vlan option +func ParseVlan(vlan string) (int, error) { + if vlan == "" { + return 0, nil // default + } + v, err := strconv.Atoi(vlan) + if err != nil { + return 0, err + } + if v < 0 || v > 4094 { + return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v) + } + return v, nil +} diff --git a/libpod/network/internal/util/util.go b/libpod/network/internal/util/util.go new file mode 100644 index 000000000..bf9d70aba --- /dev/null +++ b/libpod/network/internal/util/util.go @@ -0,0 +1,123 @@ +package util + +import ( + "errors" + "fmt" + "net" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/pkg/util" + "github.com/sirupsen/logrus" +) + +// GetBridgeInterfaceNames returns all bridge interface names +// already used by network configs +func GetBridgeInterfaceNames(n NetUtil) []string { + names := make([]string, 0, n.Len()) + n.ForEach(func(net types.Network) { + if net.Driver == types.BridgeNetworkDriver { + names = append(names, net.NetworkInterface) + } + }) + return names +} + +// GetUsedNetworkNames returns all network names already used +// by network configs +func GetUsedNetworkNames(n NetUtil) []string { + names := make([]string, 0, n.Len()) + n.ForEach(func(net types.Network) { + if net.Driver == types.BridgeNetworkDriver { + names = append(names, net.NetworkInterface) + } + }) + return names +} + +// GetFreeDeviceName returns a free device name which can +// be used for new configs as name and bridge interface name. +// The base name is suffixed by a number +func GetFreeDeviceName(n NetUtil) (string, error) { + bridgeNames := GetBridgeInterfaceNames(n) + netNames := GetUsedNetworkNames(n) + liveInterfaces, err := GetLiveNetworkNames() + if err != nil { + return "", nil + } + names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces)) + names = append(names, bridgeNames...) + names = append(names, netNames...) + names = append(names, liveInterfaces...) + // FIXME: Is a limit fine? + // Start by 1, 0 is reserved for the default network + for i := 1; i < 1000000; i++ { + deviceName := fmt.Sprintf("%s%d", n.DefaultInterfaceName(), i) + if !util.StringInSlice(deviceName, names) { + logrus.Debugf("found free device name %s", deviceName) + return deviceName, nil + } + } + return "", errors.New("could not find free device name, to many iterations") +} + +// GetUsedSubnets returns a list of all used subnets by network +// configs and interfaces on the host. +func GetUsedSubnets(n NetUtil) ([]*net.IPNet, error) { + // first, load all used subnets from network configs + subnets := make([]*net.IPNet, 0, n.Len()) + n.ForEach(func(n types.Network) { + for i := range n.Subnets { + subnets = append(subnets, &n.Subnets[i].Subnet.IPNet) + } + }) + // second, load networks from the current system + liveSubnets, err := getLiveNetworkSubnets() + if err != nil { + return nil, err + } + return append(subnets, liveSubnets...), nil +} + +// GetFreeIPv6NetworkSubnet returns a unused ipv4 subnet +func GetFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { + // the default podman network is 10.88.0.0/16 + // start locking for free /24 networks + network := &net.IPNet{ + IP: net.IP{10, 89, 0, 0}, + Mask: net.IPMask{255, 255, 255, 0}, + } + + // TODO: make sure to not use public subnets + for { + if intersectsConfig := NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig { + logrus.Debugf("found free ipv4 network subnet %s", network.String()) + return &types.Subnet{ + Subnet: types.IPNet{IPNet: *network}, + }, nil + } + var err error + network, err = NextSubnet(network) + if err != nil { + return nil, err + } + } +} + +// GetFreeIPv6NetworkSubnet returns a unused ipv6 subnet +func GetFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { + // FIXME: Is 10000 fine as limit? We should prevent an endless loop. + for i := 0; i < 10000; i++ { + // RFC4193: Choose the ipv6 subnet random and NOT sequentially. + network, err := getRandomIPv6Subnet() + if err != nil { + return nil, err + } + if intersectsConfig := NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig { + logrus.Debugf("found free ipv6 network subnet %s", network.String()) + return &types.Subnet{ + Subnet: types.IPNet{IPNet: network}, + }, nil + } + } + return nil, errors.New("failed to get random ipv6 subnet") +} diff --git a/libpod/network/internal/util/validate.go b/libpod/network/internal/util/validate.go new file mode 100644 index 000000000..62c3f3951 --- /dev/null +++ b/libpod/network/internal/util/validate.go @@ -0,0 +1,121 @@ +package util + +import ( + "net" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + "github.com/pkg/errors" +) + +// ValidateSubnet will validate a given Subnet. It checks if the +// given gateway and lease range are part of this subnet. If the +// gateway is empty and addGateway is true it will get the first +// available ip in the subnet assigned. +func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) error { + if s == nil { + return errors.New("subnet is nil") + } + if s.Subnet.IP == nil { + return errors.New("subnet ip is nil") + } + + // Reparse to ensure subnet is valid. + // Do not use types.ParseCIDR() because we want the ip to be + // the network address and not a random ip in the subnet. + _, net, err := net.ParseCIDR(s.Subnet.String()) + if err != nil { + return errors.Wrap(err, "subnet invalid") + } + + // check that the new subnet does not conflict with existing ones + if NetworkIntersectsWithNetworks(net, usedNetworks) { + return errors.Errorf("subnet %s is already used on the host or by another config", net.String()) + } + + s.Subnet = types.IPNet{IPNet: *net} + if s.Gateway != nil { + if !s.Subnet.Contains(s.Gateway) { + return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) + } + util.NormalizeIP(&s.Gateway) + } else if addGateway { + ip, err := util.FirstIPInSubnet(net) + if err != nil { + return err + } + s.Gateway = ip + } + + if s.LeaseRange != nil { + if s.LeaseRange.StartIP != nil { + if !s.Subnet.Contains(s.LeaseRange.StartIP) { + return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) + } + util.NormalizeIP(&s.LeaseRange.StartIP) + } + if s.LeaseRange.EndIP != nil { + if !s.Subnet.Contains(s.LeaseRange.EndIP) { + return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) + } + util.NormalizeIP(&s.LeaseRange.EndIP) + } + } + return nil +} + +// ValidateSubnets will validate the subnets for this network. +// It also sets the gateway if the gateway is empty and it sets +// IPv6Enabled to true if at least one subnet is ipv6. +func ValidateSubnets(network *types.Network, usedNetworks []*net.IPNet) error { + for i := range network.Subnets { + err := ValidateSubnet(&network.Subnets[i], !network.Internal, usedNetworks) + if err != nil { + return err + } + if util.IsIPv6(network.Subnets[i].Subnet.IP) { + network.IPv6Enabled = true + } + } + return nil +} + +func ValidateSetupOptions(n NetUtil, namespacePath string, options types.SetupOptions) error { + if namespacePath == "" { + return errors.New("namespacePath is empty") + } + if options.ContainerID == "" { + return errors.New("ContainerID is empty") + } + if len(options.Networks) == 0 { + return errors.New("must specify at least one network") + } + for name, netOpts := range options.Networks { + network, err := n.Network(name) + if err != nil { + return err + } + err = validatePerNetworkOpts(network, netOpts) + if err != nil { + return err + } + } + return nil +} + +// validatePerNetworkOpts checks that all given static ips are in a subnet on this network +func validatePerNetworkOpts(network *types.Network, netOpts types.PerNetworkOptions) error { + if netOpts.InterfaceName == "" { + return errors.Errorf("interface name on network %s is empty", network.Name) + } +outer: + for _, ip := range netOpts.StaticIPs { + for _, s := range network.Subnets { + if s.Subnet.Contains(ip) { + continue outer + } + } + return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.Name) + } + return nil +} diff --git a/libpod/network/netavark/config.go b/libpod/network/netavark/config.go new file mode 100644 index 000000000..5cab76710 --- /dev/null +++ b/libpod/network/netavark/config.go @@ -0,0 +1,210 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "net" + "os" + "path/filepath" + "time" + + "github.com/containers/podman/v3/libpod/define" + internalutil "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/storage/pkg/stringid" + "github.com/pkg/errors" +) + +// NetworkCreate will take a partial filled Network and fill the +// missing fields. It creates the Network and returns the full Network. +func (n *netavarkNetwork) NetworkCreate(net types.Network) (types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return types.Network{}, err + } + network, err := n.networkCreate(net, false) + if err != nil { + return types.Network{}, err + } + // add the new network to the map + n.networks[network.Name] = network + return *network, nil +} + +func (n *netavarkNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*types.Network, error) { + // if no driver is set use the default one + if newNetwork.Driver == "" { + newNetwork.Driver = types.DefaultNetworkDriver + } + if !defaultNet { + // FIXME: Should we use a different type for network create without the ID field? + // the caller is not allowed to set a specific ID + if newNetwork.ID != "" { + return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") + } + + // generate random network ID + var i int + for i = 0; i < 1000; i++ { + id := stringid.GenerateNonCryptoID() + if _, err := n.getNetwork(id); err != nil { + newNetwork.ID = id + break + } + } + if i == 1000 { + return nil, errors.New("failed to create random network ID") + } + } + + err := internalutil.CommonNetworkCreate(n, &newNetwork) + if err != nil { + return nil, err + } + + // Only get the used networks for validation if we do not create the default network. + // The default network should not be validated against used subnets, we have to ensure + // that this network can always be created even when a subnet is already used on the host. + // This could happen if you run a container on this net, then the cni interface will be + // created on the host and "block" this subnet from being used again. + // Therefore the next podman command tries to create the default net again and it would + // fail because it thinks the network is used on the host. + var usedNetworks []*net.IPNet + if !defaultNet { + usedNetworks, err = internalutil.GetUsedSubnets(n) + if err != nil { + return nil, err + } + } + + switch newNetwork.Driver { + case types.BridgeNetworkDriver: + err = internalutil.CreateBridge(n, &newNetwork, usedNetworks) + if err != nil { + return nil, err + } + // validate the given options, we do not need them but just check to make sure they are valid + for key, value := range newNetwork.Options { + switch key { + case "mtu": + _, err = internalutil.ParseMTU(value) + if err != nil { + return nil, err + } + + case "vlan": + _, err = internalutil.ParseVlan(value) + if err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("unsupported network option %s", key) + } + } + + default: + return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver) + } + + err = internalutil.ValidateSubnets(&newNetwork, usedNetworks) + if err != nil { + return nil, err + } + + // FIXME: If we have a working solution for internal networks with dns this check should be removed. + if newNetwork.DNSEnabled && newNetwork.Internal { + return nil, errors.New("cannot set internal and dns enabled") + } + + newNetwork.Created = time.Now() + + if !defaultNet { + confPath := filepath.Join(n.networkConfigDir, newNetwork.Name+".json") + f, err := os.Create(confPath) + if err != nil { + return nil, err + } + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + err = enc.Encode(newNetwork) + if err != nil { + return nil, err + } + } + + return &newNetwork, nil +} + +// NetworkRemove will remove the Network with the given name or ID. +// It does not ensure that the network is unused. +func (n *netavarkNetwork) NetworkRemove(nameOrID string) error { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return err + } + + network, err := n.getNetwork(nameOrID) + if err != nil { + return err + } + + // Removing the default network is not allowed. + if network.Name == n.defaultNetwork { + return errors.Errorf("default network %s cannot be removed", n.defaultNetwork) + } + + file := filepath.Join(n.networkConfigDir, network.Name+".json") + // make sure to not error for ErrNotExist + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + delete(n.networks, network.Name) + return nil +} + +// NetworkList will return all known Networks. Optionally you can +// supply a list of filter functions. Only if a network matches all +// functions it is returned. +func (n *netavarkNetwork) NetworkList(filters ...types.FilterFunc) ([]types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return nil, err + } + + networks := make([]types.Network, 0, len(n.networks)) +outer: + for _, net := range n.networks { + for _, filter := range filters { + // All filters have to match, if one does not match we can skip to the next network. + if !filter(*net) { + continue outer + } + } + networks = append(networks, *net) + } + return networks, nil +} + +// NetworkInspect will return the Network with the given name or ID. +func (n *netavarkNetwork) NetworkInspect(nameOrID string) (types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return types.Network{}, err + } + + network, err := n.getNetwork(nameOrID) + if err != nil { + return types.Network{}, err + } + return *network, nil +} diff --git a/libpod/network/netavark/config_test.go b/libpod/network/netavark/config_test.go new file mode 100644 index 000000000..ee4a825f1 --- /dev/null +++ b/libpod/network/netavark/config_test.go @@ -0,0 +1,1123 @@ +// +build linux + +package netavark_test + +import ( + "bytes" + "io/ioutil" + "net" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + gomegaTypes "github.com/onsi/gomega/types" + "github.com/sirupsen/logrus" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" +) + +var _ = Describe("Config", func() { + var ( + libpodNet types.ContainerNetwork + networkConfDir string + logBuffer bytes.Buffer + ) + + BeforeEach(func() { + var err error + networkConfDir, err = ioutil.TempDir("", "podman_netavark_test") + if err != nil { + Fail("Failed to create tmpdir") + + } + logBuffer = bytes.Buffer{} + logrus.SetOutput(&logBuffer) + }) + + JustBeforeEach(func() { + var err error + libpodNet, err = getNetworkInterface(networkConfDir, false) + if err != nil { + Fail("Failed to create NewCNINetworkInterface") + } + }) + + AfterEach(func() { + os.RemoveAll(networkConfDir) + }) + + Context("basic network config tests", func() { + + It("check default network config exists", func() { + networks, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks[0].Name).To(Equal("podman")) + Expect(networks[0].Driver).To(Equal("bridge")) + Expect(networks[0].ID).To(Equal("2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9")) + Expect(networks[0].NetworkInterface).To(Equal("podman0")) + Expect(networks[0].Created.Before(time.Now())).To(BeTrue()) + Expect(networks[0].Subnets).To(HaveLen(1)) + Expect(networks[0].Subnets[0].Subnet.String()).To(Equal("10.88.0.0/16")) + Expect(networks[0].Subnets[0].Gateway.String()).To(Equal("10.88.0.1")) + Expect(networks[0].Subnets[0].LeaseRange).To(BeNil()) + Expect(networks[0].IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(networks[0].Options).To(BeEmpty()) + Expect(networks[0].Labels).To(BeEmpty()) + Expect(networks[0].DNSEnabled).To(BeFalse()) + Expect(networks[0].Internal).To(BeFalse()) + }) + + It("basic network create, inspect and remove", func() { + // Because we get the time from the file create timestamp there is small precision + // loss so lets remove 500 milliseconds to make sure this test does not flake. + now := time.Now().Add(-500 * time.Millisecond) + network := types.Network{} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).To(BeEmpty()) + Expect(network1.Options).To(BeEmpty()) + Expect(network1.IPAMOptions).ToNot(BeEmpty()) + Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(network1.Created.After(now)).To(BeTrue()) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.DNSEnabled).To(BeFalse()) + Expect(network1.Internal).To(BeFalse()) + + // inspect by name + network2, err := libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // inspect by ID + network2, err = libpodNet.NetworkInspect(network1.ID) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // inspect by partial ID + network2, err = libpodNet.NetworkInspect(network1.ID[:10]) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // create a new interface to force a config load from disk + libpodNet, err = getNetworkInterface(networkConfDir, false) + Expect(err).To(BeNil()) + + network2, err = libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + Expect(path).ToNot(BeARegularFile()) + + _, err = libpodNet.NetworkInspect(network1.Name) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network not found")) + }) + + It("create two networks", func() { + network := types.Network{} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.Subnets).To(HaveLen(1)) + + network = types.Network{} + network2, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network2.Name).ToNot(Equal(network1.Name)) + Expect(network2.ID).ToNot(Equal(network1.ID)) + Expect(network2.NetworkInterface).ToNot(Equal(network1.NetworkInterface)) + Expect(network2.Subnets).To(HaveLen(1)) + Expect(network2.Subnets[0].Subnet.Contains(network1.Subnets[0].Subnet.IP)).To(BeFalse()) + }) + + It("create bridge config", func() { + network := types.Network{Driver: "bridge"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).To(BeEmpty()) + Expect(network1.Options).To(BeEmpty()) + Expect(network1.IPAMOptions).ToNot(BeEmpty()) + Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.DNSEnabled).To(BeFalse()) + Expect(network1.Internal).To(BeFalse()) + }) + + It("create bridge with same name should fail", func() { + network := types.Network{ + Driver: "bridge", + NetworkInterface: "podman2", + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).To(Equal("podman2")) + Expect(network1.Driver).To(Equal("bridge")) + + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("bridge name podman2 already in use")) + }) + + It("create bridge with subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 subnet", func() { + subnet := "fdcc::/64" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.IPv6Enabled).To(BeTrue()) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("fdcc::1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + + // reload configs from disk + libpodNet, err = getNetworkInterface(networkConfDir, false) + Expect(err).To(BeNil()) + // check the the networks are identical + network2, err := libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + }) + + It("create bridge with ipv6 enabled", func() { + network := types.Network{ + Driver: "bridge", + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(ContainSubstring(".0/24")) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv4 subnet", func() { + subnet := "10.100.0.0/24" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv6 subnet", func() { + subnet := "fd66::/64" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring(".0/24")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv4+ipv6 subnet", func() { + subnet1 := "10.100.0.0/24" + n1, _ := types.ParseCIDR(subnet1) + subnet2 := "fd66::/64" + n2, _ := types.ParseCIDR(subnet2) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n1}, {Subnet: n2}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2)) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and two ipv4 subnets", func() { + subnet1 := "10.100.0.0/24" + n1, _ := types.ParseCIDR(subnet1) + subnet2 := "10.200.0.0/24" + n2, _ := types.ParseCIDR(subnet2) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n1}, {Subnet: n2}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(3)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2)) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + Expect(network1.Subnets[2].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[2].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[2].LeaseRange).To(BeNil()) + }) + + It("create bridge with subnet and gateway", func() { + subnet := "10.0.0.5/24" + n, _ := types.ParseCIDR(subnet) + gateway := "10.0.0.50" + g := net.ParseIP(gateway) + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, Gateway: g}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.0.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal(gateway)) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + }) + + It("create bridge with subnet and gateway not in the same subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + gateway := "10.10.0.50" + g := net.ParseIP(gateway) + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, Gateway: g}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + }) + + It("create bridge with subnet and lease range", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + startIP := "10.0.0.10" + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + }}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP)) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + + endIP := "10.0.0.10" + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + EndIP: net.ParseIP(endIP), + }}, + }, + } + network1, err = libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP)) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + EndIP: net.ParseIP(endIP), + }}, + }, + } + network1, err = libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP)) + Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP)) + }) + + It("create bridge with subnet and invalid lease range", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + startIP := "10.0.1.2" + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + }}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + + endIP := "10.1.1.1" + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + EndIP: net.ParseIP(endIP), + }}, + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + }) + + It("create bridge with broken subnet", func() { + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: types.IPNet{}}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet ip is nil")) + }) + + It("create network with name", func() { + name := "myname" + network := types.Network{ + Name: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal(name)) + Expect(network1.NetworkInterface).ToNot(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid name", func() { + name := "myname@some" + network := types.Network{ + Name: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with name", func() { + name := "myname" + network := types.Network{ + Name: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal(name)) + Expect(network1.NetworkInterface).ToNot(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid name", func() { + name := "myname@some" + network := types.Network{ + Name: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with interface name", func() { + name := "myname" + network := types.Network{ + NetworkInterface: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(Equal(name)) + Expect(network1.NetworkInterface).To(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid interface name", func() { + name := "myname@some" + network := types.Network{ + NetworkInterface: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with ID should fail", func() { + id := "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121" + network := types.Network{ + ID: id, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("ID can not be set for network create")) + }) + + It("create bridge with dns", func() { + network := types.Network{ + Driver: "bridge", + DNSEnabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.DNSEnabled).To(BeTrue()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"dns_enabled": true`) + }) + + It("create bridge with internal", func() { + network := types.Network{ + Driver: "bridge", + Internal: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).ToNot(BeEmpty()) + Expect(network1.Subnets[0].Gateway).To(BeNil()) + Expect(network1.Internal).To(BeTrue()) + }) + + It("create network with labels", func() { + network := types.Network{ + Labels: map[string]string{ + "key": "value", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).ToNot(BeNil()) + Expect(network1.Labels).To(ContainElement("value")) + }) + + It("create network with mtu option", func() { + network := types.Network{ + Options: map[string]string{ + "mtu": "1500", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Options).ToNot(BeNil()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"mtu": "1500"`) + Expect(network1.Options).To(HaveKeyWithValue("mtu", "1500")) + }) + + It("create network with invalid mtu option", func() { + network := types.Network{ + Options: map[string]string{ + "mtu": "abc", + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`)) + + network = types.Network{ + Options: map[string]string{ + "mtu": "-1", + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`mtu -1 is less than zero`)) + }) + + It("create network with vlan option", func() { + network := types.Network{ + Options: map[string]string{ + "vlan": "5", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Options).ToNot(BeNil()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"vlan": "5"`) + Expect(network1.Options).To(HaveKeyWithValue("vlan", "5")) + }) + + It("create network with invalid vlan option", func() { + network := types.Network{ + Options: map[string]string{ + "vlan": "abc", + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`)) + + network = types.Network{ + Options: map[string]string{ + "vlan": "-1", + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`vlan ID -1 must be between 0 and 4094`)) + }) + + It("network create unsupported option", func() { + network := types.Network{Options: map[string]string{ + "someopt": "", + }} + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported network option someopt")) + }) + + It("network create unsupported driver", func() { + network := types.Network{ + Driver: "someDriver", + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported driver someDriver")) + }) + + It("network create internal and dns", func() { + network := types.Network{ + Driver: "bridge", + Internal: true, + DNSEnabled: true, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("cannot set internal and dns enabled")) + }) + + It("network inspect partial ID", func() { + network := types.Network{Name: "net4"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.ID).To(HaveLen(64)) + + network2, err := libpodNet.NetworkInspect(network1.ID[:10]) + Expect(err).ToNot(HaveOccurred()) + EqualNetwork(network2, network1) + }) + + It("network create two with same name", func() { + network := types.Network{Name: "net"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal("net")) + network = types.Network{Name: "net"} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network name net already used")) + }) + + It("remove default network config should fail", func() { + err := libpodNet.NetworkRemove("podman") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("default network podman cannot be removed")) + + network, err := libpodNet.NetworkInspect("podman") + Expect(err).To(BeNil()) + err = libpodNet.NetworkRemove(network.ID) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("default network podman cannot be removed")) + }) + + It("network create with same subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + subnet2 := "10.10.0.0/24" + n2, _ := types.ParseCIDR(subnet2) + network := types.Network{Subnets: []types.Subnet{{Subnet: n}, {Subnet: n2}}} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Subnets).To(HaveLen(2)) + network = types.Network{Subnets: []types.Subnet{{Subnet: n}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.0.0.0/24 is already used on the host or by another config")) + network = types.Network{Subnets: []types.Subnet{{Subnet: n2}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.10.0.0/24 is already used on the host or by another config")) + }) + }) + + Context("network load valid existing ones", func() { + + BeforeEach(func() { + dir := "testfiles/valid" + files, err := ioutil.ReadDir(dir) + if err != nil { + Fail("Failed to read test directory") + } + for _, file := range files { + filename := file.Name() + data, err := ioutil.ReadFile(filepath.Join(dir, filename)) + if err != nil { + Fail("Failed to copy test files") + } + err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700) + if err != nil { + Fail("Failed to copy test files") + } + } + }) + + It("load networks from disk", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + // test the we do not show logrus warnings/errors + logString := logBuffer.String() + Expect(logString).To(BeEmpty()) + }) + + It("change network struct fields should not affect network struct in the backend", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + + nets[0].Name = "myname" + nets, err = libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + Expect(nets).ToNot(ContainElement(HaveNetworkName("myname"))) + + network, err := libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + network.NetworkInterface = "abc" + + network, err = libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + Expect(network.NetworkInterface).ToNot(Equal("abc")) + }) + + It("bridge network", func() { + network, err := libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("bridge")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman9")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.8.0/24")) + Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.8.1")) + Expect(network.Subnets[0].LeaseRange).ToNot(BeNil()) + Expect(network.Subnets[0].LeaseRange.StartIP.String()).To(Equal("10.89.8.20")) + Expect(network.Subnets[0].LeaseRange.EndIP.String()).To(Equal("10.89.8.50")) + Expect(network.Internal).To(BeFalse()) + }) + + It("internal network", func() { + network, err := libpodNet.NetworkInspect("internal") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("internal")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman8")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.7.0/24")) + Expect(network.Subnets[0].Gateway).To(BeNil()) + Expect(network.Internal).To(BeTrue()) + }) + + It("bridge network with mtu", func() { + network, err := libpodNet.NetworkInspect("mtu") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("mtu")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman13")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.11.0/24")) + Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.11.1")) + Expect(network.Internal).To(BeFalse()) + Expect(network.Options).To(HaveLen(1)) + Expect(network.Options).To(HaveKeyWithValue("mtu", "1500")) + }) + + It("bridge network with vlan", func() { + network, err := libpodNet.NetworkInspect("vlan") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("vlan")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman14")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Options).To(HaveLen(1)) + Expect(network.Options).To(HaveKeyWithValue("vlan", "5")) + }) + + It("bridge network with labels", func() { + network, err := libpodNet.NetworkInspect("label") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("label")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman15")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Labels).To(HaveLen(1)) + Expect(network.Labels).To(HaveKeyWithValue("mykey", "value")) + }) + + It("dual stack network", func() { + network, err := libpodNet.NetworkInspect("dualstack") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("dualstack")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman21")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(2)) + + sub1, _ := types.ParseCIDR("fd10:88:a::/64") + sub2, _ := types.ParseCIDR("10.89.19.0/24") + Expect(network.Subnets).To(ContainElements( + types.Subnet{Subnet: sub1, Gateway: net.ParseIP("fd10:88:a::1")}, + types.Subnet{Subnet: sub2, Gateway: net.ParseIP("10.89.19.10").To4()}, + )) + }) + + It("network list with filters (name)", func() { + filters := map[string][]string{ + "name": {"internal", "bridge"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (partial name)", func() { + filters := map[string][]string{ + "name": {"inte", "bri"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (partial id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420", "17f29b073143d8cd97b5bbe492bde"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (driver)", func() { + filters := map[string][]string{ + "driver": {"bridge"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(7)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"), + HaveNetworkName("mtu"), HaveNetworkName("vlan"), HaveNetworkName("podman"), + HaveNetworkName("label"), HaveNetworkName("dualstack"))) + }) + + It("network list with filters (label)", func() { + filters := map[string][]string{ + "label": {"mykey"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + + filters = map[string][]string{ + "label": {"mykey=value"}, + } + filterFuncs, err = util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err = libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + }) + + It("network list with filters", func() { + filters := map[string][]string{ + "driver": {"bridge"}, + "label": {"mykey"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + Expect(filterFuncs).To(HaveLen(2)) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + + filters = map[string][]string{ + "driver": {"macvlan"}, + "label": {"mykey"}, + } + filterFuncs, err = util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err = libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(0)) + }) + + It("create bridge network with used interface name", func() { + network := types.Network{ + NetworkInterface: "podman9", + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("bridge name podman9 already in use")) + }) + }) + + Context("network load invalid existing ones", func() { + + BeforeEach(func() { + dir := "testfiles/invalid" + files, err := ioutil.ReadDir(dir) + if err != nil { + Fail("Failed to read test directory") + } + for _, file := range files { + filename := file.Name() + data, err := ioutil.ReadFile(filepath.Join(dir, filename)) + if err != nil { + Fail("Failed to copy test files") + } + err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700) + if err != nil { + Fail("Failed to copy test files") + } + } + }) + + It("load invalid networks from disk", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(1)) + logString := logBuffer.String() + Expect(logString).To(ContainSubstring("Error reading network config file \\\"%s/broken.json\\\": unexpected EOF", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid name.json\\\" has invalid name: \\\"invalid name\\\", skipping: names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*: invalid argument", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config name \\\"name_miss\\\" does not match file name \\\"name_missmatch.json\\\", skipping")) + Expect(logString).To(ContainSubstring("Network config \\\"%s/wrongID.json\\\" could not be parsed, skipping: invalid network ID \\\"someID\\\"", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid_gateway.json\\\" could not be parsed, skipping: gateway 10.89.100.1 not in subnet 10.89.9.0/24", networkConfDir)) + }) + + }) + +}) + +func grepInFile(path string, match string) { + data, err := ioutil.ReadFile(path) + ExpectWithOffset(1, err).To(BeNil()) + ExpectWithOffset(1, string(data)).To(ContainSubstring(match)) +} + +// HaveNetworkName is a custom GomegaMatcher to match a network name +func HaveNetworkName(name string) gomegaTypes.GomegaMatcher { + return WithTransform(func(e types.Network) string { + return e.Name + }, Equal(name)) +} + +// EqualNetwork must be used because comparing the time with deep equal does not work +func EqualNetwork(net1, net2 types.Network) { + ExpectWithOffset(1, net1.Created.Equal(net2.Created)).To(BeTrue(), "net1 created: %v is not equal net2 created: %v", net1.Created, net2.Created) + net1.Created = time.Time{} + net2.Created = time.Time{} + ExpectWithOffset(1, net1).To(Equal(net2)) +} diff --git a/libpod/network/netavark/const.go b/libpod/network/netavark/const.go new file mode 100644 index 000000000..9709315c6 --- /dev/null +++ b/libpod/network/netavark/const.go @@ -0,0 +1,5 @@ +// +build linux + +package netavark + +const defaultBridgeName = "podman" diff --git a/libpod/network/netavark/exec.go b/libpod/network/netavark/exec.go new file mode 100644 index 000000000..d6458eeb4 --- /dev/null +++ b/libpod/network/netavark/exec.go @@ -0,0 +1,128 @@ +package netavark + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "strconv" + + "github.com/sirupsen/logrus" +) + +type netavarkError struct { + exitCode int + // Set the json key to "error" so we can directly unmarshal into this struct + Msg string `json:"error"` + err error +} + +func (e *netavarkError) Error() string { + ec := "" + // only add the exit code the the error message if we have at least info log level + // the normal user does not need to care about the number + if e.exitCode > 0 && logrus.IsLevelEnabled(logrus.InfoLevel) { + ec = " (exit code " + strconv.Itoa(e.exitCode) + ")" + } + msg := "netavark" + ec + if len(msg) > 0 { + msg += ": " + e.Msg + } + if e.err != nil { + msg += ": " + e.err.Error() + } + return msg +} + +func (e *netavarkError) Unwrap() error { + return e.err +} + +func newNetavarkError(msg string, err error) error { + return &netavarkError{ + Msg: msg, + err: err, + } +} + +// getRustLogEnv returns the RUST_LOG env var based on the current logrus level +func getRustLogEnv() string { + level := logrus.GetLevel().String() + // rust env_log uses warn instead of warning + if level == "warning" { + level = "warn" + } + // the rust netlink library is very verbose + // make sure to only log netavark logs + return "RUST_LOG=netavark=" + level +} + +// execNetavark will execute netavark with the following arguments +// It takes the path to the binary, the list of args and an interface which is +// marshaled to json and send via stdin to netavark. The result interface is +// used to marshal the netavark output into it. This can be nil. +// All errors return by this function should be of the type netavarkError +// to provide a helpful error message. +func execNetavark(binary string, args []string, stdin, result interface{}) error { + stdinR, stdinW, err := os.Pipe() + if err != nil { + return newNetavarkError("failed to create stdin pipe", err) + } + defer stdinR.Close() + + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + return newNetavarkError("failed to create stdout pipe", err) + } + defer stdoutR.Close() + defer stdoutW.Close() + + cmd := exec.Command(binary, args...) + // connect the pipes to stdin and stdout + cmd.Stdin = stdinR + cmd.Stdout = stdoutW + // connect stderr to the podman stderr for logging + cmd.Stderr = os.Stderr + // set the netavark log level to the same as the podman + cmd.Env = append(os.Environ(), getRustLogEnv()) + // if we run with debug log level lets also set RUST_BACKTRACE=1 so we can get the full stack trace in case of panics + if logrus.IsLevelEnabled(logrus.DebugLevel) { + cmd.Env = append(cmd.Env, "RUST_BACKTRACE=1") + } + + err = cmd.Start() + if err != nil { + return newNetavarkError("failed to start process", err) + } + err = json.NewEncoder(stdinW).Encode(stdin) + stdinW.Close() + if err != nil { + return newNetavarkError("failed to encode stdin data", err) + } + + dec := json.NewDecoder(stdoutR) + + err = cmd.Wait() + stdoutW.Close() + if err != nil { + exitError := &exec.ExitError{} + if errors.As(err, &exitError) { + ne := &netavarkError{} + // lets disallow unknown fields to make sure we do not get some unexpected stuff + dec.DisallowUnknownFields() + // this will unmarshal the error message into the error struct + ne.err = dec.Decode(ne) + ne.exitCode = exitError.ExitCode() + return ne + } + return newNetavarkError("unexpected failure during execution", err) + } + + if result != nil { + err = dec.Decode(result) + if err != nil { + return newNetavarkError("failed to decode result", err) + } + } + return nil +} diff --git a/libpod/network/netavark/ipam.go b/libpod/network/netavark/ipam.go new file mode 100644 index 000000000..db46ee652 --- /dev/null +++ b/libpod/network/netavark/ipam.go @@ -0,0 +1,368 @@ +package netavark + +import ( + "encoding/json" + "fmt" + "net" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + "github.com/pkg/errors" + "go.etcd.io/bbolt" +) + +// IPAM boltdb structure +// Each network has their own bucket with the network name as bucket key. +// Inside the network bucket there is an ID bucket which maps the container ID (key) +// to a json array of ip addresses (value). +// The network bucket also has a bucket for each subnet, the subnet is used as key. +// Inside the subnet bucket an ip is used as key and the container ID as value. + +const ( + idBucket = "ids" + // lastIP this is used as key to store the last allocated ip + // note that this string should not be 4 or 16 byte long + lastIP = "lastIP" +) + +var ( + idBucketKey = []byte(idBucket) + lastIPKey = []byte(lastIP) +) + +type ipamError struct { + msg string + cause error +} + +func (e *ipamError) Error() string { + msg := "IPAM error" + if e.msg != "" { + msg += ": " + e.msg + } + if e.cause != nil { + msg += ": " + e.cause.Error() + } + return msg +} + +func newIPAMError(cause error, msg string, args ...interface{}) *ipamError { + return &ipamError{ + msg: fmt.Sprintf(msg, args...), + cause: cause, + } +} + +// openDB will open the ipam database +// Note that the caller has to Close it. +func (n *netavarkNetwork) openDB() (*bbolt.DB, error) { + db, err := bbolt.Open(n.ipamDBPath, 0600, nil) + if err != nil { + return nil, newIPAMError(err, "failed to open database %s", n.ipamDBPath) + } + return db, nil +} + +// allocIPs will allocate ips for the the container. It will change the +// NetworkOptions in place. When static ips are given it will validate +// that these are free to use and will allocate them to the container. +func (n *netavarkNetwork) allocIPs(opts *types.NetworkOptions) error { + db, err := n.openDB() + if err != nil { + return err + } + defer db.Close() + + err = db.Update(func(tx *bbolt.Tx) error { + for netName, netOpts := range opts.Networks { + network := n.networks[netName] + if network == nil { + return newIPAMError(nil, "could not find network %q", netName) + } + + // check if we have to alloc ips + if !requiresIPAMAlloc(network) { + continue + } + + // create/get network bucket + netBkt, err := tx.CreateBucketIfNotExists([]byte(netName)) + if err != nil { + return newIPAMError(err, "failed to create/get network bucket for network %s", netName) + } + + // requestIPs is the list of ips which should be used for this container + requestIPs := make([]net.IP, 0, len(network.Subnets)) + + for _, subnet := range network.Subnets { + subnetBkt, err := netBkt.CreateBucketIfNotExists([]byte(subnet.Subnet.String())) + if err != nil { + return newIPAMError(err, "failed to create/get subnet bucket for network %s", netName) + } + + // search for a static ip which matches the current subnet + // in this case the user wants this one and we should not assign a free one + var ip net.IP + for _, staticIP := range netOpts.StaticIPs { + if subnet.Subnet.Contains(staticIP) { + ip = staticIP + break + } + } + + // when static ip is requested for this network + if ip != nil { + // convert to 4 byte ipv4 if needed + util.NormalizeIP(&ip) + id := subnetBkt.Get(ip) + if id != nil { + return newIPAMError(nil, "requested ip address %s is already allocated to container ID %s", ip.String(), string(id)) + } + } else { + ip, err = getFreeIPFromBucket(subnetBkt, subnet) + if err != nil { + return err + } + err = subnetBkt.Put(lastIPKey, ip) + if err != nil { + return newIPAMError(err, "failed to store last ip in database") + } + } + + err = subnetBkt.Put(ip, []byte(opts.ContainerID)) + if err != nil { + return newIPAMError(err, "failed to store ip in database") + } + + requestIPs = append(requestIPs, ip) + } + + idsBucket, err := netBkt.CreateBucketIfNotExists(idBucketKey) + if err != nil { + return newIPAMError(err, "failed to create/get id bucket for network %s", netName) + } + + ipsBytes, err := json.Marshal(requestIPs) + if err != nil { + return newIPAMError(err, "failed to marshal ips") + } + + err = idsBucket.Put([]byte(opts.ContainerID), ipsBytes) + if err != nil { + return newIPAMError(err, "failed to store ips in database") + } + + netOpts.StaticIPs = requestIPs + opts.Networks[netName] = netOpts + } + return nil + }) + return err +} + +func getFreeIPFromBucket(bucket *bbolt.Bucket, subnet types.Subnet) (net.IP, error) { + var rangeStart net.IP + var rangeEnd net.IP + if subnet.LeaseRange != nil { + rangeStart = subnet.LeaseRange.StartIP + rangeEnd = subnet.LeaseRange.EndIP + } + + if rangeStart == nil { + // let start with the first ip in subnet + rangeStart = util.NextIP(subnet.Subnet.IP) + } + + if rangeEnd == nil { + lastIP, err := util.LastIPInSubnet(&subnet.Subnet.IPNet) + // this error should never happen but lets check anyways to prevent panics + if err != nil { + return nil, errors.Wrap(err, "failed to get lastIP") + } + // ipv4 uses the last ip in a subnet for broadcast so we cannot use it + if util.IsIPv4(lastIP) { + lastIP = util.PrevIP(lastIP) + } + rangeEnd = lastIP + } + + lastIPByte := bucket.Get(lastIPKey) + curIP := net.IP(lastIPByte) + if curIP == nil { + curIP = rangeStart + } else { + curIP = util.NextIP(curIP) + } + + // store the start ip to make sure we know when we looped over all available ips + startIP := curIP + + for { + // skip the gateway + if subnet.Gateway != nil { + if util.Cmp(curIP, subnet.Gateway) == 0 { + curIP = util.NextIP(curIP) + continue + } + } + + // if we are at the end we need to jump back to the start ip + if util.Cmp(curIP, rangeEnd) > 0 { + if util.Cmp(rangeStart, startIP) == 0 { + return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String()) + } + curIP = rangeStart + continue + } + + // check if ip is already used by another container + // if not return it + if bucket.Get(curIP) == nil { + return curIP, nil + } + + curIP = util.NextIP(curIP) + + if util.Cmp(curIP, startIP) == 0 { + return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String()) + } + } +} + +// getAssignedIPs will read the ipam database and will fill in the used ips for this container. +// It will change the NetworkOptions in place. +func (n *netavarkNetwork) getAssignedIPs(opts *types.NetworkOptions) error { + db, err := n.openDB() + if err != nil { + return err + } + defer db.Close() + + err = db.View(func(tx *bbolt.Tx) error { + for netName, netOpts := range opts.Networks { + network := n.networks[netName] + if network == nil { + return newIPAMError(nil, "could not find network %q", netName) + } + + // check if we have to alloc ips + if !requiresIPAMAlloc(network) { + continue + } + // get network bucket + netBkt := tx.Bucket([]byte(netName)) + if netBkt == nil { + return newIPAMError(nil, "failed to get network bucket for network %s", netName) + } + + idBkt := netBkt.Bucket(idBucketKey) + if idBkt == nil { + return newIPAMError(nil, "failed to get id bucket for network %s", netName) + } + + ipJSON := idBkt.Get([]byte(opts.ContainerID)) + if ipJSON == nil { + return newIPAMError(nil, "failed to get ips for container ID %s on network %s", opts.ContainerID, netName) + } + + // assignedIPs is the list of ips which should be used for this container + assignedIPs := make([]net.IP, 0, len(network.Subnets)) + + err = json.Unmarshal(ipJSON, &assignedIPs) + if err != nil { + return newIPAMError(err, "failed to unmarshal ips from database") + } + + for i := range assignedIPs { + util.NormalizeIP(&assignedIPs[i]) + } + + netOpts.StaticIPs = assignedIPs + opts.Networks[netName] = netOpts + } + return nil + }) + return err +} + +// deallocIPs will release the ips in the network options from the DB so that +// they can be reused by other containers. It expects that the network options +// are already filled with the correct ips. Use getAssignedIPs() for this. +func (n *netavarkNetwork) deallocIPs(opts *types.NetworkOptions) error { + db, err := n.openDB() + if err != nil { + return err + } + defer db.Close() + + err = db.Update(func(tx *bbolt.Tx) error { + for netName, netOpts := range opts.Networks { + network := n.networks[netName] + if network == nil { + return newIPAMError(nil, "could not find network %q", netName) + } + + // check if we have to alloc ips + if !requiresIPAMAlloc(network) { + continue + } + // get network bucket + netBkt := tx.Bucket([]byte(netName)) + if netBkt == nil { + return newIPAMError(nil, "failed to get network bucket for network %s", netName) + } + + for _, subnet := range network.Subnets { + subnetBkt := netBkt.Bucket([]byte(subnet.Subnet.String())) + if subnetBkt == nil { + return newIPAMError(nil, "failed to get subnet bucket for network %s", netName) + } + + // search for a static ip which matches the current subnet + // in this case the user wants this one and we should not assign a free one + var ip net.IP + for _, staticIP := range netOpts.StaticIPs { + if subnet.Subnet.Contains(staticIP) { + ip = staticIP + break + } + } + if ip == nil { + return newIPAMError(nil, "failed to find ip for subnet %s on network %s", subnet.Subnet.String(), netName) + } + util.NormalizeIP(&ip) + + err = subnetBkt.Delete(ip) + if err != nil { + return newIPAMError(err, "failed to remove ip %s from subnet bucket for network %s", ip.String(), netName) + } + } + + idBkt := netBkt.Bucket(idBucketKey) + if idBkt == nil { + return newIPAMError(nil, "failed to get id bucket for network %s", netName) + } + + err = idBkt.Delete([]byte(opts.ContainerID)) + if err != nil { + return newIPAMError(err, "failed to remove allocated ips for container ID %s on network %s", opts.ContainerID, netName) + } + } + return nil + }) + return err +} + +// requiresIPAMAlloc return true when we have to allocate ips for this network +// it checks the ipam driver and if subnets are set +func requiresIPAMAlloc(network *types.Network) bool { + // only do host allocation when driver is set to HostLocalIPAMDriver or unset + switch network.IPAMOptions["driver"] { + case "", types.HostLocalIPAMDriver: + default: + return false + } + + // no subnets == no ips to assign + return len(network.Subnets) > 0 +} diff --git a/libpod/network/netavark/ipam_test.go b/libpod/network/netavark/ipam_test.go new file mode 100644 index 000000000..4b3947501 --- /dev/null +++ b/libpod/network/netavark/ipam_test.go @@ -0,0 +1,433 @@ +package netavark + +import ( + "bytes" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + + "github.com/containers/podman/v3/libpod/network/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" +) + +var _ = Describe("IPAM", func() { + var ( + networkInterface *netavarkNetwork + networkConfDir string + logBuffer bytes.Buffer + ) + + BeforeEach(func() { + var err error + networkConfDir, err = ioutil.TempDir("", "podman_netavark_test") + if err != nil { + Fail("Failed to create tmpdir") + + } + logBuffer = bytes.Buffer{} + logrus.SetOutput(&logBuffer) + }) + + JustBeforeEach(func() { + libpodNet, err := NewNetworkInterface(InitConfig{ + NetworkConfigDir: networkConfDir, + IPAMDBPath: filepath.Join(networkConfDir, "ipam.db"), + LockFile: filepath.Join(networkConfDir, "netavark.lock"), + }) + if err != nil { + Fail("Failed to create NewCNINetworkInterface") + } + + networkInterface = libpodNet.(*netavarkNetwork) + // run network list to force a network load + networkInterface.NetworkList() + }) + + AfterEach(func() { + os.RemoveAll(networkConfDir) + }) + + It("simple ipam alloc", func() { + netName := types.DefaultNetworkName + for i := 2; i < 100; i++ { + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + err := networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.88.0.%d", i)).To4())) + } + }) + + It("ipam try to alloc same ip", func() { + netName := types.DefaultNetworkName + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + err := networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.88.0.2").To4())) + + opts = &types.NetworkOptions{ + ContainerID: "otherID", + Networks: map[string]types.PerNetworkOptions{ + netName: {StaticIPs: []net.IP{net.ParseIP("10.88.0.2")}}, + }, + } + err = networkInterface.allocIPs(opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID")) + }) + + It("ipam try to alloc more ips as in range", func() { + s, _ := types.ParseCIDR("10.0.0.1/24") + network, err := networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s, + LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP("10.0.0.10"), + EndIP: net.ParseIP("10.0.0.20"), + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + + for i := 10; i < 21; i++ { + opts := &types.NetworkOptions{ + ContainerID: fmt.Sprintf("someContainerID-%d", i), + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4())) + } + + opts := &types.NetworkOptions{ + ContainerID: "someContainerID-22", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + // now this should fail because all free ips are already assigned + err = networkInterface.allocIPs(opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.10 - 10.0.0.20")) + }) + + It("ipam basic setup", func() { + netName := types.DefaultNetworkName + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + expectedIP := net.ParseIP("10.88.0.2").To4() + + err := networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP)) + + // remove static ips from opts + netOpts := opts.Networks[netName] + netOpts.StaticIPs = nil + opts.Networks[netName] = netOpts + + err = networkInterface.getAssignedIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP)) + + err = networkInterface.allocIPs(opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID")) + + // dealloc the ip + err = networkInterface.deallocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP)) + }) + + It("ipam dual stack", func() { + s1, _ := types.ParseCIDR("10.0.0.0/26") + s2, _ := types.ParseCIDR("fd80::/24") + network, err := networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s1, + }, + { + Subnet: s2, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2"))) + + // remove static ips from opts + netOpts := opts.Networks[netName] + netOpts.StaticIPs = nil + opts.Networks[netName] = netOpts + + err = networkInterface.getAssignedIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2"))) + + err = networkInterface.deallocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + + // try to alloc the same again + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2"))) + }) + + It("ipam with two networks", func() { + s, _ := types.ParseCIDR("10.0.0.0/24") + network, err := networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName1 := network.Name + + s, _ = types.ParseCIDR("10.0.1.0/24") + network, err = networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName2 := network.Name + + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName1: {}, + netName2: {}, + }, + } + + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName1)) + Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks).To(HaveKey(netName2)) + Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4())) + + // remove static ips from opts + netOpts := opts.Networks[netName1] + netOpts.StaticIPs = nil + opts.Networks[netName1] = netOpts + netOpts = opts.Networks[netName2] + netOpts.StaticIPs = nil + opts.Networks[netName2] = netOpts + + err = networkInterface.getAssignedIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName1)) + Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks).To(HaveKey(netName2)) + Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4())) + + err = networkInterface.deallocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + + // try to alloc the same again + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName1)) + Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4())) + Expect(opts.Networks).To(HaveKey(netName2)) + Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4())) + }) + + It("ipam alloc more ips as in subnet", func() { + s, _ := types.ParseCIDR("10.0.0.0/26") + network, err := networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + + for i := 2; i < 64; i++ { + opts := &types.NetworkOptions{ + ContainerID: fmt.Sprintf("id-%d", i), + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + err = networkInterface.allocIPs(opts) + if i < 63 { + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4())) + } else { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.62")) + } + } + }) + + It("ipam alloc -> dealloc -> alloc", func() { + s, _ := types.ParseCIDR("10.0.0.0/27") + network, err := networkInterface.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + { + Subnet: s, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + + for i := 2; i < 10; i++ { + opts := types.NetworkOptions{ + ContainerID: fmt.Sprintf("id-%d", i), + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + err = networkInterface.allocIPs(&opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4())) + + err = networkInterface.deallocIPs(&opts) + Expect(err).ToNot(HaveOccurred()) + } + + for i := 0; i < 30; i++ { + opts := types.NetworkOptions{ + ContainerID: fmt.Sprintf("id-%d", i), + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + err = networkInterface.allocIPs(&opts) + if i < 29 { + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1)) + // The (i+8)%29+2 part looks cryptic but it is actually simple, we already have 8 ips allocated above + // so we expect the 8 available ip. We have 29 assignable ip addresses in this subnet because "i"+8 can + // be greater than 30 we have to modulo by 29 to go back to the beginning. Also the first free ip is + // network address + 2, so we have to add 2 to the result + Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", (i+8)%29+2)).To4())) + } else { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.30")) + } + } + }) + + It("ipam with dhcp driver should not set ips", func() { + network, err := networkInterface.NetworkCreate(types.Network{ + IPAMOptions: map[string]string{ + "driver": types.DHCPIPAMDriver, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + + opts := &types.NetworkOptions{ + ContainerID: "someContainerID", + Networks: map[string]types.PerNetworkOptions{ + netName: {}, + }, + } + + err = networkInterface.allocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0)) + + err = networkInterface.getAssignedIPs(opts) + Expect(err).ToNot(HaveOccurred()) + Expect(opts.Networks).To(HaveKey(netName)) + Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0)) + + // dealloc the ip + err = networkInterface.deallocIPs(opts) + Expect(err).ToNot(HaveOccurred()) + }) + +}) diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go new file mode 100644 index 000000000..6063a54e3 --- /dev/null +++ b/libpod/network/netavark/netavark_suite_test.go @@ -0,0 +1,75 @@ +// +build linux + +package netavark_test + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/containers/podman/v3/libpod/network/netavark" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + gomegaTypes "github.com/onsi/gomega/types" +) + +func TestNetavark(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Netavark Suite") +} + +var netavarkBinary string + +func init() { + netavarkBinary = os.Getenv("NETAVARK_BINARY") + if netavarkBinary == "" { + netavarkBinary = "/usr/libexec/podman/netavark" + } +} + +func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, error) { + return netavark.NewNetworkInterface(netavark.InitConfig{ + NetworkConfigDir: confDir, + IsMachine: machine, + NetavarkBinary: netavarkBinary, + IPAMDBPath: filepath.Join(confDir, "ipam.db"), + LockFile: filepath.Join(confDir, "netavark.lock"), + }) +} + +// EqualSubnet is a custom GomegaMatcher to match a subnet +// This makes sure to not use the 16 bytes ip representation. +func EqualSubnet(subnet *net.IPNet) gomegaTypes.GomegaMatcher { + return &equalSubnetMatcher{ + expected: subnet, + } +} + +type equalSubnetMatcher struct { + expected *net.IPNet +} + +func (m *equalSubnetMatcher) Match(actual interface{}) (bool, error) { + util.NormalizeIP(&m.expected.IP) + + subnet, ok := actual.(*net.IPNet) + if !ok { + return false, fmt.Errorf("EqualSubnet expects a *net.IPNet") + } + util.NormalizeIP(&subnet.IP) + + return reflect.DeepEqual(subnet, m.expected), nil +} + +func (m *equalSubnetMatcher) FailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected subnet %#v to equal subnet %#v", actual, m.expected) +} + +func (m *equalSubnetMatcher) NegatedFailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected subnet %#v not to equal subnet %#v", actual, m.expected) +} diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go new file mode 100644 index 000000000..cc6fb423c --- /dev/null +++ b/libpod/network/netavark/network.go @@ -0,0 +1,305 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + pkgutil "github.com/containers/podman/v3/pkg/util" + "github.com/containers/storage/pkg/lockfile" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type netavarkNetwork struct { + // networkConfigDir is directory where the network config files are stored. + networkConfigDir string + + // netavarkBinary is the path to the netavark binary. + netavarkBinary string + + // defaultNetwork is the name for the default network. + defaultNetwork string + // defaultSubnet is the default subnet for the default network. + defaultSubnet types.IPNet + + // ipamDBPath is the path to the ip allocation bolt db + ipamDBPath string + + // isMachine describes whenever podman runs in a podman machine environment. + isMachine bool + + // lock is a internal lock for critical operations + lock lockfile.Locker + + // modTime is the timestamp when the config dir was modified + modTime time.Time + + // networks is a map with loaded networks, the key is the network name + networks map[string]*types.Network +} + +type InitConfig struct { + // NetworkConfigDir is directory where the network config files are stored. + NetworkConfigDir string + + // NetavarkBinary is the path to the netavark binary. + NetavarkBinary string + + // IPAMDBPath is the path to the ipam database. This should be on a tmpfs. + // If empty defaults to XDG_RUNTIME_DIR/netavark/ipam.db or /run/netavark/ipam.db as root. + IPAMDBPath string + + // DefaultNetwork is the name for the default network. + DefaultNetwork string + // DefaultSubnet is the default subnet for the default network. + DefaultSubnet string + + // IsMachine describes whenever podman runs in a podman machine environment. + IsMachine bool + + // LockFile is the path to lock file. + LockFile string +} + +// NewNetworkInterface creates the ContainerNetwork interface for the netavark backend. +// Note: The networks are not loaded from disk until a method is called. +func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) { + // TODO: consider using a shared memory lock + lock, err := lockfile.GetLockfile(conf.LockFile) + if err != nil { + return nil, err + } + + defaultNetworkName := conf.DefaultNetwork + if defaultNetworkName == "" { + defaultNetworkName = types.DefaultNetworkName + } + + defaultSubnet := conf.DefaultSubnet + if defaultSubnet == "" { + defaultSubnet = types.DefaultSubnet + } + defaultNet, err := types.ParseCIDR(defaultSubnet) + if err != nil { + return nil, errors.Wrap(err, "failed to parse default subnet") + } + + ipamdbPath := conf.IPAMDBPath + if ipamdbPath == "" { + runDir, err := pkgutil.GetRuntimeDir() + if err != nil { + return nil, err + } + // as root runtimeDir is empty so use /run + if runDir == "" { + runDir = "/run" + } + ipamdbPath = filepath.Join(runDir, "netavark") + if err := os.MkdirAll(ipamdbPath, 0700); err != nil { + return nil, errors.Wrap(err, "failed to create ipam db path") + } + ipamdbPath = filepath.Join(ipamdbPath, "ipam.db") + } + + if err := os.MkdirAll(conf.NetworkConfigDir, 0755); err != nil { + return nil, err + } + + n := &netavarkNetwork{ + networkConfigDir: conf.NetworkConfigDir, + netavarkBinary: conf.NetavarkBinary, + ipamDBPath: ipamdbPath, + defaultNetwork: defaultNetworkName, + defaultSubnet: defaultNet, + isMachine: conf.IsMachine, + lock: lock, + } + + return n, nil +} + +// Drivers will return the list of supported network drivers +// for this interface. +func (n *netavarkNetwork) Drivers() []string { + return []string{types.BridgeNetworkDriver} +} + +func (n *netavarkNetwork) loadNetworks() error { + // check the mod time of the config dir + f, err := os.Stat(n.networkConfigDir) + if err != nil { + return err + } + modTime := f.ModTime() + + // skip loading networks if they are already loaded and + // if the config dir was not modified since the last call + if n.networks != nil && modTime.Equal(n.modTime) { + return nil + } + // make sure the remove all networks before we reload them + n.networks = nil + n.modTime = modTime + + files, err := ioutil.ReadDir(n.networkConfigDir) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + networks := make(map[string]*types.Network, len(files)) + for _, f := range files { + if f.IsDir() { + continue + } + if filepath.Ext(f.Name()) != ".json" { + continue + } + + path := filepath.Join(n.networkConfigDir, f.Name()) + file, err := os.Open(path) + if err != nil { + // do not log ENOENT errors + if !errors.Is(err, os.ErrNotExist) { + logrus.Warnf("Error loading network config file %q: %v", path, err) + } + continue + } + network := new(types.Network) + err = json.NewDecoder(file).Decode(network) + if err != nil { + logrus.Warnf("Error reading network config file %q: %v", path, err) + continue + } + + // check that the filename matches the network name + if network.Name+".json" != f.Name() { + logrus.Warnf("Network config name %q does not match file name %q, skipping", network.Name, f.Name()) + continue + } + + if !define.NameRegex.MatchString(network.Name) { + logrus.Warnf("Network config %q has invalid name: %q, skipping: %v", path, network.Name, define.RegexError) + continue + } + + err = parseNetwork(network) + if err != nil { + logrus.Warnf("Network config %q could not be parsed, skipping: %v", path, err) + continue + } + + logrus.Debugf("Successfully loaded network %s: %v", network.Name, network) + networks[network.Name] = network + } + + // create the default network in memory if it did not exists on disk + if networks[n.defaultNetwork] == nil { + networkInfo, err := n.createDefaultNetwork() + if err != nil { + return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork) + } + networks[n.defaultNetwork] = networkInfo + } + logrus.Debugf("Successfully loaded %d networks", len(networks)) + n.networks = networks + return nil +} + +func parseNetwork(network *types.Network) error { + if network.Labels == nil { + network.Labels = map[string]string{} + } + if network.Options == nil { + network.Options = map[string]string{} + } + if network.IPAMOptions == nil { + network.IPAMOptions = map[string]string{} + } + + if len(network.ID) != 64 { + return errors.Errorf("invalid network ID %q", network.ID) + } + + return util.ValidateSubnets(network, nil) +} + +func (n *netavarkNetwork) createDefaultNetwork() (*types.Network, error) { + net := types.Network{ + Name: n.defaultNetwork, + NetworkInterface: defaultBridgeName + "0", + // Important do not change this ID + ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + Driver: types.BridgeNetworkDriver, + Subnets: []types.Subnet{ + {Subnet: n.defaultSubnet}, + }, + } + return n.networkCreate(net, true) +} + +// getNetwork will lookup a network by name or ID. It returns an +// error when no network was found or when more than one network +// with the given (partial) ID exists. +// getNetwork will read from the networks map, therefore the caller +// must ensure that n.lock is locked before using it. +func (n *netavarkNetwork) getNetwork(nameOrID string) (*types.Network, error) { + // fast path check the map key, this will only work for names + if val, ok := n.networks[nameOrID]; ok { + return val, nil + } + // If there was no match we might got a full or partial ID. + var net *types.Network + for _, val := range n.networks { + // This should not happen because we already looked up the map by name but check anyway. + if val.Name == nameOrID { + return val, nil + } + + if strings.HasPrefix(val.ID, nameOrID) { + if net != nil { + return nil, errors.Errorf("more than one result for network ID %s", nameOrID) + } + net = val + } + } + if net != nil { + return net, nil + } + return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID) +} + +// Implement the NetUtil interface for easy code sharing with other network interfaces. + +// ForEach call the given function for each network +func (n *netavarkNetwork) ForEach(run func(types.Network)) { + for _, val := range n.networks { + run(*val) + } +} + +// Len return the number of networks +func (n *netavarkNetwork) Len() int { + return len(n.networks) +} + +// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number. +func (n *netavarkNetwork) DefaultInterfaceName() string { + return defaultBridgeName +} + +func (n *netavarkNetwork) Network(nameOrID string) (*types.Network, error) { + network, err := n.getNetwork(nameOrID) + if err != nil { + return nil, err + } + return network, nil +} diff --git a/libpod/network/netavark/run.go b/libpod/network/netavark/run.go new file mode 100644 index 000000000..2f839151e --- /dev/null +++ b/libpod/network/netavark/run.go @@ -0,0 +1,119 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "fmt" + + "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type netavarkOptions struct { + types.NetworkOptions + Networks map[string]*types.Network `json:"network_info"` +} + +// Setup will setup the container network namespace. It returns +// a map of StatusBlocks, the key is the network name. +func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions) (map[string]types.StatusBlock, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return nil, err + } + + err = util.ValidateSetupOptions(n, namespacePath, options) + if err != nil { + return nil, err + } + + // allocate IPs in the IPAM db + err = n.allocIPs(&options.NetworkOptions) + if err != nil { + return nil, err + } + + netavarkOpts, err := n.convertNetOpts(options.NetworkOptions) + if err != nil { + return nil, errors.Wrap(err, "failed to convert net opts") + } + + // trace output to get the json + if logrus.IsLevelEnabled(logrus.TraceLevel) { + b, err := json.Marshal(&netavarkOpts) + if err != nil { + return nil, err + } + // show the full netavark command so we can easily reproduce errors from the cli + logrus.Tracef("netavark command: printf '%s' | %s setup %s", string(b), n.netavarkBinary, namespacePath) + } + + result := map[string]types.StatusBlock{} + err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, &result) + + if len(result) != len(options.Networks) { + logrus.Errorf("unexpected netavark result: %v", result) + return nil, fmt.Errorf("unexpected netavark result length, want (%d), got (%d) networks", len(options.Networks), len(result)) + } + + return result, err +} + +// Teardown will teardown the container network namespace. +func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownOptions) error { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return err + } + + // get IPs from the IPAM db + err = n.getAssignedIPs(&options.NetworkOptions) + if err != nil { + // when there is an error getting the ips we should still continue + // to call teardown for netavark to prevent leaking network interfaces + logrus.Error(err) + } + + netavarkOpts, err := n.convertNetOpts(options.NetworkOptions) + if err != nil { + return errors.Wrap(err, "failed to convert net opts") + } + + retErr := execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil) + + // when netavark returned an error we still free the used ips + // otherwise we could end up in a state where block the ips forever + err = n.deallocIPs(&netavarkOpts.NetworkOptions) + if err != nil { + if retErr != nil { + logrus.Error(err) + } else { + retErr = err + } + } + + return retErr +} + +func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, error) { + netavarkOptions := netavarkOptions{ + NetworkOptions: opts, + Networks: make(map[string]*types.Network, len(opts.Networks)), + } + + for network := range opts.Networks { + net, err := n.getNetwork(network) + if err != nil { + return nil, err + } + netavarkOptions.Networks[network] = net + } + return &netavarkOptions, nil +} diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go new file mode 100644 index 000000000..3279203cc --- /dev/null +++ b/libpod/network/netavark/run_test.go @@ -0,0 +1,693 @@ +// +build linux + +package netavark_test + +// The tests have to be run as root. +// For each test there will be two network namespaces created, +// netNSTest and netNSContainer. Each test must be run inside +// netNSTest to prevent leakage in the host netns, therefore +// it should use the following structure: +// It("test name", func() { +// runTest(func() { +// // add test logic here +// }) +// }) + +import ( + "io/ioutil" + "net" + "os" + "strconv" + "sync" + "time" + + "github.com/containernetworking/plugins/pkg/ns" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + "github.com/containers/podman/v3/pkg/netns" + "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/storage/pkg/stringid" +) + +var _ = Describe("run netavark", func() { + var ( + libpodNet types.ContainerNetwork + confDir string + netNSTest ns.NetNS + netNSContainer ns.NetNS + ) + + // runTest is a helper function to run a test. It ensures that each test + // is run in its own netns. It also creates a mountns to mount a tmpfs to /var/lib/cni. + runTest := func(run func()) { + netNSTest.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + // we have to setup the loopback adapter in this netns to use port forwarding + link, err := netlink.LinkByName("lo") + Expect(err).To(BeNil(), "Failed to get loopback adapter") + err = netlink.LinkSetUp(link) + Expect(err).To(BeNil(), "Failed to set loopback adapter up") + run() + return nil + }) + } + + BeforeEach(func() { + if _, ok := os.LookupEnv("NETAVARK_BINARY"); !ok { + Skip("NETAVARK_BINARY not set skip run tests") + } + + // set the logrus settings + logrus.SetLevel(logrus.TraceLevel) + // disable extra quotes so we can easily copy the netavark command + logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true}) + logrus.SetOutput(os.Stderr) + // The tests need root privileges. + // Technically we could work around that by using user namespaces and + // the rootless cni code but this is to much work to get it right for a unit test. + if rootless.IsRootless() { + Skip("this test needs to be run as root") + } + + var err error + confDir, err = ioutil.TempDir("", "podman_netavark_test") + if err != nil { + Fail("Failed to create tmpdir") + } + + netNSTest, err = netns.NewNS() + if err != nil { + Fail("Failed to create netns") + } + + netNSContainer, err = netns.NewNS() + if err != nil { + Fail("Failed to create netns") + } + }) + + JustBeforeEach(func() { + var err error + libpodNet, err = getNetworkInterface(confDir, false) + if err != nil { + Fail("Failed to create NewCNINetworkInterface") + } + }) + + AfterEach(func() { + logrus.SetFormatter(&logrus.TextFormatter{}) + logrus.SetLevel(logrus.InfoLevel) + os.RemoveAll(confDir) + + netns.UnmountNS(netNSTest) + netNSTest.Close() + + netns.UnmountNS(netNSContainer) + netNSContainer.Close() + }) + + It("test basic setup", func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + opts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: "someID", + ContainerName: "someName", + Networks: map[string]types.PerNetworkOptions{ + defNet: { + InterfaceName: intName, + }, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), opts) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + ip := res[defNet].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip.String()).To(ContainSubstring("10.88.0.")) + gw := res[defNet].Interfaces[intName].Networks[0].Gateway + util.NormalizeIP(&gw) + Expect(gw.String()).To(Equal("10.88.0.1")) + macAddress := res[defNet].Interfaces[intName].MacAddress + Expect(macAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + + // check in the container namespace if the settings are applied + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + i, err := net.InterfaceByName(intName) + Expect(err).To(BeNil()) + Expect(i.Name).To(Equal(intName)) + Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macAddress))) + addrs, err := i.Addrs() + Expect(err).To(BeNil()) + subnet := &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(16, 32), + } + Expect(addrs).To(ContainElements(EqualSubnet(subnet))) + + // check loopback adapter + i, err = net.InterfaceByName("lo") + Expect(err).To(BeNil()) + Expect(i.Name).To(Equal("lo")) + Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) + Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") + return nil + }) + Expect(err).To(BeNil()) + + // default bridge name + bridgeName := "podman0" + // check settings on the host side + i, err := net.InterfaceByName(bridgeName) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(bridgeName)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + // test that the gateway ip is assigned to the interface + subnet := &net.IPNet{ + IP: gw, + Mask: net.CIDRMask(16, 32), + } + Expect(addrs).To(ContainElements(EqualSubnet(subnet))) + + wg := &sync.WaitGroup{} + expected := stringid.GenerateNonCryptoID() + // now check ip connectivity + err = netNSContainer.Do(func(_ ns.NetNS) error { + wg.Add(1) + runNetListener(wg, "tcp", "0.0.0.0", 5000, expected) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + conn, err := net.Dial("tcp", ip.String()+":5000") + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(expected)) + Expect(err).To(BeNil()) + conn.Close() + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts)) + Expect(err).ToNot(HaveOccurred()) + wg.Wait() + }) + }) + + It("setup two containers", func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + setupOpts1 := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts1) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + ip1 := res[defNet].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip1.String()).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + + setupOpts2 := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + + netNSContainer2, err := netns.NewNS() + Expect(err).ToNot(HaveOccurred()) + defer netns.UnmountNS(netNSContainer2) + defer netNSContainer2.Close() + + res, err = libpodNet.Setup(netNSContainer2.Path(), setupOpts2) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + ip2 := res[defNet].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip2.String()).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + Expect(ip1.Equal(ip2)).To(BeFalse(), "IP1 %s should not be equal to IP2 %s", ip1.String(), ip2.String()) + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts1)) + Expect(err).ToNot(HaveOccurred()) + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts2)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("setup dualstack network", func() { + runTest(func() { + s1, _ := types.ParseCIDR("10.0.0.1/24") + s2, _ := types.ParseCIDR("fd10:88:a::/64") + network, err := libpodNet.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + {Subnet: s1}, {Subnet: s2}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName := network.Name + intName := "eth0" + + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + Networks: map[string]types.PerNetworkOptions{ + netName: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(netName)) + Expect(res[netName].Interfaces).To(HaveKey(intName)) + Expect(res[netName].Interfaces[intName].Networks).To(HaveLen(2)) + ip1 := res[netName].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip1.String()).To(ContainSubstring("10.0.0.")) + gw1 := res[netName].Interfaces[intName].Networks[0].Gateway + Expect(gw1.String()).To(Equal("10.0.0.1")) + ip2 := res[netName].Interfaces[intName].Networks[1].Subnet.IP + Expect(ip2.String()).To(ContainSubstring("fd10:88:a::")) + gw2 := res[netName].Interfaces[intName].Networks[0].Gateway + Expect(gw2.String()).To(Equal("fd10:88:a::1")) + Expect(res[netName].Interfaces[intName].MacAddress).To(HaveLen(6)) + + // check in the container namespace if the settings are applied + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + i, err := net.InterfaceByName(intName) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(intName)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + subnet1 := s1.IPNet + subnet1.IP = ip1 + subnet2 := s2.IPNet + subnet2.IP = ip2 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet1), EqualSubnet(&subnet2))) + + // check loopback adapter + i, err = net.InterfaceByName("lo") + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal("lo")) + Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) + Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + bridgeName := network.NetworkInterface + // check settings on the host side + i, err := net.InterfaceByName(bridgeName) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(bridgeName)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + // test that the gateway ip is assigned to the interface + subnet1 := s1.IPNet + subnet1.IP = gw1 + subnet2 := s2.IPNet + subnet2.IP = gw2 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet1), EqualSubnet(&subnet2))) + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("setup two networks", func() { + runTest(func() { + s1, _ := types.ParseCIDR("10.0.0.1/24") + network1, err := libpodNet.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + {Subnet: s1}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName1 := network1.Name + intName1 := "eth0" + + s2, _ := types.ParseCIDR("10.1.0.0/24") + network2, err := libpodNet.NetworkCreate(types.Network{ + Subnets: []types.Subnet{ + {Subnet: s2}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + netName2 := network2.Name + intName2 := "eth1" + + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + Networks: map[string]types.PerNetworkOptions{ + netName1: {InterfaceName: intName1}, + netName2: {InterfaceName: intName2}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(2)) + Expect(res).To(HaveKey(netName1)) + Expect(res).To(HaveKey(netName2)) + Expect(res[netName1].Interfaces).To(HaveKey(intName1)) + Expect(res[netName2].Interfaces).To(HaveKey(intName2)) + Expect(res[netName1].Interfaces[intName1].Networks).To(HaveLen(1)) + ip1 := res[netName1].Interfaces[intName1].Networks[0].Subnet.IP + Expect(ip1.String()).To(ContainSubstring("10.0.0.")) + gw1 := res[netName1].Interfaces[intName1].Networks[0].Gateway + Expect(gw1.String()).To(Equal("10.0.0.1")) + ip2 := res[netName2].Interfaces[intName2].Networks[0].Subnet.IP + Expect(ip2.String()).To(ContainSubstring("10.1.0.")) + gw2 := res[netName2].Interfaces[intName2].Networks[0].Gateway + Expect(gw2.String()).To(Equal("10.1.0.1")) + mac1 := res[netName1].Interfaces[intName1].MacAddress + Expect(mac1).To(HaveLen(6)) + mac2 := res[netName2].Interfaces[intName2].MacAddress + Expect(mac2).To(HaveLen(6)) + + // check in the container namespace if the settings are applied + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + i, err := net.InterfaceByName(intName1) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(intName1)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + subnet1 := s1.IPNet + subnet1.IP = ip1 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet1))) + + i, err = net.InterfaceByName(intName2) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(intName2)) + addrs, err = i.Addrs() + Expect(err).ToNot(HaveOccurred()) + subnet2 := s2.IPNet + subnet2.IP = ip2 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet2))) + + // check loopback adapter + i, err = net.InterfaceByName("lo") + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal("lo")) + Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) + Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + bridgeName1 := network1.NetworkInterface + // check settings on the host side + i, err := net.InterfaceByName(bridgeName1) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(bridgeName1)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + // test that the gateway ip is assigned to the interface + subnet1 := s1.IPNet + subnet1.IP = gw1 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet1))) + + bridgeName2 := network2.NetworkInterface + // check settings on the host side + i, err = net.InterfaceByName(bridgeName2) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(bridgeName2)) + addrs, err = i.Addrs() + Expect(err).ToNot(HaveOccurred()) + // test that the gateway ip is assigned to the interface + subnet2 := s2.IPNet + subnet2.IP = gw2 + Expect(addrs).To(ContainElements(EqualSubnet(&subnet2))) + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + for _, proto := range []string{"tcp", "udp"} { + // copy proto to extra var to keep correct references in the goroutines + protocol := proto + It("run with exposed ports protocol "+protocol, func() { + runTest(func() { + testdata := stringid.GenerateNonCryptoID() + defNet := types.DefaultNetworkName + intName := "eth0" + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + PortMappings: []types.PortMapping{{ + Protocol: protocol, + HostIP: "127.0.0.1", + HostPort: 5000, + ContainerPort: 5000, + }}, + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).To(BeNil()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + var wg sync.WaitGroup + wg.Add(1) + // start a listener in the container ns + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata) + return nil + }) + Expect(err).To(BeNil()) + + conn, err := net.Dial(protocol, "127.0.0.1:5000") + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(testdata)) + Expect(err).To(BeNil()) + conn.Close() + + // wait for the listener to finish + wg.Wait() + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).To(BeNil()) + }) + }) + + It("run with range ports protocol "+protocol, func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + PortMappings: []types.PortMapping{{ + Protocol: protocol, + HostIP: "127.0.0.1", + HostPort: 5001, + ContainerPort: 5000, + Range: 3, + }}, + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).To(BeNil()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + containerIP := res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String() + Expect(containerIP).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + + // loop over all ports + for p := 5001; p < 5004; p++ { + port := p + var wg sync.WaitGroup + wg.Add(1) + testdata := stringid.GenerateNonCryptoID() + // start a listener in the container ns + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + runNetListener(&wg, protocol, containerIP, port-1, testdata) + return nil + }) + Expect(err).To(BeNil()) + + conn, err := net.Dial(protocol, net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(testdata)) + Expect(err).To(BeNil()) + conn.Close() + + // wait for the listener to finish + wg.Wait() + } + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).To(BeNil()) + }) + }) + } + + It("simple teardown", func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + opts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: "someID", + ContainerName: "someName", + Networks: map[string]types.PerNetworkOptions{ + defNet: { + InterfaceName: intName, + }, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), opts) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + ip := res[defNet].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip.String()).To(ContainSubstring("10.88.0.")) + gw := res[defNet].Interfaces[intName].Networks[0].Gateway + Expect(gw.String()).To(Equal("10.88.0.1")) + macAddress := res[defNet].Interfaces[intName].MacAddress + Expect(macAddress).To(HaveLen(6)) + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts)) + Expect(err).ToNot(HaveOccurred()) + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + // check that the container interface is removed + _, err := net.InterfaceByName(intName) + Expect(err).To(HaveOccurred()) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + // default bridge name + bridgeName := "podman0" + // check that bridge interface was removed + _, err = net.InterfaceByName(bridgeName) + Expect(err).To(HaveOccurred()) + }) + }) + + It("test netavark error", func() { + runTest(func() { + intName := "eth0" + err := netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + + attr := netlink.NewLinkAttrs() + attr.Name = "eth0" + err := netlink.LinkAdd(&netlink.Bridge{LinkAttrs: attr}) + Expect(err).ToNot(HaveOccurred()) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + defNet := types.DefaultNetworkName + opts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: "someID", + ContainerName: "someName", + Networks: map[string]types.PerNetworkOptions{ + defNet: { + InterfaceName: intName, + }, + }, + }, + } + _, err = libpodNet.Setup(netNSContainer.Path(), opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("interface eth0 already exists on container namespace")) + }) + }) +}) + +func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) { + switch protocol { + case "tcp": + ln, err := net.Listen(protocol, net.JoinHostPort(ip, strconv.Itoa(port))) + Expect(err).To(BeNil()) + // make sure to read in a separate goroutine to not block + go func() { + defer GinkgoRecover() + defer wg.Done() + defer ln.Close() + conn, err := ln.Accept() + Expect(err).To(BeNil()) + defer conn.Close() + conn.SetDeadline(time.Now().Add(1 * time.Second)) + data, err := ioutil.ReadAll(conn) + Expect(err).To(BeNil()) + Expect(string(data)).To(Equal(expectedData)) + }() + case "udp": + conn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: net.ParseIP(ip), + Port: port, + }) + Expect(err).To(BeNil()) + conn.SetDeadline(time.Now().Add(1 * time.Second)) + go func() { + defer GinkgoRecover() + defer wg.Done() + defer conn.Close() + data := make([]byte, len(expectedData)) + i, err := conn.Read(data) + Expect(err).To(BeNil()) + Expect(i).To(Equal(len(expectedData))) + Expect(string(data)).To(Equal(expectedData)) + }() + default: + Fail("unsupported protocol") + } +} diff --git a/libpod/network/netavark/testfiles/invalid/broken.json b/libpod/network/netavark/testfiles/invalid/broken.json new file mode 100644 index 000000000..8968ddc73 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/broken.json @@ -0,0 +1,16 @@ +{ + "name": "bridge", + "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1", + "lease_range": { + "start_ip": "10.89.8.20", + "end_ip": "10.89.8.50" + } + } + ], diff --git a/libpod/network/netavark/testfiles/invalid/invalid name.json b/libpod/network/netavark/testfiles/invalid/invalid name.json new file mode 100644 index 000000000..02b441279 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/invalid name.json @@ -0,0 +1,19 @@ +{ + "name": "invalid name", + "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/invalid_gateway.json b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json new file mode 100644 index 000000000..6e3a83156 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json @@ -0,0 +1,19 @@ +{ + "name": "invalid_gateway", + "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.9.0/24", + "gateway": "10.89.100.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/name_missmatch.json b/libpod/network/netavark/testfiles/invalid/name_missmatch.json new file mode 100644 index 000000000..a3142d8bb --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/name_missmatch.json @@ -0,0 +1,19 @@ +{ + "name": "name_miss", + "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", + "driver": "bridge", + "network_interface": "podman8", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.7.0/24", + "gateway": "10.89.7.1" + } + ], + "ipv6_enabled": false, + "internal": true, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/wrongID.json b/libpod/network/netavark/testfiles/invalid/wrongID.json new file mode 100644 index 000000000..7c1446306 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/wrongID.json @@ -0,0 +1,19 @@ +{ + "name": "wrongID", + "id": "someID", + "driver": "bridge", + "network_interface": "podman1", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.0.0/24", + "gateway": "10.89.0.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/bridge.json b/libpod/network/netavark/testfiles/valid/bridge.json new file mode 100644 index 000000000..f4ec82188 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/bridge.json @@ -0,0 +1,23 @@ +{ + "name": "bridge", + "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1", + "lease_range": { + "start_ip": "10.89.8.20", + "end_ip": "10.89.8.50" + } + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/dualstack.json b/libpod/network/netavark/testfiles/valid/dualstack.json new file mode 100644 index 000000000..bb4168f3a --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/dualstack.json @@ -0,0 +1,23 @@ +{ + "name": "dualstack", + "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889", + "driver": "bridge", + "network_interface": "podman21", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "fd10:88:a::/64", + "gateway": "fd10:88:a::1" + }, + { + "subnet": "10.89.19.0/24", + "gateway": "10.89.19.10" + } + ], + "ipv6_enabled": true, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/internal.json b/libpod/network/netavark/testfiles/valid/internal.json new file mode 100644 index 000000000..3ccdd3889 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/internal.json @@ -0,0 +1,18 @@ +{ + "name": "internal", + "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", + "driver": "bridge", + "network_interface": "podman8", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.7.0/24" + } + ], + "ipv6_enabled": false, + "internal": true, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/label.json b/libpod/network/netavark/testfiles/valid/label.json new file mode 100644 index 000000000..c4ed637ec --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/label.json @@ -0,0 +1,22 @@ +{ + "name": "label", + "id": "1aca80e8b55c802f7b43740da2990e1b5735bbb323d93eb5ebda8395b04025e2", + "driver": "bridge", + "network_interface": "podman15", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.13.0/24", + "gateway": "10.89.13.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "labels": { + "mykey": "value" + }, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/mtu.json b/libpod/network/netavark/testfiles/valid/mtu.json new file mode 100644 index 000000000..53fa4c9bc --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/mtu.json @@ -0,0 +1,22 @@ +{ + "name": "mtu", + "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686", + "driver": "bridge", + "network_interface": "podman13", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.11.0/24", + "gateway": "10.89.11.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "options": { + "mtu": "1500" + }, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/podman.json b/libpod/network/netavark/testfiles/valid/podman.json new file mode 100644 index 000000000..19acddc83 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/podman.json @@ -0,0 +1,19 @@ +{ + "name": "podman", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + "driver": "bridge", + "network_interface": "podman0", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.88.0.0/16", + "gateway": "10.88.0.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/vlan.json b/libpod/network/netavark/testfiles/valid/vlan.json new file mode 100644 index 000000000..30c88ec49 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/vlan.json @@ -0,0 +1,22 @@ +{ + "name": "vlan", + "id": "c3b258168c41c0bce97616716bef315eeed33eb1142904bfe7f32eb392c7cf80", + "driver": "bridge", + "network_interface": "podman14", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.12.0/24", + "gateway": "10.89.12.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "options": { + "vlan": "5" + }, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go index b2ba92735..e82b4a781 100644 --- a/libpod/network/util/ip.go +++ b/libpod/network/util/ip.go @@ -1,10 +1,7 @@ package util import ( - "crypto/rand" "net" - - "github.com/pkg/errors" ) // IsIPv6 returns true if netIP is IPv6. @@ -17,40 +14,6 @@ func IsIPv4(netIP net.IP) bool { return netIP != nil && netIP.To4() != nil } -func incByte(subnet *net.IPNet, idx int, shift uint) error { - if idx < 0 { - return errors.New("no more subnets left") - } - if subnet.IP[idx] == 255 { - subnet.IP[idx] = 0 - return incByte(subnet, idx-1, 0) - } - subnet.IP[idx] += 1 << shift - return nil -} - -// NextSubnet returns subnet incremented by 1 -func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { - newSubnet := &net.IPNet{ - IP: subnet.IP, - Mask: subnet.Mask, - } - ones, bits := newSubnet.Mask.Size() - if ones == 0 { - return nil, errors.Errorf("%s has only one subnet", subnet.String()) - } - zeroes := uint(bits - ones) - shift := zeroes % 8 - idx := ones/8 - 1 - if idx < 0 { - idx = 0 - } - if err := incByte(newSubnet, idx, shift); err != nil { - return nil, err - } - return newSubnet, nil -} - // LastIPInSubnet gets the last IP in a subnet func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer // re-parse to ensure clean network address @@ -84,30 +47,10 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer return cidr.IP, nil } -func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool { - for _, nw := range networklist { - if networkIntersect(n, nw) { - return true - } - } - return false -} - -func networkIntersect(n1, n2 *net.IPNet) bool { - return n2.Contains(n1.IP) || n1.Contains(n2.IP) -} - -// GetRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879. -func GetRandomIPv6Subnet() (net.IPNet, error) { - ip := make(net.IP, 8, net.IPv6len) - // read 8 random bytes - _, err := rand.Read(ip) - if err != nil { - return net.IPNet{}, nil +// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible +func NormalizeIP(ip *net.IP) { + ipv4 := ip.To4() + if ipv4 != nil { + *ip = ipv4 } - // first byte must be FD as per RFC3879 - ip[0] = 0xfd - // add 8 zero bytes - ip = append(ip, make([]byte, 8)...) - return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil } diff --git a/libpod/network/util/ip_calc.go b/libpod/network/util/ip_calc.go new file mode 100644 index 000000000..a27ddf78b --- /dev/null +++ b/libpod/network/util/ip_calc.go @@ -0,0 +1,53 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "math/big" + "net" +) + +// NextIP returns IP incremented by 1 +func NextIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Add(i, big.NewInt(1))) +} + +// PrevIP returns IP decremented by 1 +func PrevIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Sub(i, big.NewInt(1))) +} + +// Cmp compares two IPs, returning the usual ordering: +// a < b : -1 +// a == b : 0 +// a > b : 1 +func Cmp(a, b net.IP) int { + aa := ipToInt(a) + bb := ipToInt(b) + return aa.Cmp(bb) +} + +func ipToInt(ip net.IP) *big.Int { + if v := ip.To4(); v != nil { + return big.NewInt(0).SetBytes(v) + } + return big.NewInt(0).SetBytes(ip.To16()) +} + +func intToIP(i *big.Int) net.IP { + return net.IP(i.Bytes()) +} diff --git a/libpod/network/util/ip_test.go b/libpod/network/util/ip_test.go index c26ad140a..63ac555f0 100644 --- a/libpod/network/util/ip_test.go +++ b/libpod/network/util/ip_test.go @@ -1,9 +1,7 @@ package util import ( - "fmt" "net" - "reflect" "testing" ) @@ -12,34 +10,6 @@ func parseCIDR(n string) *net.IPNet { return parsedNet } -func TestNextSubnet(t *testing.T) { - type args struct { - subnet *net.IPNet - } - tests := []struct { - name string - args args - want *net.IPNet - wantErr bool - }{ - {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, - {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, - } - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - got, err := NextSubnet(test.args.subnet) - if (err != nil) != test.wantErr { - t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) - return - } - if !reflect.DeepEqual(got, test.want) { - t.Errorf("NextSubnet() got = %v, want %v", got, test.want) - } - }) - } -} - func TestFirstIPInSubnet(t *testing.T) { tests := []struct { name string @@ -101,25 +71,3 @@ func TestLastIPInSubnet(t *testing.T) { }) } } - -func TestGetRandomIPv6Subnet(t *testing.T) { - for i := 0; i < 1000; i++ { - t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) { - sub, err := GetRandomIPv6Subnet() - if err != nil { - t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err) - return - } - if sub.IP.To4() != nil { - t.Errorf("ip %s is not an ipv6 address", sub.IP) - } - if sub.IP[0] != 0xfd { - t.Errorf("ipv6 %s does not start with fd", sub.IP) - } - ones, bytes := sub.Mask.Size() - if ones != 64 || bytes != 128 { - t.Errorf("wrong network mask %v, it should be /64", sub.Mask) - } - }) - } -} diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 035fb5832..fc91155fa 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "regexp" + "sort" "strconv" "strings" "syscall" @@ -91,10 +92,7 @@ func (c *Container) getNetworkOptions() (types.NetworkOptions, error) { ContainerID: c.config.ID, ContainerName: getCNIPodName(c), } - // TODO remove ocicni PortMappings from container config and store as types PortMappings - if len(c.config.PortMappings) > 0 { - opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings) - } + opts.PortMappings = c.config.PortMappings networks, _, err := c.networks() if err != nil { return opts, err @@ -653,6 +651,9 @@ func getCNIPodName(c *Container) string { // Create and configure a new network namespace for a container func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (map[string]types.StatusBlock, error) { + if ctr.config.NetMode.IsSlirp4netns() { + return nil, r.setupSlirp4netns(ctr, ctrNS) + } networks, _, err := ctr.networks() if err != nil { return nil, err @@ -667,7 +668,24 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (map[string]typ if err != nil { return nil, err } - return r.setUpNetwork(ctrNS.Path(), netOpts) + netStatus, err := r.setUpNetwork(ctrNS.Path(), netOpts) + if err != nil { + return nil, err + } + + // setup rootless port forwarder when rootless with ports and the network status is empty, + // if this is called from network reload the network status will not be empty and we should + // not setup port because they are still active + if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && ctr.getNetworkStatus() == nil { + // set up port forwarder for rootless netns + netnsPath := ctrNS.Path() + // TODO: support slirp4netns port forwarder as well + // make sure to fix this in container.handleRestartPolicy() as well + // Important we have to call this after r.setUpNetwork() so that + // we can use the proper netStatus + err = r.setupRootlessPortMappingViaRLK(ctr, netnsPath, netStatus) + } + return netStatus, err } // Create and configure a new network namespace for a container @@ -690,31 +708,10 @@ func (r *Runtime) createNetNS(ctr *Container) (n ns.NetNS, q map[string]types.St logrus.Debugf("Made network namespace at %s for container %s", ctrNS.Path(), ctr.ID()) var networkStatus map[string]types.StatusBlock - if !ctr.config.NetMode.IsSlirp4netns() { - networkStatus, err = r.configureNetNS(ctr, ctrNS) - } + networkStatus, err = r.configureNetNS(ctr, ctrNS) return ctrNS, networkStatus, err } -// Configure the network namespace for a rootless container -func (r *Runtime) setupRootlessNetNS(ctr *Container) error { - if ctr.config.NetMode.IsSlirp4netns() { - return r.setupSlirp4netns(ctr) - } - networks, _, err := ctr.networks() - if err != nil { - return err - } - if len(networks) > 0 && len(ctr.config.PortMappings) > 0 { - // set up port forwarder for rootless netns - netnsPath := ctr.state.NetNS.Path() - // TODO: support slirp4netns port forwarder as well - // make sure to fix this in container.handleRestartPolicy() as well - return r.setupRootlessPortMappingViaRLK(ctr, netnsPath) - } - return nil -} - // Configure the network namespace using the container process func (r *Runtime) setupNetNS(ctr *Container) error { nsProcess := fmt.Sprintf("/proc/%d/ns/net", ctr.state.PID) @@ -1209,9 +1206,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro ContainerID: c.config.ID, ContainerName: getCNIPodName(c), } - if len(c.config.PortMappings) > 0 { - opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings) - } + opts.PortMappings = c.config.PortMappings eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName) if !exists { return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName) @@ -1303,9 +1298,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e ContainerID: c.config.ID, ContainerName: getCNIPodName(c), } - if len(c.config.PortMappings) > 0 { - opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings) - } + opts.PortMappings = c.config.PortMappings eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName) if !exists { return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName) @@ -1373,16 +1366,67 @@ func (r *Runtime) normalizeNetworkName(nameOrID string) (string, error) { return net.Name, nil } +// ocicniPortsToNetTypesPorts convert the old port format to the new one +// while deduplicating ports into ranges func ocicniPortsToNetTypesPorts(ports []types.OCICNIPortMapping) []types.PortMapping { + if len(ports) == 0 { + return nil + } + newPorts := make([]types.PortMapping, 0, len(ports)) - for _, port := range ports { - newPorts = append(newPorts, types.PortMapping{ - HostIP: port.HostIP, - HostPort: uint16(port.HostPort), - ContainerPort: uint16(port.ContainerPort), - Protocol: port.Protocol, - Range: 1, - }) + + // first sort the ports + sort.Slice(ports, func(i, j int) bool { + return compareOCICNIPorts(ports[i], ports[j]) + }) + + // we already check if the slice is empty so we can use the first element + currentPort := types.PortMapping{ + HostIP: ports[0].HostIP, + HostPort: uint16(ports[0].HostPort), + ContainerPort: uint16(ports[0].ContainerPort), + Protocol: ports[0].Protocol, + Range: 1, + } + + for i := 1; i < len(ports); i++ { + if ports[i].HostIP == currentPort.HostIP && + ports[i].Protocol == currentPort.Protocol && + ports[i].HostPort-int32(currentPort.Range) == int32(currentPort.HostPort) && + ports[i].ContainerPort-int32(currentPort.Range) == int32(currentPort.ContainerPort) { + currentPort.Range = currentPort.Range + 1 + } else { + newPorts = append(newPorts, currentPort) + currentPort = types.PortMapping{ + HostIP: ports[i].HostIP, + HostPort: uint16(ports[i].HostPort), + ContainerPort: uint16(ports[i].ContainerPort), + Protocol: ports[i].Protocol, + Range: 1, + } + } } + newPorts = append(newPorts, currentPort) return newPorts } + +// compareOCICNIPorts will sort the ocicni ports by +// 1) host ip +// 2) protocol +// 3) hostPort +// 4) container port +func compareOCICNIPorts(i, j types.OCICNIPortMapping) bool { + if i.HostIP != j.HostIP { + return i.HostIP < j.HostIP + } + + if i.Protocol != j.Protocol { + return i.Protocol < j.Protocol + } + + if i.HostPort != j.HostPort { + return i.HostPort < j.HostPort + } + + return i.ContainerPort < j.ContainerPort +} diff --git a/libpod/networking_linux_test.go b/libpod/networking_linux_test.go new file mode 100644 index 000000000..06bf05723 --- /dev/null +++ b/libpod/networking_linux_test.go @@ -0,0 +1,323 @@ +package libpod + +import ( + "fmt" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/stretchr/testify/assert" +) + +func Test_ocicniPortsToNetTypesPorts(t *testing.T) { + tests := []struct { + name string + arg []types.OCICNIPortMapping + want []types.PortMapping + }{ + { + name: "no ports", + arg: nil, + want: nil, + }, + { + name: "empty ports", + arg: []types.OCICNIPortMapping{}, + want: nil, + }, + { + name: "single port", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "two separate ports", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 9000, + ContainerPort: 90, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 9000, + ContainerPort: 90, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "two ports joined", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "three ports with different container port are not joined", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8081, + ContainerPort: 79, + Protocol: "tcp", + }, + { + HostPort: 8082, + ContainerPort: 82, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8081, + ContainerPort: 79, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8082, + ContainerPort: 82, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "three ports joined (not sorted)", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8082, + ContainerPort: 82, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 3, + }, + }, + }, + { + name: "different protocols ports are not joined", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "different host ip ports are not joined", + arg: []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.1.1", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + HostIP: "192.168.1.2", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + HostIP: "192.168.1.1", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + Range: 1, + HostIP: "192.168.1.2", + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + result := ocicniPortsToNetTypesPorts(tt.arg) + assert.Equal(t, tt.want, result, "ports do not match") + }) + } +} + +func benchmarkOCICNIPortsToNetTypesPorts(b *testing.B, ports []types.OCICNIPortMapping) { + for n := 0; n < b.N; n++ { + ocicniPortsToNetTypesPorts(ports) + } +} + +func Benchmark_ocicniPortsToNetTypesPortsNoPorts(b *testing.B) { + benchmarkOCICNIPortsToNetTypesPorts(b, nil) +} + +func Benchmark_ocicniPortsToNetTypesPorts1(b *testing.B) { + benchmarkOCICNIPortsToNetTypesPorts(b, []types.OCICNIPortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }) +} + +func Benchmark_ocicniPortsToNetTypesPorts10(b *testing.B) { + ports := make([]types.OCICNIPortMapping, 0, 10) + for i := int32(8080); i < 8090; i++ { + ports = append(ports, types.OCICNIPortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkOCICNIPortsToNetTypesPorts(b, ports) +} + +func Benchmark_ocicniPortsToNetTypesPorts100(b *testing.B) { + ports := make([]types.OCICNIPortMapping, 0, 100) + for i := int32(8080); i < 8180; i++ { + ports = append(ports, types.OCICNIPortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkOCICNIPortsToNetTypesPorts(b, ports) +} + +func Benchmark_ocicniPortsToNetTypesPorts1k(b *testing.B) { + ports := make([]types.OCICNIPortMapping, 0, 1000) + for i := int32(8080); i < 9080; i++ { + ports = append(ports, types.OCICNIPortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkOCICNIPortsToNetTypesPorts(b, ports) +} + +func Benchmark_ocicniPortsToNetTypesPorts10k(b *testing.B) { + ports := make([]types.OCICNIPortMapping, 0, 30000) + for i := int32(8080); i < 18080; i++ { + ports = append(ports, types.OCICNIPortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkOCICNIPortsToNetTypesPorts(b, ports) +} + +func Benchmark_ocicniPortsToNetTypesPorts1m(b *testing.B) { + ports := make([]types.OCICNIPortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + for i := int32(1); i <= 50000; i++ { + ports = append(ports, types.OCICNIPortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + } + b.ResetTimer() + benchmarkOCICNIPortsToNetTypesPorts(b, ports) +} diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go index 56e8eca99..674075e23 100644 --- a/libpod/networking_slirp4netns.go +++ b/libpod/networking_slirp4netns.go @@ -17,6 +17,7 @@ import ( "time" "github.com/containernetworking/plugins/pkg/ns" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/rootlessport" @@ -38,9 +39,9 @@ type slirpFeatures struct { type slirp4netnsCmdArg struct { Proto string `json:"proto,omitempty"` HostAddr string `json:"host_addr"` - HostPort int32 `json:"host_port"` + HostPort uint16 `json:"host_port"` GuestAddr string `json:"guest_addr"` - GuestPort int32 `json:"guest_port"` + GuestPort uint16 `json:"guest_port"` } type slirp4netnsCmd struct { @@ -207,7 +208,7 @@ func createBasicSlirp4netnsCmdArgs(options *slirp4netnsNetworkOptions, features } // setupSlirp4netns can be called in rootful as well as in rootless -func (r *Runtime) setupSlirp4netns(ctr *Container) error { +func (r *Runtime) setupSlirp4netns(ctr *Container, netns ns.NetNS) error { path := r.config.Engine.NetworkCmdPath if path == "" { var err error @@ -263,7 +264,7 @@ func (r *Runtime) setupSlirp4netns(ctr *Container) error { if err != nil { return errors.Wrapf(err, "failed to create rootless network sync pipe") } - netnsPath = ctr.state.NetNS.Path() + netnsPath = netns.Path() cmdArgs = append(cmdArgs, "--netns-type=path", netnsPath, "tap0") } else { defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncR) @@ -366,7 +367,7 @@ func (r *Runtime) setupSlirp4netns(ctr *Container) error { if netOptions.isSlirpHostForward { return r.setupRootlessPortMappingViaSlirp(ctr, cmd, apiSocket) } - return r.setupRootlessPortMappingViaRLK(ctr, netnsPath) + return r.setupRootlessPortMappingViaRLK(ctr, netnsPath, nil) } return nil @@ -479,7 +480,7 @@ func waitForSync(syncR *os.File, cmd *exec.Cmd, logFile io.ReadSeeker, timeout t return nil } -func (r *Runtime) setupRootlessPortMappingViaRLK(ctr *Container, netnsPath string) error { +func (r *Runtime) setupRootlessPortMappingViaRLK(ctr *Container, netnsPath string, netStatus map[string]types.StatusBlock) error { syncR, syncW, err := os.Pipe() if err != nil { return errors.Wrapf(err, "failed to open pipe") @@ -506,7 +507,7 @@ func (r *Runtime) setupRootlessPortMappingViaRLK(ctr *Container, netnsPath strin } } - childIP := getRootlessPortChildIP(ctr) + childIP := getRootlessPortChildIP(ctr, netStatus) cfg := rootlessport.Config{ Mappings: ctr.config.PortMappings, NetNSPath: netnsPath, @@ -531,9 +532,7 @@ func (r *Runtime) setupRootlessPortMappingViaRLK(ctr *Container, netnsPath strin cmd.Args = []string{rootlessport.BinaryName} // Leak one end of the pipe in rootlessport process, the other will be sent to conmon - if ctr.rootlessPortSyncR != nil { - defer errorhandling.CloseQuiet(ctr.rootlessPortSyncR) - } + defer errorhandling.CloseQuiet(ctr.rootlessPortSyncR) cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessPortSyncR, syncW) cmd.Stdin = cfgR @@ -649,7 +648,7 @@ func (r *Runtime) setupRootlessPortMappingViaSlirp(ctr *Container, cmd *exec.Cmd return nil } -func getRootlessPortChildIP(c *Container) string { +func getRootlessPortChildIP(c *Container, netStatus map[string]types.StatusBlock) string { if c.config.NetMode.IsSlirp4netns() { slirp4netnsIP, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) if err != nil { @@ -659,7 +658,7 @@ func getRootlessPortChildIP(c *Container) string { } var ipv6 net.IP - for _, status := range c.getNetworkStatus() { + for _, status := range netStatus { for _, netInt := range status.Interfaces { for _, netAddress := range netInt.Networks { ipv4 := netAddress.Subnet.IP.To4() @@ -679,15 +678,15 @@ func getRootlessPortChildIP(c *Container) string { // reloadRootlessRLKPortMapping will trigger a reload for the port mappings in the rootlessport process. // This should only be called by network connect/disconnect and only as rootless. func (c *Container) reloadRootlessRLKPortMapping() error { - childIP := getRootlessPortChildIP(c) + if len(c.config.PortMappings) == 0 { + return nil + } + childIP := getRootlessPortChildIP(c, c.state.NetworkStatus) logrus.Debugf("reloading rootless ports for container %s, childIP is %s", c.config.ID, childIP) conn, err := openUnixSocket(filepath.Join(c.runtime.config.Engine.TmpDir, "rp", c.config.ID)) if err != nil { - // This is not a hard error for backwards compatibility. A container started - // with an old version did not created the rootlessport socket. - logrus.Warnf("Could not reload rootless port mappings, port forwarding may no longer work correctly: %v", err) - return nil + return errors.Wrap(err, "could not reload rootless port mappings, port forwarding may no longer work correctly") } defer conn.Close() enc := json.NewEncoder(conn) diff --git a/libpod/oci_util.go b/libpod/oci_util.go index c1afc0d20..6d99d5836 100644 --- a/libpod/oci_util.go +++ b/libpod/oci_util.go @@ -32,93 +32,108 @@ func createUnitName(prefix string, name string) string { } // Bind ports to keep them closed on the host -func bindPorts(ports []types.OCICNIPortMapping) ([]*os.File, error) { +func bindPorts(ports []types.PortMapping) ([]*os.File, error) { var files []*os.File - notifySCTP := false - for _, i := range ports { - isV6 := net.ParseIP(i.HostIP).To4() == nil - if i.HostIP == "" { + sctpWarning := true + for _, port := range ports { + isV6 := net.ParseIP(port.HostIP).To4() == nil + if port.HostIP == "" { isV6 = false } - switch i.Protocol { - case "udp": - var ( - addr *net.UDPAddr - err error - ) - if isV6 { - addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort)) - } else { - addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort)) - } - if err != nil { - return nil, errors.Wrapf(err, "cannot resolve the UDP address") + protocols := strings.Split(port.Protocol, ",") + for _, protocol := range protocols { + for i := uint16(0); i < port.Range; i++ { + f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning) + if err != nil { + return files, err + } + if f != nil { + files = append(files, f) + } } + } + } + return files, nil +} - proto := "udp4" - if isV6 { - proto = "udp6" - } - server, err := net.ListenUDP(proto, addr) - if err != nil { - return nil, errors.Wrapf(err, "cannot listen on the UDP port") - } - f, err := server.File() - if err != nil { - return nil, errors.Wrapf(err, "cannot get file for UDP socket") - } - files = append(files, f) - // close the listener - // note that this does not affect the fd, see the godoc for server.File() - err = server.Close() - if err != nil { - logrus.Warnf("Failed to close connection: %v", err) - } +func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) { + var file *os.File + switch protocol { + case "udp": + var ( + addr *net.UDPAddr + err error + ) + if isV6 { + addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", hostIP, port)) + } else { + addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", hostIP, port)) + } + if err != nil { + return nil, errors.Wrapf(err, "cannot resolve the UDP address") + } - case "tcp": - var ( - addr *net.TCPAddr - err error - ) - if isV6 { - addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort)) - } else { - addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort)) - } - if err != nil { - return nil, errors.Wrapf(err, "cannot resolve the TCP address") - } + proto := "udp4" + if isV6 { + proto = "udp6" + } + server, err := net.ListenUDP(proto, addr) + if err != nil { + return nil, errors.Wrapf(err, "cannot listen on the UDP port") + } + file, err = server.File() + if err != nil { + return nil, errors.Wrapf(err, "cannot get file for UDP socket") + } + // close the listener + // note that this does not affect the fd, see the godoc for server.File() + err = server.Close() + if err != nil { + logrus.Warnf("Failed to close connection: %v", err) + } - proto := "tcp4" - if isV6 { - proto = "tcp6" - } - server, err := net.ListenTCP(proto, addr) - if err != nil { - return nil, errors.Wrapf(err, "cannot listen on the TCP port") - } - f, err := server.File() - if err != nil { - return nil, errors.Wrapf(err, "cannot get file for TCP socket") - } - files = append(files, f) - // close the listener - // note that this does not affect the fd, see the godoc for server.File() - err = server.Close() - if err != nil { - logrus.Warnf("Failed to close connection: %v", err) - } + case "tcp": + var ( + addr *net.TCPAddr + err error + ) + if isV6 { + addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", hostIP, port)) + } else { + addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", hostIP, port)) + } + if err != nil { + return nil, errors.Wrapf(err, "cannot resolve the TCP address") + } - case "sctp": - if !notifySCTP { - notifySCTP = true - logrus.Warnf("Port reservation for SCTP is not supported") - } - default: - return nil, fmt.Errorf("unknown protocol %s", i.Protocol) + proto := "tcp4" + if isV6 { + proto = "tcp6" + } + server, err := net.ListenTCP(proto, addr) + if err != nil { + return nil, errors.Wrapf(err, "cannot listen on the TCP port") + } + file, err = server.File() + if err != nil { + return nil, errors.Wrapf(err, "cannot get file for TCP socket") + } + // close the listener + // note that this does not affect the fd, see the godoc for server.File() + err = server.Close() + if err != nil { + logrus.Warnf("Failed to close connection: %v", err) } + + case "sctp": + if *sctpWarning { + logrus.Info("Port reservation for SCTP is not supported") + *sctpWarning = false + } + default: + return nil, fmt.Errorf("unknown protocol %s", protocol) } - return files, nil + return file, nil } func getOCIRuntimeError(runtimeMsg string) error { diff --git a/libpod/options.go b/libpod/options.go index f1cf015f8..0cc4c784c 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -227,6 +227,19 @@ func WithNetworkCmdPath(path string) RuntimeOption { } } +// WithNetworkBackend specifies the name of the network backend. +func WithNetworkBackend(name string) RuntimeOption { + return func(rt *Runtime) error { + if rt.valid { + return define.ErrRuntimeFinalized + } + + rt.config.Network.NetworkBackend = name + + return nil + } +} + // WithCgroupManager specifies the manager implementation name which is used to // handle cgroups for containers. // Current valid values are "cgroupfs" and "systemd". @@ -1051,7 +1064,7 @@ func WithDependencyCtrs(ctrs []*Container) CtrCreateOption { // namespace with a minimal configuration. // An optional array of port mappings can be provided. // Conflicts with WithNetNSFrom(). -func WithNetNS(portMappings []nettypes.OCICNIPortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption { +func WithNetNS(portMappings []nettypes.PortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized @@ -2084,21 +2097,6 @@ func WithInfraContainer() PodCreateOption { } } -// WithInfraContainerPorts tells the pod to add port bindings to the pause container -func WithInfraContainerPorts(bindings []nettypes.OCICNIPortMapping, infraSpec *specgen.SpecGenerator) []nettypes.PortMapping { - bindingSpec := []nettypes.PortMapping{} - for _, bind := range bindings { - currBind := nettypes.PortMapping{} - currBind.ContainerPort = uint16(bind.ContainerPort) - currBind.HostIP = bind.HostIP - currBind.HostPort = uint16(bind.HostPort) - currBind.Protocol = bind.Protocol - bindingSpec = append(bindingSpec, currBind) - } - infraSpec.PortMappings = bindingSpec - return infraSpec.PortMappings -} - // WithVolatile sets the volatile flag for the container storage. // The option can potentially cause data loss when used on a container that must survive a machine reboot. func WithVolatile() CtrCreateOption { diff --git a/libpod/runtime.go b/libpod/runtime.go index b01f8dd13..c751df79b 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -28,6 +28,7 @@ import ( "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/libpod/lock" "github.com/containers/podman/v3/libpod/network/cni" + "github.com/containers/podman/v3/libpod/network/netavark" nettypes "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/plugin" "github.com/containers/podman/v3/libpod/shutdown" @@ -483,16 +484,45 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } } - netInterface, err := cni.NewCNINetworkInterface(cni.InitConfig{ - CNIConfigDir: runtime.config.Network.NetworkConfigDir, - CNIPluginDirs: runtime.config.Network.CNIPluginDirs, - DefaultNetwork: runtime.config.Network.DefaultNetwork, - DefaultSubnet: runtime.config.Network.DefaultSubnet, - IsMachine: runtime.config.Engine.MachineEnabled, - LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "cni.lock"), - }) - if err != nil { - return errors.Wrapf(err, "could not create network interface") + var netInterface nettypes.ContainerNetwork + + switch runtime.config.Network.NetworkBackend { + case "", "cni": + netInterface, err = cni.NewCNINetworkInterface(cni.InitConfig{ + CNIConfigDir: runtime.config.Network.NetworkConfigDir, + CNIPluginDirs: runtime.config.Network.CNIPluginDirs, + DefaultNetwork: runtime.config.Network.DefaultNetwork, + DefaultSubnet: runtime.config.Network.DefaultSubnet, + IsMachine: runtime.config.Engine.MachineEnabled, + LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "cni.lock"), + }) + if err != nil { + return errors.Wrapf(err, "could not create network interface") + } + if runtime.config.Network.NetworkBackend == "" { + // set backend to cni so that podman info can display it + runtime.config.Network.NetworkBackend = "cni" + } + + case "netavark": + netavarkBin, err := runtime.config.FindHelperBinary("netavark", false) + if err != nil { + return err + } + + netInterface, err = netavark.NewNetworkInterface(netavark.InitConfig{ + NetavarkBinary: netavarkBin, + NetworkConfigDir: filepath.Join(runtime.config.Engine.StaticDir, "networks"), + DefaultNetwork: runtime.config.Network.DefaultNetwork, + DefaultSubnet: runtime.config.Network.DefaultSubnet, + IsMachine: runtime.config.Engine.MachineEnabled, + LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "netavark.lock"), + }) + if err != nil { + return errors.Wrapf(err, "could not create network interface") + } + default: + return errors.Errorf("unsupported network backend %q, check network_backend in containers.conf", runtime.config.Network.NetworkBackend) } runtime.network = netInterface diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 0a7db33f1..114bf9315 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -326,15 +326,6 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai } } - if ctr.config.Name == "" { - name, err := r.generateName() - if err != nil { - return nil, err - } - - ctr.config.Name = name - } - // Check CGroup parent sanity, and set it if it was not set. // Only if we're actually configuring CGroups. if !ctr.config.NoCgroups { diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index 7d7fef4d1..15050ef48 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -43,18 +43,6 @@ func (r *Runtime) NewPod(ctx context.Context, p specgen.PodSpecGenerator, option } } - if pod.config.Name == "" { - name, err := r.generateName() - if err != nil { - return nil, err - } - pod.config.Name = name - } - - if p.InfraContainerSpec != nil && p.InfraContainerSpec.Hostname == "" { - p.InfraContainerSpec.Hostname = pod.config.Name - } - // Allocate a lock for the pod lock, err := r.lockManager.AllocateLock() if err != nil { @@ -131,9 +119,33 @@ func (r *Runtime) NewPod(ctx context.Context, p specgen.PodSpecGenerator, option logrus.Infof("Pod has an infra container, but shares no namespaces") } - if err := r.state.AddPod(pod); err != nil { - return nil, errors.Wrapf(err, "error adding pod to state") + // Unless the user has specified a name, use a randomly generated one. + // Note that name conflicts may occur (see #11735), so we need to loop. + generateName := pod.config.Name == "" + var addPodErr error + for { + if generateName { + name, err := r.generateName() + if err != nil { + return nil, err + } + pod.config.Name = name + } + + if p.InfraContainerSpec != nil && p.InfraContainerSpec.Hostname == "" { + p.InfraContainerSpec.Hostname = pod.config.Name + } + if addPodErr = r.state.AddPod(pod); addPodErr == nil { + return pod, nil + } + if !generateName || (errors.Cause(addPodErr) != define.ErrPodExists && errors.Cause(addPodErr) != define.ErrCtrExists) { + break + } + } + if addPodErr != nil { + return nil, errors.Wrapf(addPodErr, "error adding pod to state") } + return pod, nil } diff --git a/libpod/state_test.go b/libpod/state_test.go index 4799d7b8d..5c3b0d7f7 100644 --- a/libpod/state_test.go +++ b/libpod/state_test.go @@ -11,6 +11,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/lock" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/storage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -3705,3 +3706,42 @@ func TestGetContainerConfigNonExistentIDFails(t *testing.T) { assert.Error(t, err) }) } + +// Test that the state will convert the ports to the new format +func TestConvertPortMapping(t *testing.T) { + runForAllStates(t, func(t *testing.T, state State, manager lock.Manager) { + testCtr, err := getTestCtr1(manager) + assert.NoError(t, err) + + ports := testCtr.config.PortMappings + + oldPorts := []types.OCICNIPortMapping{ + { + HostPort: 80, + ContainerPort: 90, + Protocol: "tcp", + HostIP: "192.168.3.3", + }, + { + HostPort: 100, + ContainerPort: 110, + Protocol: "udp", + HostIP: "192.168.4.4", + }, + } + + testCtr.config.OldPortMappings = oldPorts + testCtr.config.PortMappings = nil + + err = state.AddContainer(testCtr) + assert.NoError(t, err) + + retrievedCtr, err := state.Container(testCtr.ID()) + assert.NoError(t, err) + + // set values to expected ones + testCtr.config.PortMappings = ports + + testContainersEqual(t, retrievedCtr, testCtr, true) + }) +} diff --git a/libpod/util.go b/libpod/util.go index 8f8303ff2..5154a261e 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -295,19 +295,21 @@ func writeHijackHeader(r *http.Request, conn io.Writer) { } // Convert OCICNI port bindings into Inspect-formatted port bindings. -func makeInspectPortBindings(bindings []types.OCICNIPortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort { +func makeInspectPortBindings(bindings []types.PortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort { portBindings := make(map[string][]define.InspectHostPort) for _, port := range bindings { - key := fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol) - hostPorts := portBindings[key] - if hostPorts == nil { - hostPorts = []define.InspectHostPort{} + protocols := strings.Split(port.Protocol, ",") + for _, protocol := range protocols { + for i := uint16(0); i < port.Range; i++ { + key := fmt.Sprintf("%d/%s", port.ContainerPort+i, protocol) + hostPorts := portBindings[key] + hostPorts = append(hostPorts, define.InspectHostPort{ + HostIP: port.HostIP, + HostPort: fmt.Sprintf("%d", port.HostPort+i), + }) + portBindings[key] = hostPorts + } } - hostPorts = append(hostPorts, define.InspectHostPort{ - HostIP: port.HostIP, - HostPort: fmt.Sprintf("%d", port.HostPort), - }) - portBindings[key] = hostPorts } // add exposed ports without host port information to match docker for port, protocols := range expose { diff --git a/nix/default-arm64.nix b/nix/default-arm64.nix index bb958a193..fa076f27d 100644 --- a/nix/default-arm64.nix +++ b/nix/default-arm64.nix @@ -59,7 +59,8 @@ let self = with pkgs; buildGoModule rec { name = "podman"; - src = ./..; + src = builtins.filterSource + (path: type: !(type == "directory" && baseNameOf path == "bin")) ./..; vendorSha256 = null; doCheck = false; enableParallelBuilding = true; diff --git a/nix/default.nix b/nix/default.nix index 1dc6f92b6..30ae21503 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -57,7 +57,8 @@ let self = with pkgs; buildGoModule rec { name = "podman"; - src = ./..; + src = builtins.filterSource + (path: type: !(type == "directory" && baseNameOf path == "bin")) ./..; vendorSha256 = null; doCheck = false; enableParallelBuilding = true; diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index f53e31f9b..da82c9745 100644 --- a/pkg/checkpoint/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -6,6 +6,7 @@ import ( "os" metadata "github.com/checkpoint-restore/checkpointctl/lib" + "github.com/checkpoint-restore/go-criu/v5/stats" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" @@ -39,6 +40,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt "volumes", "ctr.log", "artifacts", + stats.StatsDump, metadata.RootFsDiffTar, metadata.DeletedFilesFile, metadata.NetworkStatusFile, @@ -193,7 +195,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt } if len(restoreOptions.PublishPorts) > 0 { - ports, _, _, err := generate.ParsePortMapping(restoreOptions.PublishPorts) + ports, err := generate.ParsePortMapping(restoreOptions.PublishPorts, nil) if err != nil { return nil, err } diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index 58f231a2f..d018d373f 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -54,7 +54,7 @@ type ListContainer struct { // boolean to be set PodName string // Port mappings - Ports []types.OCICNIPortMapping + Ports []types.PortMapping // Size of the container rootfs. Requires the size boolean to be true Size *define.ContainerSize // Time when container started diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index deae85fe1..869c616ea 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -422,7 +422,7 @@ type ContainerPortOptions struct { // the CLI to output ports type ContainerPortReport struct { Id string //nolint - Ports []nettypes.OCICNIPortMapping + Ports []nettypes.PortMapping } // ContainerCpOptions describes input options for cp. diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index b0f9ae408..d72f64b5e 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -27,6 +27,7 @@ type ImageEngine interface { ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error) Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error + Transfer(ctx context.Context, scpOpts ImageScpOptions) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 38cdc8f2f..7583ce442 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -329,6 +329,10 @@ type ImageScpOptions struct { Save ImageSaveOptions // Load options used for the second half of the scp operation Load ImageLoadOptions + // Rootless determines whether we are loading locally from root storage to rootless storage + Rootless bool + // User is used in conjunction with Rootless to determine which user to use to obtain the uid + User string } // ImageTreeOptions provides options for ImageEngine.Tree() diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 081a2464b..a4d6bcf86 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -124,8 +124,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, if err != nil { return nil, err } - - b, err := generateKubeYAML(po) + b, err := generateKubeYAML(libpod.ConvertV1PodToYAMLPod(po)) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 7aa202334..5c0227986 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -6,9 +6,12 @@ import ( "io/ioutil" "net/url" "os" + "os/exec" + "os/user" "path" "path/filepath" "strconv" + "strings" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" @@ -18,6 +21,7 @@ import ( "github.com/containers/image/v5/signature" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities/reports" domainUtils "github.com/containers/podman/v3/pkg/domain/utils" @@ -330,6 +334,67 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri return pushError } +// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root +func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { + if scpOpts.User == "" { + return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") + } + var u *user.User + scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid + _, err := strconv.Atoi(scpOpts.User) + if err != nil { + u, err = user.Lookup(scpOpts.User) + if err != nil { + return err + } + } else { + u, err = user.LookupId(scpOpts.User) + if err != nil { + return err + } + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return err + } + gid, err := strconv.Atoi(u.Gid) + if err != nil { + return err + } + err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access + if err != nil { + return err + } + + podman, err := os.Executable() + if err != nil { + return err + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") + cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + logrus.Debug("Executing load command su") + err = cmd.Run() + if err != nil { + return err + } + } else { + cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + logrus.Debug("Executing load command machinectl") + err = cmd.Run() + if err != nil { + return err + } + } + + return nil +} + func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { // Allow tagging manifest list instead of resolving instances from manifest lookupOptions := &libimage.LookupImageOptions{ManifestList: true} diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index 7ec6135ee..cfb674b6d 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -200,6 +200,9 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo if fs.Changed("network-cmd-path") { options = append(options, libpod.WithNetworkCmdPath(cfg.Engine.NetworkCmdPath)) } + if fs.Changed("network-backend") { + options = append(options, libpod.WithNetworkBackend(cfg.Network.NetworkBackend)) + } if fs.Changed("events-backend") { options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger)) diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index e17f746a5..fde57972f 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -12,6 +12,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/bindings/images" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities/reports" @@ -122,6 +123,10 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. return &entities.ImagePullReport{Images: pulledImages}, nil } +func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { + return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage") +} + func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, opt entities.ImageTagOptions) error { options := new(images.TagOptions) for _, newTag := range tags { diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index 7f9228666..3e81d5c14 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -325,7 +325,7 @@ func becomeRootInUserNS(pausePid, fileToRead string, fileOutput *os.File) (_ boo uidsMapped = err == nil } if !uidsMapped { - logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids") + logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids if not using a network user") setgroups := fmt.Sprintf("/proc/%d/setgroups", pid) err = ioutil.WriteFile(setgroups, []byte("deny\n"), 0666) if err != nil { diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 7b9e5bbfa..7e6075789 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -23,7 +23,7 @@ const ( // Config needs to be provided to the process via stdin as a JSON string. // stdin needs to be closed after the message has been written. type Config struct { - Mappings []types.OCICNIPortMapping + Mappings []types.PortMapping NetNSPath string ExitFD int ReadyFD int diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 88ebc7ae3..501bce05d 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -204,11 +204,11 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec // replacing necessary values with those specified in pod creation func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { if len(p.PortMappings) > 0 { - ports, _, _, err := ParsePortMapping(p.PortMappings) + ports, err := ParsePortMapping(p.PortMappings, nil) if err != nil { return nil, err } - p.InfraContainerSpec.PortMappings = libpod.WithInfraContainerPorts(ports, p.InfraContainerSpec) + p.InfraContainerSpec.PortMappings = ports } switch p.NetNS.NSMode { case specgen.Default, "": diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go index 992b4a8e9..53a5e5697 100644 --- a/pkg/specgen/generate/ports.go +++ b/pkg/specgen/generate/ports.go @@ -2,7 +2,9 @@ package generate import ( "context" + "fmt" "net" + "sort" "strconv" "strings" @@ -11,6 +13,7 @@ import ( "github.com/containers/podman/v3/utils" "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -21,252 +24,323 @@ const ( protoSCTP = "sctp" ) -// Parse port maps to OCICNI port mappings. -// Returns a set of OCICNI port mappings, and maps of utilized container and +// joinTwoPortsToRangePortIfPossible will expect two ports the previous port one must have a lower or equal hostPort than the current port. +func joinTwoPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, + previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) { + // no previous port just return the current one + if previousPort == nil { + return &port, nil + } + if previousPort.HostPort+previousPort.Range >= port.HostPort { + // check if the port range matches the host and container ports + portDiff := port.HostPort - previousPort.HostPort + if portDiff == port.ContainerPort-previousPort.ContainerPort { + // calc the new range use the old range and add the difference between the ports + newRange := port.Range + portDiff + // if the newRange is greater than the old range use it + // this is important otherwise we would could lower the range + if newRange > previousPort.Range { + previousPort.Range = newRange + } + return previousPort, nil + } + // if both host port ranges overlap and the container port range did not match + // we have to error because we cannot assign the same host port to more than one container port + if previousPort.HostPort+previousPort.Range-1 > port.HostPort { + return nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", port.HostPort, port.Protocol) + } + } + // we could not join the ports so we append the old one to the list + // and return the current port as previous port + addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, previousPort) + return &port, nil +} + +// joinTwoContainerPortsToRangePortIfPossible will expect two ports with both no host port set, +// the previous port one must have a lower or equal containerPort than the current port. +func joinTwoContainerPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, + previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) { + // no previous port just return the current one + if previousPort == nil { + return &port, nil + } + if previousPort.ContainerPort+previousPort.Range > port.ContainerPort { + // calc the new range use the old range and add the difference between the ports + newRange := port.ContainerPort - previousPort.ContainerPort + port.Range + // if the newRange is greater than the old range use it + // this is important otherwise we would could lower the range + if newRange > previousPort.Range { + previousPort.Range = newRange + } + return previousPort, nil + } + // we could not join the ports so we append the old one to the list + // and return the current port as previous port + newPort, err := getRandomHostPort(currentHostPorts, *previousPort) + if err != nil { + return nil, err + } + addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, &newPort) + return &port, nil +} + +func addPortToUsedPorts(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, port *types.PortMapping) { + for i := uint16(0); i < port.Range; i++ { + h := port.HostPort + i + allHostPorts[h] = true + currentHostPorts[h] = true + c := port.ContainerPort + i + allContainerPorts[c] = true + } + *ports = append(*ports, *port) +} + +// getRandomHostPort get a random host port mapping for the given port +// the caller has to supply a array with he already used ports +func getRandomHostPort(hostPorts *[65536]bool, port types.PortMapping) (types.PortMapping, error) { +outer: + for i := 0; i < 15; i++ { + ranPort, err := utils.GetRandomPort() + if err != nil { + return port, err + } + + // if port range is exceeds max port we cannot use it + if ranPort+int(port.Range) > 65535 { + continue + } + + // check if there is a port in the range which is used + for j := 0; j < int(port.Range); j++ { + // port already used + if hostPorts[ranPort+j] { + continue outer + } + } + + port.HostPort = uint16(ranPort) + return port, nil + } + + // add range to error message if needed + rangePort := "" + if port.Range > 1 { + rangePort = fmt.Sprintf("with range %d ", port.Range) + } + + return port, errors.Errorf("failed to find an open port to expose container port %d %son the host", port.ContainerPort, rangePort) +} + +// Parse port maps to port mappings. +// Returns a set of port mappings, and maps of utilized container and // host ports. -func ParsePortMapping(portMappings []types.PortMapping) ([]types.OCICNIPortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) { - // First, we need to validate the ports passed in the specgen, and then - // convert them into CNI port mappings. - type tempMapping struct { - mapping types.OCICNIPortMapping - startOfRange bool - isInRange bool +func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) { + if len(portMappings) == 0 && len(exposePorts) == 0 { + return nil, nil } - tempMappings := []tempMapping{} - - // To validate, we need two maps: one for host ports, one for container - // ports. - // Each is a map of protocol to map of IP address to map of port to - // port (for hostPortValidate, it's host port to container port; - // for containerPortValidate, container port to host port. - // These will ensure no collisions. - hostPortValidate := make(map[string]map[string]map[uint16]uint16) - containerPortValidate := make(map[string]map[string]map[uint16]uint16) - - // Initialize the first level of maps (we can't really guess keys for - // the rest). - for _, proto := range []string{protoTCP, protoUDP, protoSCTP} { - hostPortValidate[proto] = make(map[string]map[uint16]uint16) - containerPortValidate[proto] = make(map[string]map[uint16]uint16) + + // tempMapping stores the ports without ip and protocol + type tempMapping struct { + hostPort uint16 + containerPort uint16 + rangePort uint16 } - postAssignHostPort := false + // portMap is a temporary structure to sort all ports + // the map is hostIp -> protocol -> array of mappings + portMap := make(map[string]map[string][]tempMapping) + + // allUsedContainerPorts stores all used ports for each protocol + // the key is the protocol and the array is 65536 elements long for each port. + allUsedContainerPortsMap := make(map[string][65536]bool) + allUsedHostPortsMap := make(map[string][65536]bool) - // Iterate through all port mappings, generating OCICNI PortMapping - // structs and validating there is no overlap. + // First, we need to validate the ports passed in the specgen for _, port := range portMappings { // First, check proto protocols, err := checkProtocol(port.Protocol, true) if err != nil { - return nil, nil, nil, err - } - - // Validate host IP - hostIP := port.HostIP - if hostIP == "" { - hostIP = "0.0.0.0" + return nil, err } - if ip := net.ParseIP(hostIP); ip == nil { - return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP) + if port.HostIP != "" { + if ip := net.ParseIP(port.HostIP); ip == nil { + return nil, errors.Errorf("invalid IP address %q in port mapping", port.HostIP) + } } // Validate port numbers and range. - len := port.Range - if len == 0 { - len = 1 + portRange := port.Range + if portRange == 0 { + portRange = 1 } containerPort := port.ContainerPort if containerPort == 0 { - return nil, nil, nil, errors.Errorf("container port number must be non-0") + return nil, errors.Errorf("container port number must be non-0") } hostPort := port.HostPort - if uint32(len-1)+uint32(containerPort) > 65535 { - return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number") + if uint32(portRange-1)+uint32(containerPort) > 65535 { + return nil, errors.Errorf("container port range exceeds maximum allowable port number") } - if uint32(len-1)+uint32(hostPort) > 65536 { - return nil, nil, nil, errors.Errorf("host port range exceeds maximum allowable port number") + if uint32(portRange-1)+uint32(hostPort) > 65535 { + return nil, errors.Errorf("host port range exceeds maximum allowable port number") } - // Iterate through ports, populating maps to check for conflicts - // and generating CNI port mappings. - for _, p := range protocols { - hostIPMap := hostPortValidate[p] - ctrIPMap := containerPortValidate[p] - - hostPortMap, ok := hostIPMap[hostIP] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostIPMap[hostIP] = hostPortMap + hostProtoMap, ok := portMap[port.HostIP] + if !ok { + hostProtoMap = make(map[string][]tempMapping) + for _, proto := range []string{protoTCP, protoUDP, protoSCTP} { + hostProtoMap[proto] = make([]tempMapping, 0) } - ctrPortMap, ok := ctrIPMap[hostIP] - if !ok { - ctrPortMap = make(map[uint16]uint16) - ctrIPMap[hostIP] = ctrPortMap - } - - // Iterate through all port numbers in the requested - // range. - var index uint16 - for index = 0; index < len; index++ { - cPort := containerPort + index - hPort := hostPort - // Only increment host port if it's not 0. - if hostPort != 0 { - hPort += index - } - - if cPort == 0 { - return nil, nil, nil, errors.Errorf("container port cannot be 0") - } + portMap[port.HostIP] = hostProtoMap + } - // Host port is allowed to be 0. If it is, we - // select a random port on the host. - // This will happen *after* all other ports are - // placed, to ensure we don't accidentally - // select a port that a later mapping wanted. - if hPort == 0 { - // If we already have a host port - // assigned to their container port - - // just use that. - if ctrPortMap[cPort] != 0 { - hPort = ctrPortMap[cPort] - } else { - postAssignHostPort = true - } - } else { - testHPort := hostPortMap[hPort] - if testHPort != 0 && testHPort != cPort { - return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p) - } - hostPortMap[hPort] = cPort - - // Mapping a container port to multiple - // host ports is allowed. - // We only store the latest of these in - // the container port map - we don't - // need to know all of them, just one. - testCPort := ctrPortMap[cPort] - ctrPortMap[cPort] = hPort - - // If we have an exact duplicate, just continue - if testCPort == hPort && testHPort == cPort { - continue - } - } + p := tempMapping{ + hostPort: port.HostPort, + containerPort: port.ContainerPort, + rangePort: portRange, + } - // We appear to be clear. Make an OCICNI port - // struct. - // Don't use hostIP - we want to preserve the - // empty string hostIP by default for compat. - cniPort := types.OCICNIPortMapping{ - HostPort: int32(hPort), - ContainerPort: int32(cPort), - Protocol: p, - HostIP: port.HostIP, - } - tempMappings = append( - tempMappings, - tempMapping{ - mapping: cniPort, - startOfRange: port.Range > 1 && index == 0, - isInRange: port.Range > 1, - }, - ) - } + for _, proto := range protocols { + hostProtoMap[proto] = append(hostProtoMap[proto], p) } } - // Handle any 0 host ports now by setting random container ports. - if postAssignHostPort { - remadeMappings := make([]types.OCICNIPortMapping, 0, len(tempMappings)) - - var ( - candidate int - err error - ) - - // Iterate over all - for _, tmp := range tempMappings { - p := tmp.mapping + // we do no longer need the original port mappings + // set it to 0 length so we can resuse it to populate + // the slice again while keeping the underlying capacity + portMappings = portMappings[:0] - if p.HostPort != 0 { - remadeMappings = append(remadeMappings, p) + for hostIP, protoMap := range portMap { + for protocol, ports := range protoMap { + ports := ports + if len(ports) == 0 { continue } - - hostIPMap := hostPortValidate[p.Protocol] - ctrIPMap := containerPortValidate[p.Protocol] - - hostPortMap, ok := hostIPMap[p.HostIP] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostIPMap[p.HostIP] = hostPortMap + // 1. sort the ports by host port + // use a small hack to make sure ports with host port 0 are sorted last + sort.Slice(ports, func(i, j int) bool { + if ports[i].hostPort == ports[j].hostPort { + return ports[i].containerPort < ports[j].containerPort + } + if ports[i].hostPort == 0 { + return false + } + if ports[j].hostPort == 0 { + return true + } + return ports[i].hostPort < ports[j].hostPort + }) + + allUsedContainerPorts := allUsedContainerPortsMap[protocol] + allUsedHostPorts := allUsedHostPortsMap[protocol] + var usedHostPorts [65536]bool + + var previousPort *types.PortMapping + var i int + for i = 0; i < len(ports); i++ { + if ports[i].hostPort == 0 { + // because the ports are sorted and host port 0 is last + // we can break when we hit 0 + // we will fit them in afterwards + break + } + p := types.PortMapping{ + HostIP: hostIP, + Protocol: protocol, + HostPort: ports[i].hostPort, + ContainerPort: ports[i].containerPort, + Range: ports[i].rangePort, + } + var err error + previousPort, err = joinTwoPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort, p) + if err != nil { + return nil, err + } } - ctrPortMap, ok := ctrIPMap[p.HostIP] - if !ok { - ctrPortMap = make(map[uint16]uint16) - ctrIPMap[p.HostIP] = ctrPortMap + if previousPort != nil { + addPortToUsedPorts(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort) } - // See if container port has been used elsewhere - if ctrPortMap[uint16(p.ContainerPort)] != 0 { - // Duplicate definition. Let's not bother - // including it. - continue + // now take care of the hostPort = 0 ports + previousPort = nil + for i < len(ports) { + p := types.PortMapping{ + HostIP: hostIP, + Protocol: protocol, + ContainerPort: ports[i].containerPort, + Range: ports[i].rangePort, + } + var err error + previousPort, err = joinTwoContainerPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort, p) + if err != nil { + return nil, err + } + i++ + } + if previousPort != nil { + newPort, err := getRandomHostPort(&usedHostPorts, *previousPort) + if err != nil { + return nil, err + } + addPortToUsedPorts(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, &newPort) } - // Max retries to ensure we don't loop forever. - for i := 0; i < 15; i++ { - // Only get a random candidate for single entries or the start - // of a range. Otherwise we just increment the candidate. - if !tmp.isInRange || tmp.startOfRange { - candidate, err = utils.GetRandomPort() + allUsedContainerPortsMap[protocol] = allUsedContainerPorts + allUsedHostPortsMap[protocol] = allUsedHostPorts + } + } + + if len(exposePorts) > 0 { + logrus.Debugf("Adding exposed ports") + + for port, protocols := range exposePorts { + newProtocols := make([]string, 0, len(protocols)) + for _, protocol := range protocols { + if !allUsedContainerPortsMap[protocol][port] { + p := types.PortMapping{ + ContainerPort: port, + Protocol: protocol, + Range: 1, + } + allPorts := allUsedContainerPortsMap[protocol] + p, err := getRandomHostPort(&allPorts, p) if err != nil { - return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort) + return nil, err } + portMappings = append(portMappings, p) } else { - candidate++ - } - - if hostPortMap[uint16(candidate)] == 0 { - logrus.Debugf("Successfully assigned container port %d to host port %d (IP %s Protocol %s)", p.ContainerPort, candidate, p.HostIP, p.Protocol) - hostPortMap[uint16(candidate)] = uint16(p.ContainerPort) - ctrPortMap[uint16(p.ContainerPort)] = uint16(candidate) - p.HostPort = int32(candidate) - break + newProtocols = append(newProtocols, protocol) } } - if p.HostPort == 0 { - return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort) + // make sure to delete the key from the map if there are no protocols left + if len(newProtocols) == 0 { + delete(exposePorts, port) + } else { + exposePorts[port] = newProtocols } - remadeMappings = append(remadeMappings, p) } - return remadeMappings, containerPortValidate, hostPortValidate, nil } + return portMappings, nil +} - finalMappings := []types.OCICNIPortMapping{} - for _, m := range tempMappings { - finalMappings = append(finalMappings, m.mapping) +func appendProtocolsNoDuplicates(slice []string, protocols []string) []string { + for _, proto := range protocols { + if util.StringInSlice(proto, slice) { + continue + } + slice = append(slice, proto) } - - return finalMappings, containerPortValidate, hostPortValidate, nil + return slice } // Make final port mappings for the container -func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.OCICNIPortMapping, map[uint16][]string, error) { - finalMappings, containerPortValidate, hostPortValidate, err := ParsePortMapping(s.PortMappings) - if err != nil { - return nil, nil, err - } - - // No exposed ports so return the port mappings we've made so far. - if len(s.Expose) == 0 && imageData == nil { - return finalMappings, nil, nil - } - - logrus.Debugf("Adding exposed ports") - +func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) { expose := make(map[uint16]string) + var err error if imageData != nil { expose, err = GenExposedPorts(imageData.Config.ExposedPorts) if err != nil { @@ -274,103 +348,30 @@ func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData } } - // We need to merge s.Expose into image exposed ports - for k, v := range s.Expose { - expose[k] = v - } - // There's been a request to expose some ports. Let's do that. - // Start by figuring out what needs to be exposed. - // This is a map of container port number to protocols to expose. - toExpose := make(map[uint16][]string) - for port, proto := range expose { - // Validate protocol first - protocols, err := checkProtocol(proto, false) - if err != nil { - return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port) - } - - if port == 0 { - return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number") - } - - // Check to see if the port is already present in existing - // mappings. - for _, p := range protocols { - ctrPortMap, ok := containerPortValidate[p]["0.0.0.0"] - if !ok { - ctrPortMap = make(map[uint16]uint16) - containerPortValidate[p]["0.0.0.0"] = ctrPortMap + toExpose := make(map[uint16][]string, len(s.Expose)+len(expose)) + for _, expose := range []map[uint16]string{expose, s.Expose} { + for port, proto := range expose { + if port == 0 { + return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number") } - - if portNum := ctrPortMap[port]; portNum == 0 { - // We want to expose this port for this protocol - exposeProto, ok := toExpose[port] - if !ok { - exposeProto = []string{} - } - exposeProto = append(exposeProto, p) - toExpose[port] = exposeProto + protocols, err := checkProtocol(proto, false) + if err != nil { + return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port) } + toExpose[port] = appendProtocolsNoDuplicates(toExpose[port], protocols) } } - // If not publishing exposed ports return mappings and exposed ports. + publishPorts := toExpose if !s.PublishExposedPorts { - return finalMappings, toExpose, nil + publishPorts = nil } - // We now have a final list of ports that we want exposed. - // Let's find empty, unallocated host ports for them. - for port, protocols := range toExpose { - for _, p := range protocols { - // Find an open port on the host. - // I see a faint possibility that this will infinite - // loop trying to find a valid open port, so I've - // included a max-tries counter. - hostPort := 0 - tries := 15 - for hostPort == 0 && tries > 0 { - // We can't select a specific protocol, which is - // unfortunate for the UDP case. - candidate, err := utils.GetRandomPort() - if err != nil { - return nil, nil, err - } - - // Check if the host port is already bound - hostPortMap, ok := hostPortValidate[p]["0.0.0.0"] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostPortValidate[p]["0.0.0.0"] = hostPortMap - } - - if checkPort := hostPortMap[uint16(candidate)]; checkPort != 0 { - // Host port is already allocated, try again - tries-- - continue - } - - hostPortMap[uint16(candidate)] = port - hostPort = candidate - logrus.Debugf("Mapping exposed port %d/%s to host port %d", port, p, hostPort) - - // Make a CNI port mapping - cniPort := types.OCICNIPortMapping{ - HostPort: int32(candidate), - ContainerPort: int32(port), - Protocol: p, - HostIP: "", - } - finalMappings = append(finalMappings, cniPort) - } - if tries == 0 && hostPort == 0 { - // We failed to find an open port. - return nil, nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port) - } - } + finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts) + if err != nil { + return nil, nil, err } - - return finalMappings, nil, nil + return finalMappings, toExpose, nil } // Check a string to ensure it is a comma-separated set of valid protocols @@ -409,7 +410,7 @@ func checkProtocol(protocol string, allowSCTP bool) ([]string, error) { } func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) { - expose := make(map[uint16]string) + expose := make(map[uint16]string, len(exposedPorts)) for imgExpose := range exposedPorts { // Expose format is portNumber[/protocol] splitExpose := strings.SplitN(imgExpose, "/", 2) @@ -420,12 +421,20 @@ func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error if num > 65535 || num < 1 { return nil, errors.Errorf("%d from image EXPOSE statement %q is not a valid port number", num, imgExpose) } - // No need to validate protocol, we'll do it below. - if len(splitExpose) == 1 { - expose[uint16(num)] = "tcp" + + // No need to validate protocol, we'll do it later. + newProto := "tcp" + if len(splitExpose) == 2 { + newProto = splitExpose[1] + } + + proto := expose[uint16(num)] + if len(proto) > 1 { + proto = proto + "," + newProto } else { - expose[uint16(num)] = splitExpose[1] + proto = newProto } + expose[uint16(num)] = proto } return expose, nil } diff --git a/pkg/specgen/generate/ports_bench_test.go b/pkg/specgen/generate/ports_bench_test.go new file mode 100644 index 000000000..06f02acda --- /dev/null +++ b/pkg/specgen/generate/ports_bench_test.go @@ -0,0 +1,197 @@ +package generate + +import ( + "fmt" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" +) + +func benchmarkParsePortMapping(b *testing.B, ports []types.PortMapping) { + for n := 0; n < b.N; n++ { + ParsePortMapping(ports, nil) + } +} + +func BenchmarkParsePortMappingNoPorts(b *testing.B) { + benchmarkParsePortMapping(b, nil) +} + +func BenchmarkParsePortMapping1(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }) +} + +func BenchmarkParsePortMapping100(b *testing.B) { + ports := make([]types.PortMapping, 0, 100) + for i := uint16(8080); i < 8180; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping1k(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000) + for i := uint16(8080); i < 9080; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping10k(b *testing.B) { + ports := make([]types.PortMapping, 0, 30000) + for i := uint16(8080); i < 18080; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + for i := uint16(1); i <= 50000; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse100(b *testing.B) { + ports := make([]types.PortMapping, 0, 100) + for i := uint16(8180); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse1k(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000) + for i := uint16(9080); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse10k(b *testing.B) { + ports := make([]types.PortMapping, 0, 30000) + for i := uint16(18080); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + for i := uint16(50000); i > 0; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingRange1(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }) +} + +func BenchmarkParsePortMappingRange100(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 100, + }, + }) +} + +func BenchmarkParsePortMappingRange1k(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1000, + }, + }) +} + +func BenchmarkParsePortMappingRange10k(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10000, + }, + }) +} + +func BenchmarkParsePortMappingRange1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + ports = append(ports, types.PortMapping{ + HostPort: 1, + ContainerPort: 1, + Protocol: "tcp", + Range: 50000, + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} diff --git a/pkg/specgen/generate/ports_test.go b/pkg/specgen/generate/ports_test.go new file mode 100644 index 000000000..20d5d0166 --- /dev/null +++ b/pkg/specgen/generate/ports_test.go @@ -0,0 +1,989 @@ +package generate + +import ( + "testing" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/stretchr/testify/assert" +) + +func TestParsePortMappingWithHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + arg2 map[uint16][]string + want []types.PortMapping + }{ + { + name: "no ports", + arg: nil, + want: nil, + }, + { + name: "one tcp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one tcp port no proto", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one udp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "one sctp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + Range: 1, + }, + }, + }, + { + name: "one port two protocols", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp,udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "one port three protocols", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp,udp,sctp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + Range: 1, + }, + }, + }, + { + name: "one port with range 1", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one port with range 5", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 5, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 5, + }, + }, + }, + { + name: "two ports joined", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two ports joined with range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two ports with no overlapping range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 9090, + ContainerPort: 9090, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 9090, + ContainerPort: 9090, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + }, + }, + { + name: "four ports with two overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 100, + ContainerPort: 5, + Protocol: "tcp", + }, + { + HostPort: 101, + ContainerPort: 6, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 15, + }, + { + HostPort: 100, + ContainerPort: 5, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 2, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + }, + }, + { + name: "four overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 8090, + ContainerPort: 90, + Protocol: "tcp", + Range: 7, + }, + { + HostPort: 8095, + ContainerPort: 95, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 17, + }, + }, + }, + { + name: "one port range overlaps 5 ports", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Range: 20, + }, + { + HostPort: 8085, + ContainerPort: 85, + Range: 2, + }, + { + HostPort: 8090, + ContainerPort: 90, + }, + { + HostPort: 8095, + ContainerPort: 95, + }, + { + HostPort: 8096, + ContainerPort: 96, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 20, + }, + }, + }, + { + name: "different host ip same port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.1.1", + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.2.1", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.1.1", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.2.1", + Range: 1, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, tt.arg2) + assert.NoError(t, err, "error is not nil") + // use ElementsMatch instead of Equal because the order is not consistent + assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingWithoutHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + arg2 map[uint16][]string + want []types.PortMapping + }{ + { + name: "one tcp port", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one port with two protocols", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp,udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "same port twice", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "neighbor ports are not joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "overlapping range ports are joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "four overlapping range ports are joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 3, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 82, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 5, + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 15, + }, + }, + }, + { + name: "expose one tcp port", + arg2: map[uint16][]string{ + 8080: {"tcp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "expose already defined port", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + }, + }, + arg2: map[uint16][]string{ + 8080: {"tcp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "expose different proto", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + }, + }, + arg2: map[uint16][]string{ + 8080: {"udp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "udp", + Range: 1, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, tt.arg2) + assert.NoError(t, err, "error is not nil") + + // because we always get random host ports when it is set to 0 we cannot check that exactly + // check if it is not 0 and set to to 0 afterwards + for i := range got { + assert.Greater(t, got[i].HostPort, uint16(0), "host port is zero") + got[i].HostPort = 0 + } + + // use ElementsMatch instead of Equal because the order is not consistent + assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingMixedHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + want []types.PortMapping + resetHostPorts []int + }{ + { + name: "two ports one without a hostport set", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + }, + { + HostPort: 8080, + ContainerPort: 8080, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1}, + }, + { + name: "two ports one without a hostport set, inverted order", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + }, + { + HostPort: 0, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1}, + }, + { + name: "three ports without host ports, one with a hostport set, , inverted order", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + }, + { + HostPort: 0, + ContainerPort: 85, + }, + { + HostPort: 0, + ContainerPort: 90, + }, + { + HostPort: 8080, + ContainerPort: 8080, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 85, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1, 2, 3}, + }, + { + name: "three ports without host ports, one with a hostport set", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + }, + { + HostPort: 0, + ContainerPort: 90, + }, + { + HostPort: 0, + ContainerPort: 85, + }, + { + HostPort: 0, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 85, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1, 2, 3}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, nil) + assert.NoError(t, err, "error is not nil") + + // because we always get random host ports when it is set to 0 we cannot check that exactly + // use resetHostPorts to know which port element is 0 + for _, num := range tt.resetHostPorts { + assert.Greater(t, got[num].HostPort, uint16(0), "host port is zero") + got[num].HostPort = 0 + } + + assert.Equal(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingError(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + err string + }{ + { + name: "container port is 0", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 0, + Protocol: "tcp", + }, + }, + err: "container port number must be non-0", + }, + { + name: "container port range exceeds max", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 65000, + Protocol: "tcp", + Range: 10000, + }, + }, + err: "container port range exceeds maximum allowable port number", + }, + { + name: "host port range exceeds max", + arg: []types.PortMapping{ + { + HostPort: 60000, + ContainerPort: 1, + Protocol: "tcp", + Range: 10000, + }, + }, + err: "host port range exceeds maximum allowable port number", + }, + { + name: "invalid protocol", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "1", + }, + }, + err: "unrecognized protocol \"1\" in port mapping", + }, + { + name: "invalid protocol 2", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp,u", + }, + }, + err: "unrecognized protocol \"u\" in port mapping", + }, + { + name: "invalid ip address", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + HostIP: "blah", + }, + }, + err: "invalid IP address \"blah\" in port mapping", + }, + { + name: "invalid overalpping range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Range: 5, + }, + { + HostPort: 8081, + ContainerPort: 60, + }, + }, + err: "conflicting port mappings for host port 8081 (protocol tcp)", + }, + { + name: "big port range with host port zero does not fit", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 1, + Range: 65535, + }, + }, + err: "failed to find an open port to expose container port 1 with range 65535 on the host", + }, + { + name: "big port range with host port zero does not fit", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 1000, + Range: 64535, + }, + }, + err: "failed to find an open port to expose container port 1000 with range 64535 on the host", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, err := ParsePortMapping(tt.arg, nil) + assert.EqualError(t, err, tt.err, "error does not match") + }) + } +} diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index a8efe1ca9..be6b782b5 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -5,9 +5,11 @@ import ( "net" "os" "os/exec" + "path/filepath" "strings" "time" + "github.com/checkpoint-restore/go-criu/v5/stats" "github.com/containers/podman/v3/pkg/checkpoint/crutils" "github.com/containers/podman/v3/pkg/criu" . "github.com/containers/podman/v3/test/utils" @@ -1191,4 +1193,55 @@ var _ = Describe("Podman checkpoint", func() { // Remove exported checkpoint os.Remove(fileName) }) + + It("podman checkpoint container with export and statistics", func() { + localRunString := getRunString([]string{ + "--rm", + ALPINE, + "top", + }) + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + cid := session.OutputToString() + fileName := "/tmp/checkpoint-" + cid + ".tar.gz" + + result := podmanTest.Podman([]string{ + "container", + "checkpoint", + "-l", "-e", + fileName, + }) + result.WaitWithDefaultTimeout() + + // As the container has been started with '--rm' it will be completely + // cleaned up after checkpointing. + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(0)) + + // Extract checkpoint archive + destinationDirectory, err := CreateTempDirInTempDir() + Expect(err).ShouldNot(HaveOccurred()) + + tarsession := SystemExec( + "tar", + []string{ + "xf", + fileName, + "-C", + destinationDirectory, + }, + ) + Expect(tarsession).Should(Exit(0)) + + _, err = os.Stat(filepath.Join(destinationDirectory, stats.StatsDump)) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(os.RemoveAll(destinationDirectory)).To(BeNil()) + + // Remove exported checkpoint + os.Remove(fileName) + }) }) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index e598f7ab9..200faae2d 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -208,9 +208,7 @@ var _ = SynchronizedAfterSuite(func() {}, // PodmanTestCreate creates a PodmanTestIntegration instance for the tests func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { - var ( - podmanRemoteBinary string - ) + var podmanRemoteBinary string host := GetHostDistributionInfo() cwd, _ := os.Getwd() @@ -220,12 +218,11 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { podmanBinary = os.Getenv("PODMAN_BINARY") } - if remote { - podmanRemoteBinary = filepath.Join(cwd, "../../bin/podman-remote") - if os.Getenv("PODMAN_REMOTE_BINARY") != "" { - podmanRemoteBinary = os.Getenv("PODMAN_REMOTE_BINARY") - } + podmanRemoteBinary = filepath.Join(cwd, "../../bin/podman-remote") + if os.Getenv("PODMAN_REMOTE_BINARY") != "" { + podmanRemoteBinary = os.Getenv("PODMAN_REMOTE_BINARY") } + conmonBinary := filepath.Join("/usr/libexec/podman/conmon") altConmonBinary := "/usr/bin/conmon" if _, err := os.Stat(conmonBinary); os.IsNotExist(err) { @@ -271,12 +268,13 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { p := &PodmanTestIntegration{ PodmanTest: PodmanTest{ - PodmanBinary: podmanBinary, - ArtifactPath: ARTIFACT_DIR, - TempDir: tempDir, - RemoteTest: remote, - ImageCacheFS: storageFs, - ImageCacheDir: ImageCacheDir, + PodmanBinary: podmanBinary, + RemotePodmanBinary: podmanRemoteBinary, + ArtifactPath: ARTIFACT_DIR, + TempDir: tempDir, + RemoteTest: remote, + ImageCacheFS: storageFs, + ImageCacheDir: ImageCacheDir, }, ConmonBinary: conmonBinary, CrioRoot: filepath.Join(tempDir, "crio"), @@ -289,8 +287,8 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { CgroupManager: cgroupManager, Host: host, } + if remote { - p.PodmanTest.RemotePodmanBinary = podmanRemoteBinary uuid := stringid.GenerateNonCryptoID() if !rootless.IsRootless() { p.RemoteSocket = fmt.Sprintf("unix:/run/podman/podman-%s.sock", uuid) @@ -632,6 +630,19 @@ func SkipIfNotRootless(reason string) { } } +func SkipIfSystemdNotRunning(reason string) { + checkReason(reason) + + cmd := exec.Command("systemctl", "list-units") + err := cmd.Run() + if err != nil { + if _, ok := err.(*exec.Error); ok { + ginkgo.Skip("[notSystemd]: not running " + reason) + } + Expect(err).ToNot(HaveOccurred()) + } +} + func SkipIfNotSystemd(manager, reason string) { checkReason(reason) if manager != "systemd" { @@ -683,6 +694,41 @@ func SkipIfContainerized(reason string) { } } +func SkipIfRemote(reason string) { + checkReason(reason) + if !IsRemote() { + return + } + ginkgo.Skip("[remote]: " + reason) +} + +// SkipIfInContainer skips a test if the test is run inside a container +func SkipIfInContainer(reason string) { + checkReason(reason) + if os.Getenv("TEST_ENVIRON") == "container" { + Skip("[container]: " + reason) + } +} + +// SkipIfNotActive skips a test if the given systemd unit is not active +func SkipIfNotActive(unit string, reason string) { + checkReason(reason) + + var buffer bytes.Buffer + cmd := exec.Command("systemctl", "is-active", unit) + cmd.Stdout = &buffer + err := cmd.Start() + Expect(err).ToNot(HaveOccurred()) + + err = cmd.Wait() + Expect(err).ToNot(HaveOccurred()) + + Expect(err).ToNot(HaveOccurred()) + if strings.TrimSpace(buffer.String()) != "active" { + Skip(fmt.Sprintf("[systemd]: unit %s is not active: %s", unit, reason)) + } +} + // PodmanAsUser is the exec call to podman on the filesystem with the specified uid/gid and environment func (p *PodmanTestIntegration) PodmanAsUser(args []string, uid, gid uint32, cwd string, env []string) *PodmanSessionIntegration { podmanSession := p.PodmanAsUserBase(args, uid, gid, cwd, env, false, false, nil, nil) diff --git a/test/e2e/config/containers-netns.conf b/test/e2e/config/containers-netns.conf new file mode 100644 index 000000000..3f796f25d --- /dev/null +++ b/test/e2e/config/containers-netns.conf @@ -0,0 +1,3 @@ +[containers] + +netns = "host" diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go index b2666c789..c9a6f926f 100644 --- a/test/e2e/healthcheck_run_test.go +++ b/test/e2e/healthcheck_run_test.go @@ -52,6 +52,18 @@ var _ = Describe("Podman healthcheck run", func() { Expect(hc).Should(Exit(125)) }) + It("podman healthcheck from image's config (not container config)", func() { + // Regression test for #12226: a health check may be defined in + // the container or the container-config of an image. + session := podmanTest.Podman([]string{"create", "--name", "hc", "quay.io/libpod/healthcheck:config-only", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + hc := podmanTest.Podman([]string{"container", "inspect", "--format", "{{.Config.Healthcheck}}", "hc"}) + hc.WaitWithDefaultTimeout() + Expect(hc).Should(Exit(0)) + Expect(hc.OutputToString()).To(Equal("{[CMD-SHELL curl -f http://localhost/ || exit 1] 0s 5m0s 3s 0}")) + }) + It("podman disable healthcheck with --health-cmd=none on valid container", func() { session := podmanTest.Podman([]string{"run", "-dt", "--health-cmd", "none", "--name", "hc", healthcheck}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go index 9fd8d7e27..acea2993d 100644 --- a/test/e2e/image_scp_test.go +++ b/test/e2e/image_scp_test.go @@ -22,12 +22,14 @@ var _ = Describe("podman image scp", func() { ) BeforeEach(func() { + ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF") conf, err := ioutil.TempFile("", "containersconf") if err != nil { panic(err) } os.Setenv("CONTAINERS_CONF", conf.Name()) + tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -38,6 +40,7 @@ var _ = Describe("podman image scp", func() { AfterEach(func() { podmanTest.Cleanup() + os.Remove(os.Getenv("CONTAINERS_CONF")) if ConfPath.IsSet { os.Setenv("CONTAINERS_CONF", ConfPath.Value) @@ -58,6 +61,25 @@ var _ = Describe("podman image scp", func() { Expect(scp).To(Exit(0)) }) + It("podman image scp root to rootless transfer", func() { + SkipIfNotRootless("this is a rootless only test, transfering from root to rootless using PodmanAsUser") + if IsRemote() { + Skip("this test is only for non-remote") + } + env := os.Environ() + img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root + img.WaitWithDefaultTimeout() + Expect(img).To(Exit(0)) + scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us) + scp.WaitWithDefaultTimeout() + Expect(scp).To(Exit(0)) + + list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root + list.WaitWithDefaultTimeout() + Expect(list).To(Exit(0)) + Expect(list.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + }) + It("podman image scp bogus image", func() { if IsRemote() { Skip("this test is only for non-remote") diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index ad511cc9e..1fa29daa1 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -16,20 +16,12 @@ import ( "time" "github.com/containers/podman/v3/pkg/rootless" - "github.com/onsi/ginkgo" ) func IsRemote() bool { return true } -func SkipIfRemote(reason string) { - if len(reason) < 5 { - panic("SkipIfRemote must specify a reason to skip") - } - ginkgo.Skip("[remote]: " + reason) -} - // Podman is the exec call to podman on the filesystem func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration { var remoteArgs = []string{"--remote", "--url", p.RemoteSocket} diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index 6d2d3fee8..001a869b1 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -16,9 +16,6 @@ func IsRemote() bool { return false } -func SkipIfRemote(string) { -} - // Podman is the exec call to podman on the filesystem func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration { podmanSession := p.PodmanBase(args, false, false) diff --git a/test/e2e/network_connect_disconnect_test.go b/test/e2e/network_connect_disconnect_test.go index 6cddf9285..2205a1263 100644 --- a/test/e2e/network_connect_disconnect_test.go +++ b/test/e2e/network_connect_disconnect_test.go @@ -87,6 +87,7 @@ var _ = Describe("Podman network connect and disconnect", func() { dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) dis.WaitWithDefaultTimeout() Expect(dis).Should(Exit(0)) + Expect(dis.ErrorToString()).Should(Equal("")) inspect := podmanTest.Podman([]string{"container", "inspect", "test", "--format", "{{len .NetworkSettings.Networks}}"}) inspect.WaitWithDefaultTimeout() @@ -183,6 +184,7 @@ var _ = Describe("Podman network connect and disconnect", func() { connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"}) connect.WaitWithDefaultTimeout() Expect(connect).Should(Exit(0)) + Expect(connect.ErrorToString()).Should(Equal("")) inspect := podmanTest.Podman([]string{"container", "inspect", "test", "--format", "{{len .NetworkSettings.Networks}}"}) inspect.WaitWithDefaultTimeout() diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go index 34e879ed4..12aeffd1b 100644 --- a/test/e2e/pod_create_test.go +++ b/test/e2e/pod_create_test.go @@ -957,4 +957,22 @@ ENTRYPOINT ["sleep","99999"] Expect(ctr3.OutputToString()).To(ContainSubstring("hello")) }) + It("podman pod create read network mode from config", func() { + confPath, err := filepath.Abs("config/containers-netns.conf") + Expect(err).ToNot(HaveOccurred()) + os.Setenv("CONTAINERS_CONF", confPath) + defer os.Unsetenv("CONTAINERS_CONF") + if IsRemote() { + podmanTest.RestartRemoteService() + } + + pod := podmanTest.Podman([]string{"pod", "create", "--name", "test", "--infra-name", "test-infra"}) + pod.WaitWithDefaultTimeout() + Expect(pod).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", "--format", "{{.HostConfig.NetworkMode}}", "test-infra"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).Should(Equal("host")) + }) }) diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go index bdf3ce5d6..c64cfd2d5 100644 --- a/test/e2e/run_networking_test.go +++ b/test/e2e/run_networking_test.go @@ -494,6 +494,23 @@ var _ = Describe("Podman run networking", func() { Expect(containerConfig[0].NetworkSettings.Ports["80/tcp"][0].HostPort).ToNot(Equal(80)) }) + It("podman run forward sctp protocol", func() { + SkipIfRootless("sctp protocol only works as root") + session := podmanTest.Podman([]string{"--log-level=info", "run", "--name=test", "-p", "80/sctp", "-p", "81/sctp", ALPINE}) + session.Wait(90) + Expect(session).Should(Exit(0)) + // we can only check logrus on local podman + if !IsRemote() { + // check that the info message for sctp protocol is only displayed once + Expect(strings.Count(session.ErrorToString(), "Port reservation for SCTP is not supported")).To(Equal(1), "`Port reservation for SCTP is not supported` is not displayed exactly one time in the logrus logs") + } + results := podmanTest.Podman([]string{"inspect", "test"}) + results.Wait(30) + Expect(results).Should(Exit(0)) + Expect(results.OutputToString()).To(ContainSubstring(`"80/sctp":`)) + Expect(results.OutputToString()).To(ContainSubstring(`"81/sctp":`)) + }) + It("podman run hostname test", func() { session := podmanTest.Podman([]string{"run", "--rm", ALPINE, "printenv", "HOSTNAME"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/system_connection_test.go b/test/e2e/system_connection_test.go index 842ae8df6..76b442ce8 100644 --- a/test/e2e/system_connection_test.go +++ b/test/e2e/system_connection_test.go @@ -3,7 +3,11 @@ package integration import ( "fmt" "io/ioutil" + "net/url" "os" + "os/exec" + "os/user" + "path/filepath" "github.com/containers/common/pkg/config" . "github.com/containers/podman/v3/test/utils" @@ -19,22 +23,16 @@ var _ = Describe("podman system connection", func() { IsSet bool }{} - var ( - podmanTest *PodmanTestIntegration - ) + var podmanTest *PodmanTestIntegration BeforeEach(func() { ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF") conf, err := ioutil.TempFile("", "containersconf") - if err != nil { - panic(err) - } + Expect(err).ToNot(HaveOccurred()) os.Setenv("CONTAINERS_CONF", conf.Name()) tempdir, err := CreateTempDirInTempDir() - if err != nil { - panic(err) - } + Expect(err).ToNot(HaveOccurred()) podmanTest = PodmanTestCreate(tempdir) podmanTest.Setup() }) @@ -49,196 +47,241 @@ var _ = Describe("podman system connection", func() { } f := CurrentGinkgoTestDescription() - timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) - GinkgoWriter.Write([]byte(timedResult)) + GinkgoWriter.Write( + []byte( + fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()))) }) - It("add ssh://", func() { - cmd := []string{"system", "connection", "add", - "--default", - "--identity", "~/.ssh/id_rsa", - "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA")) - Expect(cfg.Engine.ServiceDestinations["QA"]).To(Equal( - config.Destination{ - URI: "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - Identity: "~/.ssh/id_rsa", - }, - )) - - cmd = []string{"system", "connection", "rename", - "QA", - "QE", - } - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - - cfg, err = config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QE")) - Expect(cfg.Engine.ServiceDestinations["QE"]).To(Equal( - config.Destination{ - URI: "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - Identity: "~/.ssh/id_rsa", - }, - )) - }) + Context("without running API service", func() { + It("add ssh://", func() { + cmd := []string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) - It("add UDS", func() { - cmd := []string{"system", "connection", "add", - "QA-UDS", - "unix:///run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-UDS")) - Expect(cfg.Engine.ServiceDestinations["QA-UDS"]).To(Equal( - config.Destination{ - URI: "unix:///run/podman/podman.sock", - Identity: "", - }, - )) - - cmd = []string{"system", "connection", "add", - "QA-UDS1", - "--socket-path", "/run/user/podman/podman.sock", - "unix:///run/podman/podman.sock", - } - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err = config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-UDS")) - Expect(cfg.Engine.ServiceDestinations["QA-UDS1"]).To(Equal( - config.Destination{ - URI: "unix:///run/user/podman/podman.sock", - Identity: "", - }, - )) - }) + cfg, err := config.ReadCustomConfig() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).To(HaveActiveService("QA")) + Expect(cfg).Should(VerifyService( + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "~/.ssh/id_rsa", + )) - It("add tcp", func() { - cmd := []string{"system", "connection", "add", - "QA-TCP", - "tcp://localhost:8888", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-TCP")) - Expect(cfg.Engine.ServiceDestinations["QA-TCP"]).To(Equal( - config.Destination{ - URI: "tcp://localhost:8888", - Identity: "", - }, - )) - }) + cmd = []string{"system", "connection", "rename", + "QA", + "QE", + } + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) - It("remove", func() { - cmd := []string{"system", "connection", "add", - "--default", - "--identity", "~/.ssh/id_rsa", - "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) + Expect(config.ReadCustomConfig()).To(HaveActiveService("QE")) + }) - for i := 0; i < 2; i++ { - cmd = []string{"system", "connection", "remove", "QA"} + It("add UDS", func() { + cmd := []string{"system", "connection", "add", + "QA-UDS", + "unix:///run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-UDS", + "unix:///run/podman/podman.sock", + "", + )) + + cmd = []string{"system", "connection", "add", + "QA-UDS1", + "--socket-path", "/run/user/podman/podman.sock", + "unix:///run/podman/podman.sock", + } session = podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) + Expect(session.Out.Contents()).Should(BeEmpty()) - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(BeEmpty()) - Expect(cfg.Engine.ServiceDestinations).To(BeEmpty()) - } - }) + Expect(config.ReadCustomConfig()).Should(HaveActiveService("QA-UDS")) + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-UDS1", + "unix:///run/user/podman/podman.sock", + "", + )) + }) - It("default", func() { - for _, name := range []string{"devl", "qe"} { + It("add tcp", func() { cmd := []string{"system", "connection", "add", + "QA-TCP", + "tcp://localhost:8888", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-TCP", + "tcp://localhost:8888", + "", + )) + }) + + It("remove", func() { + session := podmanTest.Podman([]string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // two passes to test that removing non-existent connection is not an error + for i := 0; i < 2; i++ { + session = podmanTest.Podman([]string{"system", "connection", "remove", "QA"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + cfg, err := config.ReadCustomConfig() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg.Engine.ActiveService).To(BeEmpty()) + Expect(cfg.Engine.ServiceDestinations).To(BeEmpty()) + } + }) + + It("remove --all", func() { + session := podmanTest.Podman([]string{"system", "connection", "add", "--default", "--identity", "~/.ssh/id_rsa", - name, + "QA", "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"system", "connection", "remove", "--all"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + Expect(session.Err.Contents()).Should(BeEmpty()) + + session = podmanTest.Podman([]string{"system", "connection", "list"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + }) + + It("default", func() { + for _, name := range []string{"devl", "qe"} { + cmd := []string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + name, + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) } + + cmd := []string{"system", "connection", "default", "devl"} session := podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - } + Expect(session.Out.Contents()).Should(BeEmpty()) - cmd := []string{"system", "connection", "default", "devl"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("devl")) - - cmd = []string{"system", "connection", "list"} - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("Name *URI *Identity *Default")) - - cmd = []string{"system", "connection", "list", "--format", "{{.Name}}"} - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.OutputToString()).Should(Equal("devl qe")) - }) + Expect(config.ReadCustomConfig()).Should(HaveActiveService("devl")) - It("failed default", func() { - cmd := []string{"system", "connection", "default", "devl"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).ShouldNot(Exit(0)) - Expect(session.Err).Should(Say("destination is not defined")) - }) + cmd = []string{"system", "connection", "list"} + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out).Should(Say("Name *URI *Identity *Default")) + + cmd = []string{"system", "connection", "list", "--format", "{{.Name}}"} + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("devl qe")) + }) + + It("failed default", func() { + cmd := []string{"system", "connection", "default", "devl"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).ShouldNot(Exit(0)) + Expect(session.Err).Should(Say("destination is not defined")) + }) + + It("failed rename", func() { + cmd := []string{"system", "connection", "rename", "devl", "QE"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).ShouldNot(Exit(0)) + Expect(session.Err).Should(Say("destination is not defined")) + }) - It("failed rename", func() { - cmd := []string{"system", "connection", "rename", "devl", "QE"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).ShouldNot(Exit(0)) - Expect(session.Err).Should(Say("destination is not defined")) + It("empty list", func() { + cmd := []string{"system", "connection", "list"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(len(session.OutputToStringArray())).Should(Equal(1)) + Expect(session.Err.Contents()).Should(BeEmpty()) + }) }) - It("empty list", func() { - cmd := []string{"system", "connection", "list"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - Expect(session.Err).Should(Say("")) + Context("sshd and API services required", func() { + BeforeEach(func() { + // These tests are unique in as much as they require podman, podman-remote, systemd and sshd. + // podman-remote commands will be executed by ginkgo directly. + SkipIfContainerized("sshd is not available when running in a container") + SkipIfRemote("connection heuristic requires both podman and podman-remote binaries") + SkipIfNotRootless("FIXME: setup ssh keys when root") + SkipIfSystemdNotRunning("cannot test connection heuristic if systemd is not running") + SkipIfNotActive("sshd", "cannot test connection heuristic if sshd is not running") + }) + + It("add ssh:// socket path using connection heuristic", func() { + u, err := user.Current() + Expect(err).ShouldNot(HaveOccurred()) + + cmd := exec.Command(podmanTest.RemotePodmanBinary, + "system", "connection", "add", + "--default", + "--identity", filepath.Join(u.HomeDir, ".ssh", "id_ed25519"), + "QA", + fmt.Sprintf("ssh://%s@localhost", u.Username)) + + session, err := Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%q failed to execute", podmanTest.RemotePodmanBinary)) + Eventually(session, DefaultWaitTimeout).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + Expect(session.Err.Contents()).Should(BeEmpty()) + + uri := url.URL{ + Scheme: "ssh", + User: url.User(u.Username), + Host: "localhost:22", + Path: fmt.Sprintf("/run/user/%s/podman/podman.sock", u.Uid), + } + + Expect(config.ReadCustomConfig()).Should(HaveActiveService("QA")) + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA", + uri.String(), + filepath.Join(u.HomeDir, ".ssh", "id_ed25519"), + )) + }) }) }) diff --git a/test/system/001-basic.bats b/test/system/001-basic.bats index 78b8ecdfd..03f07d602 100644 --- a/test/system/001-basic.bats +++ b/test/system/001-basic.bats @@ -120,9 +120,7 @@ function setup() { fi run_podman 125 --remote - is "$output" "Error: missing command 'podman COMMAND' -Try 'podman --help' for more information." \ - "podman --remote show usage message without running endpoint" + is "$output" ".*Usage:" "podman --remote show usage message without running endpoint" } # This is for development only; it's intended to make sure our timeout diff --git a/test/system/015-help.bats b/test/system/015-help.bats index b0795b524..a87081687 100644 --- a/test/system/015-help.bats +++ b/test/system/015-help.bats @@ -149,12 +149,12 @@ function check_help() { count=$(expr $count + 1) done - # Any command that takes subcommands, must throw error if called + # Any command that takes subcommands, prints its help and errors if called # without one. dprint "podman $@" run_podman '?' "$@" is "$status" 125 "'podman $*' without any subcommand - exit status" - is "$output" "Error: missing command .*$@ COMMAND" \ + is "$output" ".*Usage:.*Error: missing command '.*$@ COMMAND'" \ "'podman $*' without any subcommand - expected error message" # Assume that 'NoSuchCommand' is not a command diff --git a/test/system/120-load.bats b/test/system/120-load.bats index e9959271f..a5508b2f4 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -126,6 +126,26 @@ verify_iid_and_name() { verify_iid_and_name $img_name } +@test "podman load - from URL" { + get_iid_and_name + run_podman save $img_name -o $archive + run_podman rmi $iid + + HOST_PORT=$(random_free_port) + SERVER=http://127.0.0.1:$HOST_PORT + + # Bind-mount the archive to a container running httpd + run_podman run -d --name myweb -p "$HOST_PORT:80" \ + -v $archive:/var/www/image.tar:Z \ + -w /var/www \ + $IMAGE /bin/busybox-extras httpd -f -p 80 + + run_podman load -i $SERVER/image.tar + verify_iid_and_name $img_name + + run_podman rm -f -t0 myweb +} + @test "podman load - redirect corrupt payload" { run_podman 125 load <<< "Danger, Will Robinson!! This is a corrupt tarball!" is "$output" \ diff --git a/test/system/200-pod.bats b/test/system/200-pod.bats index 09a419914..60cfbc9dd 100644 --- a/test/system/200-pod.bats +++ b/test/system/200-pod.bats @@ -60,6 +60,25 @@ function teardown() { run_podman pod rm -f -t 0 $podid } + +@test "podman pod create - custom infra image" { + image="i.do/not/exist:image" + + tmpdir=$PODMAN_TMPDIR/pod-test + run mkdir -p $tmpdir + containersconf=$tmpdir/containers.conf + cat >$containersconf <<EOF +[engine] +infra_image="$image" +EOF + + run_podman 125 pod create --infra-image $image + is "$output" ".*initializing source docker://$image:.*" + + CONTAINERS_CONF=$containersconf run_podman 125 pod create + is "$output" ".*initializing source docker://$image:.*" +} + function rm_podman_pause_image() { run_podman version --format "{{.Server.Version}}-{{.Server.Built}}" run_podman rmi -f "localhost/podman-pause:$output" diff --git a/test/system/272-system-connection.bats b/test/system/272-system-connection.bats index 5a90d9398..4e9ac4dd6 100644 --- a/test/system/272-system-connection.bats +++ b/test/system/272-system-connection.bats @@ -34,10 +34,7 @@ function teardown() { | xargs -l1 --no-run-if-empty umount # Remove all system connections - run_podman system connection ls --format json - while read name; do - run_podman system connection rm "$name" - done < <(jq -r '.[].Name' <<<"$output") + run_podman system connection rm --all basic_teardown } @@ -53,7 +50,8 @@ function _run_podman_remote() { # Very basic test, does not actually connect at any time @test "podman system connection - basic add / ls / remove" { run_podman system connection ls - is "$output" "" "system connection ls: no connections" + is "$output" "Name URI Identity Default" \ + "system connection ls: no connections" c1="c1_$(random_string 15)" c2="c2_$(random_string 15)" diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index b3471b425..21350ed36 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -78,6 +78,9 @@ load helpers if [[ -z $cidr ]]; then # regex to match that we are in 10.X subnet match="10\..*" + # force bridge networking also for rootless + # this ensures that rootless + bridge + userns + ports works + network_arg="--network bridge" else # Issue #9828 make sure a custom slir4netns cidr also works network_arg="--network slirp4netns:cidr=$cidr" @@ -167,6 +170,13 @@ load helpers $IMAGE nc -l -n -v -p $myport cid="$output" + # FIXME: debugging for #11871 + run_podman exec $cid cat /etc/resolv.conf + if is_rootless; then + run_podman unshare --rootless-cni cat /etc/resolv.conf + fi + ps uxww + # check that dns is working inside the container run_podman exec $cid nslookup google.com @@ -420,6 +430,7 @@ load helpers is "$output" "[${cid:0:12}]" "short container id in network aliases" run_podman network disconnect $netname $cid + is "$output" "" "Output should be empty (no errors)" # check that we cannot curl (timeout after 3 sec) run curl --max-time 3 -s $SERVER/index.txt @@ -428,6 +439,7 @@ load helpers fi run_podman network connect $netname $cid + is "$output" "" "Output should be empty (no errors)" # curl should work again run curl --max-time 3 -s $SERVER/index.txt @@ -444,6 +456,12 @@ load helpers die "MAC address did not change after podman network disconnect/connect" fi + # Disconnect/reconnect of a container *with no ports* should succeed quietly + run_podman network disconnect $netname $background_cid + is "$output" "" "disconnect of container with no open ports" + run_podman network connect $netname $background_cid + is "$output" "" "(re)connect of container with no open ports" + # FIXME FIXME FIXME: #11825: bodhi tests are failing, remote+rootless only, # with "dnsmasq: failed to create inotify". This error has never occurred # in CI, and Ed has been unable to reproduce it on 1minutetip. This next @@ -454,6 +472,7 @@ load helpers # connect a second network run_podman network connect $netname2 $cid + is "$output" "" "Output should be empty (no errors)" # check network2 alias for container short id run_podman inspect $cid --format "{{(index .NetworkSettings.Networks \"$netname2\").Aliases}}" diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats index e9910f3d2..c0d601586 100644 --- a/test/upgrade/test-upgrade.bats +++ b/test/upgrade/test-upgrade.bats @@ -97,6 +97,7 @@ podman \$opts run --name myfailedcontainer --label mylabel=$LABEL_FAILED \ podman \$opts run -d --name myrunningcontainer --label mylabel=$LABEL_RUNNING \ --network bridge \ -p $HOST_PORT:80 \ + -p 127.0.0.1:8080-8082:8080-8082 \ -v $pmroot/var/www:/var/www \ -w /var/www \ --mac-address aa:bb:cc:dd:ee:ff \ @@ -122,8 +123,15 @@ EOF # Clean up vestiges of previous run $PODMAN rm -f podman_parent || true - # Not entirely a NOP! This is just so we get /run/crun created on a CI VM - $PODMAN run --rm $OLD_PODMAN true + + local netname=testnet-$(random_string 10) + $PODMAN network create $netname + + # Not entirely a NOP! This is just so we get the /run/... mount points created on a CI VM + # --mac-address is needed to create /run/cni, --network is needed to create /run/containers for dnsname + $PODMAN run --rm --mac-address 78:28:a6:8d:24:8a --network $netname $OLD_PODMAN true + $PODMAN network rm -f $netname + # # Use new-podman to run the above script under old-podman. @@ -135,7 +143,8 @@ EOF # # mount /etc/containers/storage.conf to use the same storage settings as on the host # mount /dev/shm because the container locks are stored there - # mount /var/lib/cni and /etc/cni/net.d for cni networking + # mount /var/lib/cni, /run/cni and /etc/cni/net.d for cni networking + # mount /run/containers for the dnsname plugin # $PODMAN run -d --name podman_parent --pid=host \ --privileged \ @@ -146,6 +155,8 @@ EOF -v /dev/fuse:/dev/fuse \ -v /run/crun:/run/crun \ -v /run/netns:/run/netns:rshared \ + -v /run/containers:/run/containers \ + -v /run/cni:/run/cni \ -v /var/lib/cni:/var/lib/cni \ -v /etc/cni/net.d:/etc/cni/net.d \ -v /dev/shm:/dev/shm \ @@ -186,7 +197,7 @@ EOF is "${lines[1]}" "mycreatedcontainer--Created----$LABEL_CREATED" "created" is "${lines[2]}" "mydonecontainer--Exited (0).*----<no value>" "done" is "${lines[3]}" "myfailedcontainer--Exited (17) .*----$LABEL_FAILED" "fail" - is "${lines[4]}" "myrunningcontainer--Up .*--0.0.0.0:$HOST_PORT->80/tcp--$LABEL_RUNNING" "running" + is "${lines[4]}" "myrunningcontainer--Up .*--0\.0\.0\.0:$HOST_PORT->80\/tcp, 127\.0\.0\.1\:8080-8082->8080-8082\/tcp--$LABEL_RUNNING" "running" # For debugging: dump containers and IDs if [[ -n "$PODMAN_UPGRADE_TEST_DEBUG" ]]; then diff --git a/test/utils/matchers.go b/test/utils/matchers.go index 07c1232e7..17ff3ea75 100644 --- a/test/utils/matchers.go +++ b/test/utils/matchers.go @@ -2,57 +2,164 @@ package utils import ( "fmt" + "net/url" + "github.com/containers/common/pkg/config" + . "github.com/onsi/gomega" "github.com/onsi/gomega/format" "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/types" ) +// HaveActiveService verifies the given service is the active service +func HaveActiveService(name interface{}) OmegaMatcher { + return WithTransform( + func(cfg *config.Config) string { + return cfg.Engine.ActiveService + }, + Equal(name)) +} + +type ServiceMatcher struct { + types.GomegaMatcher + Name interface{} + URI interface{} + Identity interface{} + failureMessage string + negatedFailureMessage string +} + +func VerifyService(name, uri, identity interface{}) OmegaMatcher { + return &ServiceMatcher{ + Name: name, + URI: uri, + Identity: identity, + } +} + +func (matcher *ServiceMatcher) Match(actual interface{}) (success bool, err error) { + cfg, ok := actual.(*config.Config) + if !ok { + return false, fmt.Errorf("ServiceMatcher matcher expects a config.Config") + } + + if _, err = url.Parse(matcher.URI.(string)); err != nil { + return false, err + } + + success, err = HaveKey(matcher.Name).Match(cfg.Engine.ServiceDestinations) + if !success || err != nil { + matcher.failureMessage = HaveKey(matcher.Name).FailureMessage(cfg.Engine.ServiceDestinations) + matcher.negatedFailureMessage = HaveKey(matcher.Name).NegatedFailureMessage(cfg.Engine.ServiceDestinations) + return + } + + sd := cfg.Engine.ServiceDestinations[matcher.Name.(string)] + success, err = Equal(matcher.URI).Match(sd.URI) + if !success || err != nil { + matcher.failureMessage = Equal(matcher.URI).FailureMessage(sd.URI) + matcher.negatedFailureMessage = Equal(matcher.URI).NegatedFailureMessage(sd.URI) + return + } + + success, err = Equal(matcher.Identity).Match(sd.Identity) + if !success || err != nil { + matcher.failureMessage = Equal(matcher.Identity).FailureMessage(sd.Identity) + matcher.negatedFailureMessage = Equal(matcher.Identity).NegatedFailureMessage(sd.Identity) + return + } + + return true, nil +} + +func (matcher *ServiceMatcher) FailureMessage(_ interface{}) string { + return matcher.failureMessage +} + +func (matcher *ServiceMatcher) NegatedFailureMessage(_ interface{}) string { + return matcher.negatedFailureMessage +} + +type URLMatcher struct { + matchers.EqualMatcher +} + +// VerifyURL matches when actual is a valid URL and matches expected +func VerifyURL(uri interface{}) OmegaMatcher { + return &URLMatcher{matchers.EqualMatcher{Expected: uri}} +} + +func (matcher *URLMatcher) Match(actual interface{}) (bool, error) { + e, ok := matcher.Expected.(string) + if !ok { + return false, fmt.Errorf("VerifyURL requires string inputs %T is not supported", matcher.Expected) + } + e_uri, err := url.Parse(e) + if err != nil { + return false, err + } + + a, ok := actual.(string) + if !ok { + return false, fmt.Errorf("VerifyURL requires string inputs %T is not supported", actual) + } + a_uri, err := url.Parse(a) + if err != nil { + return false, err + } + + return (&matchers.EqualMatcher{Expected: e_uri}).Match(a_uri) +} + +type ExitMatcher struct { + types.GomegaMatcher + Expected int + Actual int +} + // ExitWithError matches when assertion is > argument. Default 0 -// Modeled after the gomega Exit() matcher -func ExitWithError(optionalExitCode ...int) *exitMatcher { +// Modeled after the gomega Exit() matcher and also operates on sessions. +func ExitWithError(optionalExitCode ...int) *ExitMatcher { exitCode := 0 if len(optionalExitCode) > 0 { exitCode = optionalExitCode[0] } - return &exitMatcher{exitCode: exitCode} + return &ExitMatcher{Expected: exitCode} } -type exitMatcher struct { - exitCode int - actualExitCode int -} - -func (m *exitMatcher) Match(actual interface{}) (success bool, err error) { +// Match follows gexec.Matcher interface +func (matcher *ExitMatcher) Match(actual interface{}) (success bool, err error) { exiter, ok := actual.(gexec.Exiter) if !ok { return false, fmt.Errorf("ExitWithError must be passed a gexec.Exiter (Missing method ExitCode() int) Got:\n#{format.Object(actual, 1)}") } - m.actualExitCode = exiter.ExitCode() - if m.actualExitCode == -1 { + matcher.Actual = exiter.ExitCode() + if matcher.Actual == -1 { return false, nil } - return m.actualExitCode > m.exitCode, nil + return matcher.Actual > matcher.Expected, nil } -func (m *exitMatcher) FailureMessage(actual interface{}) (message string) { - if m.actualExitCode == -1 { +func (matcher *ExitMatcher) FailureMessage(_ interface{}) (message string) { + if matcher.Actual == -1 { return "Expected process to exit. It did not." } - return format.Message(m.actualExitCode, "to be greater than exit code:", m.exitCode) + return format.Message(matcher.Actual, "to be greater than exit code: ", matcher.Expected) } -func (m *exitMatcher) NegatedFailureMessage(actual interface{}) (message string) { - if m.actualExitCode == -1 { +func (matcher *ExitMatcher) NegatedFailureMessage(_ interface{}) (message string) { + switch { + case matcher.Actual == -1: return "you really shouldn't be able to see this!" - } else { - if m.exitCode == -1 { - return "Expected process not to exit. It did." - } - return format.Message(m.actualExitCode, "is less than or equal to exit code:", m.exitCode) + case matcher.Expected == -1: + return "Expected process not to exit. It did." } + return format.Message(matcher.Actual, "is less than or equal to exit code: ", matcher.Expected) } -func (m *exitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + +func (matcher *ExitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { session, ok := actual.(*gexec.Session) if ok { return session.ExitCode() == -1 diff --git a/test/utils/utils.go b/test/utils/utils.go index d4d5e6e2f..8d1edb23a 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -397,7 +397,7 @@ func tagOutputToMap(imagesOutput []string) map[string]map[string]bool { return m } -// GetHostDistributionInfo returns a struct with its distribution name and version +// GetHostDistributionInfo returns a struct with its distribution Name and version func GetHostDistributionInfo() HostOS { f, err := os.Open(OSReleasePath) defer f.Close() @@ -491,13 +491,3 @@ func RandomString(n int) string { } return string(b) } - -//SkipIfInContainer skips a test if the test is run inside a container -func SkipIfInContainer(reason string) { - if len(reason) < 5 { - panic("SkipIfInContainer must specify a reason to skip") - } - if os.Getenv("TEST_ENVIRON") == "container" { - Skip("[container]: " + reason) - } -} diff --git a/vendor/github.com/checkpoint-restore/go-criu/v5/magic/types.go b/vendor/github.com/checkpoint-restore/go-criu/v5/magic/types.go new file mode 100644 index 000000000..24cc3989a --- /dev/null +++ b/vendor/github.com/checkpoint-restore/go-criu/v5/magic/types.go @@ -0,0 +1,12 @@ +package magic + +const ( + ImgCommonMagic = 0x54564319 /* Sarov (a.k.a. Arzamas-16) */ + ImgServiceMagic = 0x55105940 /* Zlatoust */ + StatsMagic = 0x57093306 /* Ostashkov */ + + PrimaryMagicOffset = 0x0 + SecondaryMagicOffset = 0x4 + SizeOffset = 0x8 + PayloadOffset = 0xC +) diff --git a/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.pb.go b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.pb.go new file mode 100644 index 000000000..ff011fc2c --- /dev/null +++ b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.pb.go @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: MIT + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.12.4 +// source: stats/stats.proto + +package stats + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This one contains statistics about dump/restore process +type DumpStatsEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FreezingTime *uint32 `protobuf:"varint,1,req,name=freezing_time,json=freezingTime" json:"freezing_time,omitempty"` + FrozenTime *uint32 `protobuf:"varint,2,req,name=frozen_time,json=frozenTime" json:"frozen_time,omitempty"` + MemdumpTime *uint32 `protobuf:"varint,3,req,name=memdump_time,json=memdumpTime" json:"memdump_time,omitempty"` + MemwriteTime *uint32 `protobuf:"varint,4,req,name=memwrite_time,json=memwriteTime" json:"memwrite_time,omitempty"` + PagesScanned *uint64 `protobuf:"varint,5,req,name=pages_scanned,json=pagesScanned" json:"pages_scanned,omitempty"` + PagesSkippedParent *uint64 `protobuf:"varint,6,req,name=pages_skipped_parent,json=pagesSkippedParent" json:"pages_skipped_parent,omitempty"` + PagesWritten *uint64 `protobuf:"varint,7,req,name=pages_written,json=pagesWritten" json:"pages_written,omitempty"` + IrmapResolve *uint32 `protobuf:"varint,8,opt,name=irmap_resolve,json=irmapResolve" json:"irmap_resolve,omitempty"` + PagesLazy *uint64 `protobuf:"varint,9,req,name=pages_lazy,json=pagesLazy" json:"pages_lazy,omitempty"` + PagePipes *uint64 `protobuf:"varint,10,opt,name=page_pipes,json=pagePipes" json:"page_pipes,omitempty"` + PagePipeBufs *uint64 `protobuf:"varint,11,opt,name=page_pipe_bufs,json=pagePipeBufs" json:"page_pipe_bufs,omitempty"` + ShpagesScanned *uint64 `protobuf:"varint,12,opt,name=shpages_scanned,json=shpagesScanned" json:"shpages_scanned,omitempty"` + ShpagesSkippedParent *uint64 `protobuf:"varint,13,opt,name=shpages_skipped_parent,json=shpagesSkippedParent" json:"shpages_skipped_parent,omitempty"` + ShpagesWritten *uint64 `protobuf:"varint,14,opt,name=shpages_written,json=shpagesWritten" json:"shpages_written,omitempty"` +} + +func (x *DumpStatsEntry) Reset() { + *x = DumpStatsEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_stats_stats_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DumpStatsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DumpStatsEntry) ProtoMessage() {} + +func (x *DumpStatsEntry) ProtoReflect() protoreflect.Message { + mi := &file_stats_stats_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DumpStatsEntry.ProtoReflect.Descriptor instead. +func (*DumpStatsEntry) Descriptor() ([]byte, []int) { + return file_stats_stats_proto_rawDescGZIP(), []int{0} +} + +func (x *DumpStatsEntry) GetFreezingTime() uint32 { + if x != nil && x.FreezingTime != nil { + return *x.FreezingTime + } + return 0 +} + +func (x *DumpStatsEntry) GetFrozenTime() uint32 { + if x != nil && x.FrozenTime != nil { + return *x.FrozenTime + } + return 0 +} + +func (x *DumpStatsEntry) GetMemdumpTime() uint32 { + if x != nil && x.MemdumpTime != nil { + return *x.MemdumpTime + } + return 0 +} + +func (x *DumpStatsEntry) GetMemwriteTime() uint32 { + if x != nil && x.MemwriteTime != nil { + return *x.MemwriteTime + } + return 0 +} + +func (x *DumpStatsEntry) GetPagesScanned() uint64 { + if x != nil && x.PagesScanned != nil { + return *x.PagesScanned + } + return 0 +} + +func (x *DumpStatsEntry) GetPagesSkippedParent() uint64 { + if x != nil && x.PagesSkippedParent != nil { + return *x.PagesSkippedParent + } + return 0 +} + +func (x *DumpStatsEntry) GetPagesWritten() uint64 { + if x != nil && x.PagesWritten != nil { + return *x.PagesWritten + } + return 0 +} + +func (x *DumpStatsEntry) GetIrmapResolve() uint32 { + if x != nil && x.IrmapResolve != nil { + return *x.IrmapResolve + } + return 0 +} + +func (x *DumpStatsEntry) GetPagesLazy() uint64 { + if x != nil && x.PagesLazy != nil { + return *x.PagesLazy + } + return 0 +} + +func (x *DumpStatsEntry) GetPagePipes() uint64 { + if x != nil && x.PagePipes != nil { + return *x.PagePipes + } + return 0 +} + +func (x *DumpStatsEntry) GetPagePipeBufs() uint64 { + if x != nil && x.PagePipeBufs != nil { + return *x.PagePipeBufs + } + return 0 +} + +func (x *DumpStatsEntry) GetShpagesScanned() uint64 { + if x != nil && x.ShpagesScanned != nil { + return *x.ShpagesScanned + } + return 0 +} + +func (x *DumpStatsEntry) GetShpagesSkippedParent() uint64 { + if x != nil && x.ShpagesSkippedParent != nil { + return *x.ShpagesSkippedParent + } + return 0 +} + +func (x *DumpStatsEntry) GetShpagesWritten() uint64 { + if x != nil && x.ShpagesWritten != nil { + return *x.ShpagesWritten + } + return 0 +} + +type RestoreStatsEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PagesCompared *uint64 `protobuf:"varint,1,req,name=pages_compared,json=pagesCompared" json:"pages_compared,omitempty"` + PagesSkippedCow *uint64 `protobuf:"varint,2,req,name=pages_skipped_cow,json=pagesSkippedCow" json:"pages_skipped_cow,omitempty"` + ForkingTime *uint32 `protobuf:"varint,3,req,name=forking_time,json=forkingTime" json:"forking_time,omitempty"` + RestoreTime *uint32 `protobuf:"varint,4,req,name=restore_time,json=restoreTime" json:"restore_time,omitempty"` + PagesRestored *uint64 `protobuf:"varint,5,opt,name=pages_restored,json=pagesRestored" json:"pages_restored,omitempty"` +} + +func (x *RestoreStatsEntry) Reset() { + *x = RestoreStatsEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_stats_stats_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RestoreStatsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreStatsEntry) ProtoMessage() {} + +func (x *RestoreStatsEntry) ProtoReflect() protoreflect.Message { + mi := &file_stats_stats_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreStatsEntry.ProtoReflect.Descriptor instead. +func (*RestoreStatsEntry) Descriptor() ([]byte, []int) { + return file_stats_stats_proto_rawDescGZIP(), []int{1} +} + +func (x *RestoreStatsEntry) GetPagesCompared() uint64 { + if x != nil && x.PagesCompared != nil { + return *x.PagesCompared + } + return 0 +} + +func (x *RestoreStatsEntry) GetPagesSkippedCow() uint64 { + if x != nil && x.PagesSkippedCow != nil { + return *x.PagesSkippedCow + } + return 0 +} + +func (x *RestoreStatsEntry) GetForkingTime() uint32 { + if x != nil && x.ForkingTime != nil { + return *x.ForkingTime + } + return 0 +} + +func (x *RestoreStatsEntry) GetRestoreTime() uint32 { + if x != nil && x.RestoreTime != nil { + return *x.RestoreTime + } + return 0 +} + +func (x *RestoreStatsEntry) GetPagesRestored() uint64 { + if x != nil && x.PagesRestored != nil { + return *x.PagesRestored + } + return 0 +} + +type StatsEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Dump *DumpStatsEntry `protobuf:"bytes,1,opt,name=dump" json:"dump,omitempty"` + Restore *RestoreStatsEntry `protobuf:"bytes,2,opt,name=restore" json:"restore,omitempty"` +} + +func (x *StatsEntry) Reset() { + *x = StatsEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_stats_stats_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsEntry) ProtoMessage() {} + +func (x *StatsEntry) ProtoReflect() protoreflect.Message { + mi := &file_stats_stats_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsEntry.ProtoReflect.Descriptor instead. +func (*StatsEntry) Descriptor() ([]byte, []int) { + return file_stats_stats_proto_rawDescGZIP(), []int{2} +} + +func (x *StatsEntry) GetDump() *DumpStatsEntry { + if x != nil { + return x.Dump + } + return nil +} + +func (x *StatsEntry) GetRestore() *RestoreStatsEntry { + if x != nil { + return x.Restore + } + return nil +} + +var File_stats_stats_proto protoreflect.FileDescriptor + +var file_stats_stats_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x04, 0x0a, 0x10, 0x64, 0x75, 0x6d, 0x70, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x73, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x72, 0x65, 0x65, + 0x7a, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x02, 0x28, 0x0d, 0x52, + 0x0c, 0x66, 0x72, 0x65, 0x65, 0x7a, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x66, 0x72, 0x6f, 0x7a, 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x02, + 0x28, 0x0d, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x7a, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x6d, 0x65, 0x6d, 0x64, 0x75, 0x6d, 0x70, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x02, 0x28, 0x0d, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x64, 0x75, 0x6d, 0x70, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x6d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x04, 0x20, 0x02, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x65, 0x6d, 0x77, 0x72, 0x69, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, + 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x18, 0x05, 0x20, 0x02, 0x28, 0x04, 0x52, 0x0c, 0x70, + 0x61, 0x67, 0x65, 0x73, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x70, + 0x61, 0x67, 0x65, 0x73, 0x5f, 0x73, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x02, 0x28, 0x04, 0x52, 0x12, 0x70, 0x61, 0x67, 0x65, 0x73, + 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a, + 0x0d, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x18, 0x07, + 0x20, 0x02, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x73, 0x57, 0x72, 0x69, 0x74, 0x74, + 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x72, 0x6d, 0x61, 0x70, 0x5f, 0x72, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x69, 0x72, 0x6d, 0x61, 0x70, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x73, + 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x18, 0x09, 0x20, 0x02, 0x28, 0x04, 0x52, 0x09, 0x70, 0x61, 0x67, + 0x65, 0x73, 0x4c, 0x61, 0x7a, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x70, + 0x69, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, + 0x50, 0x69, 0x70, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x69, + 0x70, 0x65, 0x5f, 0x62, 0x75, 0x66, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, + 0x61, 0x67, 0x65, 0x50, 0x69, 0x70, 0x65, 0x42, 0x75, 0x66, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x73, + 0x68, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x73, 0x68, 0x70, 0x61, 0x67, 0x65, 0x73, 0x53, 0x63, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x68, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, + 0x73, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x73, 0x68, 0x70, 0x61, 0x67, 0x65, 0x73, 0x53, 0x6b, 0x69, + 0x70, 0x70, 0x65, 0x64, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x68, + 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0e, 0x73, 0x68, 0x70, 0x61, 0x67, 0x65, 0x73, 0x57, 0x72, 0x69, 0x74, + 0x74, 0x65, 0x6e, 0x22, 0xd5, 0x01, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x73, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x61, 0x67, 0x65, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x02, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, + 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x73, 0x6b, 0x69, 0x70, + 0x70, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x02, 0x28, 0x04, 0x52, 0x0f, 0x70, + 0x61, 0x67, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64, 0x43, 0x6f, 0x77, 0x12, 0x21, + 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x02, 0x28, 0x0d, 0x52, 0x0b, 0x66, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x02, 0x28, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x67, 0x65, 0x73, 0x5f, 0x72, 0x65, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, + 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x22, 0x64, 0x0a, 0x0b, 0x73, + 0x74, 0x61, 0x74, 0x73, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x04, 0x64, 0x75, + 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x75, 0x6d, 0x70, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x73, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x75, 0x6d, + 0x70, 0x12, 0x2e, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x73, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, +} + +var ( + file_stats_stats_proto_rawDescOnce sync.Once + file_stats_stats_proto_rawDescData = file_stats_stats_proto_rawDesc +) + +func file_stats_stats_proto_rawDescGZIP() []byte { + file_stats_stats_proto_rawDescOnce.Do(func() { + file_stats_stats_proto_rawDescData = protoimpl.X.CompressGZIP(file_stats_stats_proto_rawDescData) + }) + return file_stats_stats_proto_rawDescData +} + +var file_stats_stats_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_stats_stats_proto_goTypes = []interface{}{ + (*DumpStatsEntry)(nil), // 0: dump_stats_entry + (*RestoreStatsEntry)(nil), // 1: restore_stats_entry + (*StatsEntry)(nil), // 2: stats_entry +} +var file_stats_stats_proto_depIdxs = []int32{ + 0, // 0: stats_entry.dump:type_name -> dump_stats_entry + 1, // 1: stats_entry.restore:type_name -> restore_stats_entry + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_stats_stats_proto_init() } +func file_stats_stats_proto_init() { + if File_stats_stats_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_stats_stats_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DumpStatsEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_stats_stats_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RestoreStatsEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_stats_stats_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatsEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_stats_stats_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_stats_stats_proto_goTypes, + DependencyIndexes: file_stats_stats_proto_depIdxs, + MessageInfos: file_stats_stats_proto_msgTypes, + }.Build() + File_stats_stats_proto = out.File + file_stats_stats_proto_rawDesc = nil + file_stats_stats_proto_goTypes = nil + file_stats_stats_proto_depIdxs = nil +} diff --git a/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.proto b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.proto new file mode 100644 index 000000000..64e46181d --- /dev/null +++ b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/stats.proto @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +syntax = "proto2"; + +// This one contains statistics about dump/restore process +message dump_stats_entry { + required uint32 freezing_time = 1; + required uint32 frozen_time = 2; + required uint32 memdump_time = 3; + required uint32 memwrite_time = 4; + + required uint64 pages_scanned = 5; + required uint64 pages_skipped_parent = 6; + required uint64 pages_written = 7; + + optional uint32 irmap_resolve = 8; + + required uint64 pages_lazy = 9; + optional uint64 page_pipes = 10; + optional uint64 page_pipe_bufs = 11; + + optional uint64 shpages_scanned = 12; + optional uint64 shpages_skipped_parent = 13; + optional uint64 shpages_written = 14; +} + +message restore_stats_entry { + required uint64 pages_compared = 1; + required uint64 pages_skipped_cow = 2; + + required uint32 forking_time = 3; + required uint32 restore_time = 4; + + optional uint64 pages_restored = 5; +} + +message stats_entry { + optional dump_stats_entry dump = 1; + optional restore_stats_entry restore = 2; +} diff --git a/vendor/github.com/checkpoint-restore/go-criu/v5/stats/types.go b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/types.go new file mode 100644 index 000000000..9044ad976 --- /dev/null +++ b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/types.go @@ -0,0 +1,6 @@ +package stats + +const ( + StatsDump = "stats-dump" + StatsRestore = "stats-restore" +) diff --git a/vendor/github.com/checkpoint-restore/go-criu/v5/stats/utils.go b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/utils.go new file mode 100644 index 000000000..368b2039e --- /dev/null +++ b/vendor/github.com/checkpoint-restore/go-criu/v5/stats/utils.go @@ -0,0 +1,54 @@ +package stats + +import ( + "encoding/binary" + "errors" + "io/ioutil" + "os" + "path/filepath" + + "github.com/checkpoint-restore/go-criu/v5/magic" + "google.golang.org/protobuf/proto" +) + +func readStatisticsFile(imgDir *os.File, fileName string) (*StatsEntry, error) { + buf, err := ioutil.ReadFile(filepath.Join(imgDir.Name(), fileName)) + if err != nil { + return nil, err + } + + if binary.LittleEndian.Uint32(buf[magic.PrimaryMagicOffset:magic.SecondaryMagicOffset]) != magic.ImgServiceMagic { + return nil, errors.New("Primary magic not found") + } + + if binary.LittleEndian.Uint32(buf[magic.SecondaryMagicOffset:magic.SizeOffset]) != magic.StatsMagic { + return nil, errors.New("Secondary magic not found") + } + + payloadSize := binary.LittleEndian.Uint32(buf[magic.SizeOffset:magic.PayloadOffset]) + + st := &StatsEntry{} + if err := proto.Unmarshal(buf[magic.PayloadOffset:magic.PayloadOffset+payloadSize], st); err != nil { + return nil, err + } + + return st, nil +} + +func CriuGetDumpStats(imgDir *os.File) (*DumpStatsEntry, error) { + st, err := readStatisticsFile(imgDir, StatsDump) + if err != nil { + return nil, err + } + + return st.GetDump(), nil +} + +func CriuGetRestoreStats(imgDir *os.File) (*RestoreStatsEntry, error) { + st, err := readStatisticsFile(imgDir, StatsRestore) + if err != nil { + return nil, err + } + + return st.GetRestore(), nil +} diff --git a/vendor/github.com/containers/common/libimage/download.go b/vendor/github.com/containers/common/libimage/download.go deleted file mode 100644 index 54edf1b9a..000000000 --- a/vendor/github.com/containers/common/libimage/download.go +++ /dev/null @@ -1,46 +0,0 @@ -package libimage - -import ( - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - - "github.com/pkg/errors" -) - -// tmpdir returns a path to a temporary directory. -func tmpdir() string { - tmpdir := os.Getenv("TMPDIR") - if tmpdir == "" { - tmpdir = "/var/tmp" - } - - return tmpdir -} - -// downloadFromURL downloads an image in the format "https:/example.com/myimage.tar" -// and temporarily saves in it $TMPDIR/importxyz, which is deleted after the image is imported -func (r *Runtime) downloadFromURL(source string) (string, error) { - fmt.Printf("Downloading from %q\n", source) - - outFile, err := ioutil.TempFile(r.systemContext.BigFilesTemporaryDir, "import") - if err != nil { - return "", errors.Wrap(err, "error creating file") - } - defer outFile.Close() - - response, err := http.Get(source) // nolint:noctx - if err != nil { - return "", errors.Wrapf(err, "error downloading %q", source) - } - defer response.Body.Close() - - _, err = io.Copy(outFile, response.Body) - if err != nil { - return "", errors.Wrapf(err, "error saving %s to %s", source, outFile.Name()) - } - - return outFile.Name(), nil -} diff --git a/vendor/github.com/containers/common/libimage/import.go b/vendor/github.com/containers/common/libimage/import.go index bcfb4e129..67ab654b2 100644 --- a/vendor/github.com/containers/common/libimage/import.go +++ b/vendor/github.com/containers/common/libimage/import.go @@ -2,9 +2,11 @@ package libimage import ( "context" + "fmt" "net/url" "os" + "github.com/containers/common/pkg/download" storageTransport "github.com/containers/image/v5/storage" tarballTransport "github.com/containers/image/v5/tarball" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -61,7 +63,8 @@ func (r *Runtime) Import(ctx context.Context, path string, options *ImportOption u, err := url.ParseRequestURI(path) if err == nil && u.Scheme != "" { // If source is a URL, download the file. - file, err := r.downloadFromURL(path) + fmt.Printf("Downloading from %q\n", path) + file, err := download.FromURL(r.systemContext.BigFilesTemporaryDir, path) if err != nil { return "", err } diff --git a/vendor/github.com/containers/common/libimage/inspect.go b/vendor/github.com/containers/common/libimage/inspect.go index 007cbdd89..d44ebf46e 100644 --- a/vendor/github.com/containers/common/libimage/inspect.go +++ b/vendor/github.com/containers/common/libimage/inspect.go @@ -187,7 +187,12 @@ func (i *Image) Inspect(ctx context.Context, options *InspectOptions) (*ImageDat return nil, err } data.Comment = dockerManifest.Comment + // NOTE: Health checks may be listed in the container config or + // the config. data.HealthCheck = dockerManifest.ContainerConfig.Healthcheck + if data.HealthCheck == nil { + data.HealthCheck = dockerManifest.Config.Healthcheck + } } if data.Annotations == nil { diff --git a/vendor/github.com/containers/common/libimage/runtime.go b/vendor/github.com/containers/common/libimage/runtime.go index 7f25df200..d1b6e6cfb 100644 --- a/vendor/github.com/containers/common/libimage/runtime.go +++ b/vendor/github.com/containers/common/libimage/runtime.go @@ -21,6 +21,16 @@ import ( // Faster than the standard library, see https://github.com/json-iterator/go. var json = jsoniter.ConfigCompatibleWithStandardLibrary +// tmpdir returns a path to a temporary directory. +func tmpdir() string { + tmpdir := os.Getenv("TMPDIR") + if tmpdir == "" { + tmpdir = "/var/tmp" + } + + return tmpdir +} + // RuntimeOptions allow for creating a customized Runtime. type RuntimeOptions struct { // The base system context of the runtime which will be used throughout diff --git a/vendor/github.com/containers/common/pkg/config/config.go b/vendor/github.com/containers/common/pkg/config/config.go index 45230703d..2eda0290a 100644 --- a/vendor/github.com/containers/common/pkg/config/config.go +++ b/vendor/github.com/containers/common/pkg/config/config.go @@ -461,6 +461,10 @@ type SetOptions struct { // NetworkConfig represents the "network" TOML config table type NetworkConfig struct { + // NetworkBackend determines what backend should be used for Podman's + // networking. + NetworkBackend string `toml:"network_backend,omitempty"` + // CNIPluginDirs is where CNI plugin binaries are stored. CNIPluginDirs []string `toml:"cni_plugin_dirs,omitempty"` @@ -1151,10 +1155,11 @@ func (c *Config) FindHelperBinary(name string, searchPATH bool) (string, error) if searchPATH { return exec.LookPath(name) } + configHint := "To resolve this error, set the helper_binaries_dir key in the `[engine]` section of containers.conf to the directory containing your helper binaries." if len(c.Engine.HelperBinariesDir) == 0 { - return "", errors.Errorf("could not find %q because there are no helper binary directories configured", name) + return "", errors.Errorf("could not find %q because there are no helper binary directories configured. %s", name, configHint) } - return "", errors.Errorf("could not find %q in one of %v", name, c.Engine.HelperBinariesDir) + return "", errors.Errorf("could not find %q in one of %v. %s", name, c.Engine.HelperBinariesDir, configHint) } // ImageCopyTmpDir default directory to store tempory image files during copy diff --git a/vendor/github.com/containers/common/pkg/config/containers.conf b/vendor/github.com/containers/common/pkg/config/containers.conf index 1d3c003e3..0c3ea8054 100644 --- a/vendor/github.com/containers/common/pkg/config/containers.conf +++ b/vendor/github.com/containers/common/pkg/config/containers.conf @@ -260,6 +260,10 @@ default_sysctls = [ [network] +# Network backend to use. Default "CNI". +# +#network_backend = "cni" + # Path to directory where CNI plugin binaries are located. # #cni_plugin_dirs = [ diff --git a/vendor/github.com/containers/common/pkg/config/default.go b/vendor/github.com/containers/common/pkg/config/default.go index e72e1b3e4..515c46e8b 100644 --- a/vendor/github.com/containers/common/pkg/config/default.go +++ b/vendor/github.com/containers/common/pkg/config/default.go @@ -201,6 +201,7 @@ func DefaultConfig() (*Config, error) { UserNSSize: DefaultUserNSSize, }, Network: NetworkConfig{ + NetworkBackend: "cni", DefaultNetwork: "podman", DefaultSubnet: DefaultSubnet, NetworkConfigDir: cniConfig, diff --git a/vendor/github.com/containers/common/pkg/download/download.go b/vendor/github.com/containers/common/pkg/download/download.go new file mode 100644 index 000000000..abf4c8773 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/download/download.go @@ -0,0 +1,31 @@ +package download + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// FromURL downloads the specified source to a file in tmpdir (OS defaults if +// empty). +func FromURL(tmpdir, source string) (string, error) { + tmp, err := ioutil.TempFile(tmpdir, "") + if err != nil { + return "", fmt.Errorf("creating temporary download file: %w", err) + } + defer tmp.Close() + + response, err := http.Get(source) // nolint:noctx + if err != nil { + return "", fmt.Errorf("downloading %s: %w", source, err) + } + defer response.Body.Close() + + _, err = io.Copy(tmp, response.Body) + if err != nil { + return "", fmt.Errorf("copying %s to %s: %w", source, tmp.Name(), err) + } + + return tmp.Name(), nil +} diff --git a/vendor/github.com/containers/common/pkg/seccomp/default_linux.go b/vendor/github.com/containers/common/pkg/seccomp/default_linux.go index cf333744c..d196384f0 100644 --- a/vendor/github.com/containers/common/pkg/seccomp/default_linux.go +++ b/vendor/github.com/containers/common/pkg/seccomp/default_linux.go @@ -80,6 +80,7 @@ func DefaultProfile() *Seccomp { "vmsplice", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, }, @@ -574,6 +575,7 @@ func DefaultProfile() *Seccomp { "open_by_handle_at", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -609,6 +611,7 @@ func DefaultProfile() *Seccomp { "setns", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -630,6 +633,7 @@ func DefaultProfile() *Seccomp { "chroot", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -657,6 +661,7 @@ func DefaultProfile() *Seccomp { "query_module", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -678,6 +683,7 @@ func DefaultProfile() *Seccomp { "acct", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -707,6 +713,7 @@ func DefaultProfile() *Seccomp { "ptrace", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -730,6 +737,7 @@ func DefaultProfile() *Seccomp { "ioperm", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -757,6 +765,7 @@ func DefaultProfile() *Seccomp { "clock_settime64", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -778,6 +787,7 @@ func DefaultProfile() *Seccomp { "vhangup", }, Action: ActErrno, + Errno: "EPERM", ErrnoRet: &eperm, Args: []*Arg{}, Excludes: Filter{ @@ -789,6 +799,7 @@ func DefaultProfile() *Seccomp { "socket", }, Action: ActErrno, + Errno: "EINVAL", ErrnoRet: &einval, Args: []*Arg{ { @@ -867,6 +878,7 @@ func DefaultProfile() *Seccomp { return &Seccomp{ DefaultAction: ActErrno, + DefaultErrno: "ENOSYS", DefaultErrnoRet: &enosys, ArchMap: arches(), Syscalls: syscalls, diff --git a/vendor/github.com/containers/common/pkg/seccomp/errno_list.go b/vendor/github.com/containers/common/pkg/seccomp/errno_list.go new file mode 100644 index 000000000..55b92ecc8 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/seccomp/errno_list.go @@ -0,0 +1,91 @@ +package seccomp + +import ( + "golang.org/x/sys/unix" +) + +// Error table +var errnoArch = map[string]uint{ + "EPERM": uint(unix.EPERM), + "ENOENT": uint(unix.ENOENT), + "ESRCH": uint(unix.ESRCH), + "EIO": uint(unix.EIO), + "ENXIO": uint(unix.ENXIO), + "E2BIG": uint(unix.E2BIG), + "ENOEXEC": uint(unix.ENOEXEC), + "EBADF": uint(unix.EBADF), + "ECHILD": uint(unix.ECHILD), + "EDEADLK": uint(unix.EDEADLK), + "ENOMEM": uint(unix.ENOMEM), + "EACCES": uint(unix.EACCES), + "EFAULT": uint(unix.EFAULT), + "ENOTBLK": uint(unix.ENOTBLK), + "EBUSY": uint(unix.EBUSY), + "EEXIST": uint(unix.EEXIST), + "EXDEV": uint(unix.EXDEV), + "ENODEV": uint(unix.ENODEV), + "ENOTDIR": uint(unix.ENOTDIR), + "EISDIR": uint(unix.EISDIR), + "EINVAL": uint(unix.EINVAL), + "ENFILE": uint(unix.ENFILE), + "EMFILE": uint(unix.EMFILE), + "ENOTTY": uint(unix.ENOTTY), + "ETXTBSY": uint(unix.ETXTBSY), + "EFBIG": uint(unix.EFBIG), + "ENOSPC": uint(unix.ENOSPC), + "ESPIPE": uint(unix.ESPIPE), + "EROFS": uint(unix.EROFS), + "EMLINK": uint(unix.EMLINK), + "EPIPE": uint(unix.EPIPE), + "EDOM": uint(unix.EDOM), + "ERANGE": uint(unix.ERANGE), + "EAGAIN": uint(unix.EAGAIN), + "EINPROGRESS": uint(unix.EINPROGRESS), + "EALREADY": uint(unix.EALREADY), + "ENOTSOCK": uint(unix.ENOTSOCK), + "EDESTADDRREQ": uint(unix.EDESTADDRREQ), + "EMSGSIZE": uint(unix.EMSGSIZE), + "EPROTOTYPE": uint(unix.EPROTOTYPE), + "ENOPROTOOPT": uint(unix.ENOPROTOOPT), + "EPROTONOSUPPORT": uint(unix.EPROTONOSUPPORT), + "ESOCKTNOSUPPORT": uint(unix.ESOCKTNOSUPPORT), + "EOPNOTSUPP": uint(unix.EOPNOTSUPP), + "EPFNOSUPPORT": uint(unix.EPFNOSUPPORT), + "EAFNOSUPPORT": uint(unix.EAFNOSUPPORT), + "EADDRINUSE": uint(unix.EADDRINUSE), + "EADDRNOTAVAIL": uint(unix.EADDRNOTAVAIL), + "ENETDOWN": uint(unix.ENETDOWN), + "ENETUNREACH": uint(unix.ENETUNREACH), + "ENETRESET": uint(unix.ENETRESET), + "ECONNABORTED": uint(unix.ECONNABORTED), + "ECONNRESET": uint(unix.ECONNRESET), + "ENOBUFS": uint(unix.ENOBUFS), + "EISCONN": uint(unix.EISCONN), + "ENOTCONN": uint(unix.ENOTCONN), + "ESHUTDOWN": uint(unix.ESHUTDOWN), + "ETOOMANYREFS": uint(unix.ETOOMANYREFS), + "ETIMEDOUT": uint(unix.ETIMEDOUT), + "ECONNREFUSED": uint(unix.ECONNREFUSED), + "ELOOP": uint(unix.ELOOP), + "ENAMETOOLONG": uint(unix.ENAMETOOLONG), + "EHOSTDOWN": uint(unix.EHOSTDOWN), + "EHOSTUNREACH": uint(unix.EHOSTUNREACH), + "ENOTEMPTY": uint(unix.ENOTEMPTY), + "EUSERS": uint(unix.EUSERS), + "EDQUOT": uint(unix.EDQUOT), + "ESTALE": uint(unix.ESTALE), + "EREMOTE": uint(unix.EREMOTE), + "ENOLCK": uint(unix.ENOLCK), + "ENOSYS": uint(unix.ENOSYS), + "EILSEQ": uint(unix.EILSEQ), + "ENOMEDIUM": uint(unix.ENOMEDIUM), + "EMEDIUMTYPE": uint(unix.EMEDIUMTYPE), + "EOVERFLOW": uint(unix.EOVERFLOW), + "ECANCELED": uint(unix.ECANCELED), + "EIDRM": uint(unix.EIDRM), + "ENOMSG": uint(unix.ENOMSG), + "ENOTSUP": uint(unix.ENOTSUP), + "EBADMSG": uint(unix.EBADMSG), + "ENOTRECOVERABLE": uint(unix.ENOTRECOVERABLE), + "EOWNERDEAD": uint(unix.EOWNERDEAD), +} diff --git a/vendor/github.com/containers/common/pkg/seccomp/seccomp.json b/vendor/github.com/containers/common/pkg/seccomp/seccomp.json index c009134e3..9314eb3cc 100644 --- a/vendor/github.com/containers/common/pkg/seccomp/seccomp.json +++ b/vendor/github.com/containers/common/pkg/seccomp/seccomp.json @@ -1,6 +1,7 @@ { "defaultAction": "SCMP_ACT_ERRNO", "defaultErrnoRet": 38, + "defaultErrno": "ENOSYS", "archMap": [ { "architecture": "SCMP_ARCH_X86_64", @@ -87,7 +88,8 @@ "comment": "", "includes": {}, "excludes": {}, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -650,7 +652,8 @@ "CAP_DAC_READ_SEARCH" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -693,7 +696,8 @@ "CAP_SYS_ADMIN" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -722,7 +726,8 @@ "CAP_SYS_CHROOT" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -757,7 +762,8 @@ "CAP_SYS_MODULE" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -786,7 +792,8 @@ "CAP_SYS_PACCT" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -823,7 +830,8 @@ "CAP_SYS_PTRACE" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -854,7 +862,8 @@ "CAP_SYS_RAWIO" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -889,7 +898,8 @@ "CAP_SYS_TIME" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -918,7 +928,8 @@ "CAP_SYS_TTY_CONFIG" ] }, - "errnoRet": 1 + "errnoRet": 1, + "errno": "EPERM" }, { "names": [ @@ -946,7 +957,8 @@ "CAP_AUDIT_WRITE" ] }, - "errnoRet": 22 + "errnoRet": 22, + "errno": "EINVAL" }, { "names": [ diff --git a/vendor/github.com/containers/common/pkg/seccomp/seccomp_linux.go b/vendor/github.com/containers/common/pkg/seccomp/seccomp_linux.go index af36b9990..0c022ac7a 100644 --- a/vendor/github.com/containers/common/pkg/seccomp/seccomp_linux.go +++ b/vendor/github.com/containers/common/pkg/seccomp/seccomp_linux.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "github.com/opencontainers/runtime-spec/specs-go" libseccomp "github.com/seccomp/libseccomp-golang" @@ -66,6 +67,37 @@ func inSlice(slice []string, s string) bool { return false } +func getArchitectures(config *Seccomp, newConfig *specs.LinuxSeccomp) error { + if len(config.Architectures) != 0 && len(config.ArchMap) != 0 { + return errors.New("'architectures' and 'archMap' were specified in the seccomp profile, use either 'architectures' or 'archMap'") + } + + // if config.Architectures == 0 then libseccomp will figure out the architecture to use + if len(config.Architectures) != 0 { + for _, a := range config.Architectures { + newConfig.Architectures = append(newConfig.Architectures, specs.Arch(a)) + } + } + return nil +} + +func getErrno(errno string, def *uint) (*uint, error) { + if errno == "" { + return def, nil + } + v, err := strconv.ParseUint(errno, 10, 32) + if err == nil { + v2 := uint(v) + return &v2, nil + } + + v2, found := errnoArch[errno] + if !found { + return nil, fmt.Errorf("unknown errno %s", errno) + } + return &v2, nil +} + func setupSeccomp(config *Seccomp, rs *specs.Spec) (*specs.LinuxSeccomp, error) { if config == nil { return nil, nil @@ -84,15 +116,8 @@ func setupSeccomp(config *Seccomp, rs *specs.Spec) (*specs.LinuxSeccomp, error) arch = native.String() } - if len(config.Architectures) != 0 && len(config.ArchMap) != 0 { - return nil, errors.New("'architectures' and 'archMap' were specified in the seccomp profile, use either 'architectures' or 'archMap'") - } - - // if config.Architectures == 0 then libseccomp will figure out the architecture to use - if len(config.Architectures) != 0 { - for _, a := range config.Architectures { - newConfig.Architectures = append(newConfig.Architectures, specs.Arch(a)) - } + if err := getArchitectures(config, newConfig); err != nil { + return nil, err } if len(config.ArchMap) != 0 { @@ -111,7 +136,11 @@ func setupSeccomp(config *Seccomp, rs *specs.Spec) (*specs.LinuxSeccomp, error) } newConfig.DefaultAction = specs.LinuxSeccompAction(config.DefaultAction) - newConfig.DefaultErrnoRet = config.DefaultErrnoRet + + newConfig.DefaultErrnoRet, err = getErrno(config.DefaultErrno, config.DefaultErrnoRet) + if err != nil { + return nil, err + } Loop: // Loop through all syscall blocks and convert them to libcontainer format after filtering them @@ -145,12 +174,17 @@ Loop: return nil, errors.New("'name' and 'names' were specified in the seccomp profile, use either 'name' or 'names'") } + errno, err := getErrno(call.Errno, call.ErrnoRet) + if err != nil { + return nil, err + } + if call.Name != "" { - newConfig.Syscalls = append(newConfig.Syscalls, createSpecsSyscall([]string{call.Name}, call.Action, call.Args, call.ErrnoRet)) + newConfig.Syscalls = append(newConfig.Syscalls, createSpecsSyscall([]string{call.Name}, call.Action, call.Args, errno)) } if len(call.Names) > 0 { - newConfig.Syscalls = append(newConfig.Syscalls, createSpecsSyscall(call.Names, call.Action, call.Args, call.ErrnoRet)) + newConfig.Syscalls = append(newConfig.Syscalls, createSpecsSyscall(call.Names, call.Action, call.Args, errno)) } } diff --git a/vendor/github.com/containers/common/pkg/seccomp/types.go b/vendor/github.com/containers/common/pkg/seccomp/types.go index 07751f729..a8a9e9d4f 100644 --- a/vendor/github.com/containers/common/pkg/seccomp/types.go +++ b/vendor/github.com/containers/common/pkg/seccomp/types.go @@ -6,8 +6,12 @@ package seccomp // Seccomp represents the config for a seccomp profile for syscall restriction. type Seccomp struct { - DefaultAction Action `json:"defaultAction"` + DefaultAction Action `json:"defaultAction"` + + // DefaultErrnoRet is obsolete, please use DefaultErrno DefaultErrnoRet *uint `json:"defaultErrnoRet,omitempty"` + DefaultErrno string `json:"defaultErrno,omitempty"` + // Architectures is kept to maintain backward compatibility with the old // seccomp profile. Architectures []Arch `json:"architectures,omitempty"` @@ -107,5 +111,7 @@ type Syscall struct { Comment string `json:"comment"` Includes Filter `json:"includes"` Excludes Filter `json:"excludes"` - ErrnoRet *uint `json:"errnoRet,omitempty"` + // ErrnoRet is obsolete, please use Errno + ErrnoRet *uint `json:"errnoRet,omitempty"` + Errno string `json:"errno,omitempty"` } diff --git a/vendor/modules.txt b/vendor/modules.txt index 2312c65ff..9b6bd34bf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -47,7 +47,9 @@ github.com/cespare/xxhash/v2 github.com/checkpoint-restore/checkpointctl/lib # github.com/checkpoint-restore/go-criu/v5 v5.2.0 github.com/checkpoint-restore/go-criu/v5 +github.com/checkpoint-restore/go-criu/v5/magic github.com/checkpoint-restore/go-criu/v5/rpc +github.com/checkpoint-restore/go-criu/v5/stats # github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline # github.com/container-orchestrated-devices/container-device-interface v0.0.0-20210325223243-f99e8b6c10b9 @@ -95,7 +97,7 @@ github.com/containers/buildah/pkg/rusage github.com/containers/buildah/pkg/sshagent github.com/containers/buildah/pkg/util github.com/containers/buildah/util -# github.com/containers/common v0.46.1-0.20211026130826-7abfd453c86f +# github.com/containers/common v0.46.1-0.20211110143743-73e7b462c358 github.com/containers/common/libimage github.com/containers/common/libimage/manifests github.com/containers/common/pkg/apparmor @@ -107,6 +109,7 @@ github.com/containers/common/pkg/chown github.com/containers/common/pkg/completion github.com/containers/common/pkg/config github.com/containers/common/pkg/defaultnet +github.com/containers/common/pkg/download github.com/containers/common/pkg/filters github.com/containers/common/pkg/flag github.com/containers/common/pkg/manifests @@ -489,7 +492,7 @@ github.com/onsi/ginkgo/reporters/stenographer github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty github.com/onsi/ginkgo/types -# github.com/onsi/gomega v1.16.0 +# github.com/onsi/gomega v1.17.0 => github.com/onsi/gomega v1.16.0 github.com/onsi/gomega github.com/onsi/gomega/format github.com/onsi/gomega/gbytes |