diff options
-rw-r--r-- | cmd/podman/common/create_opts.go | 7 | ||||
-rw-r--r-- | cmd/podman/common/netflags.go | 2 | ||||
-rw-r--r-- | libpod/container.go | 3 | ||||
-rw-r--r-- | libpod/container_log_linux.go | 276 | ||||
-rw-r--r-- | libpod/runtime.go | 6 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_create.go | 2 | ||||
-rw-r--r-- | pkg/specgen/generate/namespaces.go | 2 | ||||
-rw-r--r-- | pkg/specgen/namespaces.go | 8 | ||||
-rw-r--r-- | test/e2e/logs_test.go | 2 | ||||
-rw-r--r-- | test/e2e/run_networking_test.go | 14 | ||||
-rw-r--r-- | test/system/035-logs.bats | 52 | ||||
-rw-r--r-- | test/system/130-kill.bats | 3 | ||||
-rw-r--r-- | vendor/github.com/containers/common/pkg/defaultnet/default_network.go | 222 | ||||
-rw-r--r-- | vendor/modules.txt | 1 |
14 files changed, 473 insertions, 127 deletions
diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 77ac781a5..76d7345fc 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/cgroups" @@ -140,7 +141,7 @@ func stringMaptoArray(m map[string]string) []string { // ContainerCreateToContainerCLIOpts converts a compat input struct to cliopts so it can be converted to // a specgen spec. -func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroupsManager string) (*ContainerCLIOpts, []string, error) { +func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*ContainerCLIOpts, []string, error) { var ( capAdd []string cappDrop []string @@ -248,7 +249,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup } // netMode - nsmode, _, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode)) + nsmode, _, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode), true) if err != nil { return nil, nil, err } @@ -507,7 +508,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup cliOpts.Restart = policy } - if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && cgroupsManager == "systemd") { + if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && rtc.Engine.CgroupManager == "systemd") { cliOpts.MemorySwappiness = *cc.HostConfig.MemorySwappiness } else { cliOpts.MemorySwappiness = -1 diff --git a/cmd/podman/common/netflags.go b/cmd/podman/common/netflags.go index 4d0a554a6..9941bc716 100644 --- a/cmd/podman/common/netflags.go +++ b/cmd/podman/common/netflags.go @@ -201,7 +201,7 @@ func NetFlagsToNetOptions(cmd *cobra.Command) (*entities.NetOptions, error) { parts := strings.SplitN(network, ":", 2) - ns, cniNets, err := specgen.ParseNetworkNamespace(network) + ns, cniNets, err := specgen.ParseNetworkNamespace(network, containerConfig.Containers.RootlessNetworking == "cni") if err != nil { return nil, err } diff --git a/libpod/container.go b/libpod/container.go index 591cf9bc5..c6f0cd618 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -14,7 +14,6 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/lock" - "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/storage" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -1168,7 +1167,7 @@ func (c *Container) Networks() ([]string, bool, error) { func (c *Container) networks() ([]string, bool, error) { networks, err := c.runtime.state.GetNetworks(c) if err != nil && errors.Cause(err) == define.ErrNoSuchNetwork { - if len(c.config.Networks) == 0 && !rootless.IsRootless() { + if len(c.config.Networks) == 0 && c.config.NetMode.IsBridge() { return []string{c.runtime.netPlugin.GetDefaultNetworkName()}, true, nil } return c.config.Networks, false, nil diff --git a/libpod/container_log_linux.go b/libpod/container_log_linux.go index ec4fa9724..892ee34e3 100644 --- a/libpod/container_log_linux.go +++ b/libpod/container_log_linux.go @@ -6,14 +6,12 @@ package libpod import ( "context" "fmt" - "io" - "math" "strings" "time" - "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/libpod/logs" - journal "github.com/coreos/go-systemd/v22/sdjournal" + "github.com/coreos/go-systemd/v22/sdjournal" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -24,122 +22,187 @@ const ( // journaldLogErr is the journald priority signifying stderr journaldLogErr = "3" - - // bufLen is the length of the buffer to read from a k8s-file - // formatted log line - // let's set it as 2k just to be safe if k8s-file format ever changes - bufLen = 16384 ) func (c *Container) readFromJournal(ctx context.Context, options *logs.LogOptions, logChannel chan *logs.LogLine) error { - var config journal.JournalReaderConfig - if options.Tail < 0 { - config.NumFromTail = 0 - } else if options.Tail == 0 { - config.NumFromTail = math.MaxUint64 - } else { - config.NumFromTail = uint64(options.Tail) + journal, err := sdjournal.NewJournal() + if err != nil { + return err } - if options.Multi { - config.Formatter = journalFormatterWithID - } else { - config.Formatter = journalFormatter - } - defaultTime := time.Time{} - if options.Since != defaultTime { - // coreos/go-systemd/sdjournal doesn't correctly handle requests for data in the future - // return nothing instead of falsely printing - if time.Now().Before(options.Since) { - return nil - } - // coreos/go-systemd/sdjournal expects a negative time.Duration for times in the past - config.Since = -time.Since(options.Since) + // While logs are written to the `logChannel`, we inspect each event + // and stop once the container has died. Having logs and events in one + // stream prevents a race condition that we faced in #10323. + + // Add the filters for events. + match := sdjournal.Match{Field: "SYSLOG_IDENTIFIER", Value: "podman"} + if err := journal.AddMatch(match.String()); err != nil { + return errors.Wrapf(err, "adding filter to journald logger: %v", match) + } + match = sdjournal.Match{Field: "PODMAN_ID", Value: c.ID()} + if err := journal.AddMatch(match.String()); err != nil { + return errors.Wrapf(err, "adding filter to journald logger: %v", match) } - config.Matches = append(config.Matches, journal.Match{ - Field: "CONTAINER_ID_FULL", - Value: c.ID(), - }) - options.WaitGroup.Add(1) - r, err := journal.NewJournalReader(config) - if err != nil { + // Add the filter for logs. Note the disjunction so that we match + // either the events or the logs. + if err := journal.AddDisjunction(); err != nil { + return errors.Wrap(err, "adding filter disjunction to journald logger") + } + match = sdjournal.Match{Field: "CONTAINER_ID_FULL", Value: c.ID()} + if err := journal.AddMatch(match.String()); err != nil { + return errors.Wrapf(err, "adding filter to journald logger: %v", match) + } + + if err := journal.SeekHead(); err != nil { return err } - if r == nil { - return errors.Errorf("journal reader creation failed") + // API requires Next() immediately after SeekHead(). + if _, err := journal.Next(); err != nil { + return errors.Wrap(err, "initial journal cursor") } - if options.Tail == math.MaxInt64 { - r.Rewind() + + // API requires a next|prev before getting a cursor. + if _, err := journal.Previous(); err != nil { + return errors.Wrap(err, "initial journal cursor") } - state, err := c.State() - if err != nil { - return err + + // Note that the initial cursor may not yet be ready, so we'll do an + // exponential backoff. + var cursor string + var cursorError error + for i := 1; i <= 3; i++ { + cursor, cursorError = journal.GetCursor() + if err != nil { + continue + } + time.Sleep(time.Duration(i*100) * time.Millisecond) + break + } + if cursorError != nil { + return errors.Wrap(cursorError, "inital journal cursor") + } + + // We need the container's events in the same journal to guarantee + // consistency, see #10323. + if options.Follow && c.runtime.config.Engine.EventsLogger != "journald" { + return errors.Errorf("using --follow with the journald --log-driver but without the journald --events-backend (%s) is not supported", c.runtime.config.Engine.EventsLogger) } - if options.Follow && state == define.ContainerStateRunning { - go func() { - done := make(chan bool) - until := make(chan time.Time) - go func() { - select { - case <-ctx.Done(): - until <- time.Time{} - case <-done: - // nothing to do anymore + options.WaitGroup.Add(1) + go func() { + defer func() { + options.WaitGroup.Done() + if err := journal.Close(); err != nil { + logrus.Errorf("Unable to close journal: %v", err) + } + }() + + afterTimeStamp := false // needed for options.Since + tailQueue := []*logs.LogLine{} // needed for options.Tail + doTail := options.Tail > 0 + for { + select { + case <-ctx.Done(): + // Remote client may have closed/lost the connection. + return + default: + // Fallthrough + } + + if _, err := journal.Next(); err != nil { + logrus.Errorf("Failed to move journal cursor to next entry: %v", err) + return + } + latestCursor, err := journal.GetCursor() + if err != nil { + logrus.Errorf("Failed to get journal cursor: %v", err) + return + } + + // Hit the end of the journal. + if cursor == latestCursor { + if doTail { + // Flush *once* we hit the end of the journal. + startIndex := int64(len(tailQueue)-1) - options.Tail + if startIndex < 0 { + startIndex = 0 + } + for i := startIndex; i < int64(len(tailQueue)); i++ { + logChannel <- tailQueue[i] + } + tailQueue = nil + doTail = false + } + // Unless we follow, quit. + if !options.Follow { + return } - }() - go func() { - // FIXME (#10323): we are facing a terrible - // race condition here. At the time the - // container dies and `c.Wait()` has returned, - // we may not have received all journald logs. - // So far there is no other way than waiting - // for a second. Ultimately, `r.Follow` is - // racy and we may have to implement our custom - // logic here. - c.Wait(ctx) - time.Sleep(time.Second) - until <- time.Time{} - }() - follower := journaldFollowBuffer{logChannel, options.Multi} - err := r.Follow(until, follower) + // Sleep until something's happening on the journal. + journal.Wait(sdjournal.IndefiniteWait) + continue + } + cursor = latestCursor + + entry, err := journal.GetEntry() if err != nil { - logrus.Debugf(err.Error()) + logrus.Errorf("Failed to get journal entry: %v", err) + return } - r.Close() - options.WaitGroup.Done() - done <- true - return - }() - return nil - } - go func() { - bytes := make([]byte, bufLen) - // /me complains about no do-while in go - ec, err := r.Read(bytes) - for ec != 0 && err == nil { - // because we are reusing bytes, we need to make - // sure the old data doesn't get into the new line - bytestr := string(bytes[:ec]) - logLine, err2 := logs.NewJournaldLogLine(bytestr, options.Multi) - if err2 != nil { - logrus.Error(err2) + if !afterTimeStamp { + entryTime := time.Unix(0, int64(entry.RealtimeTimestamp)*int64(time.Microsecond)) + if entryTime.Before(options.Since) { + continue + } + afterTimeStamp = true + } + + // If we're reading an event and the container exited/died, + // then we're done and can return. + event, ok := entry.Fields["PODMAN_EVENT"] + if ok { + status, err := events.StringToStatus(event) + if err != nil { + logrus.Errorf("Failed to translate event: %v", err) + return + } + if status == events.Exited { + return + } + continue + } + + var message string + var formatError error + + if options.Multi { + message, formatError = journalFormatterWithID(entry) + } else { + message, formatError = journalFormatter(entry) + } + + if formatError != nil { + logrus.Errorf("Failed to parse journald log entry: %v", err) + return + } + + logLine, err := logs.NewJournaldLogLine(message, options.Multi) + if err != nil { + logrus.Errorf("Failed parse log line: %v", err) + return + } + if doTail { + tailQueue = append(tailQueue, logLine) continue } logChannel <- logLine - ec, err = r.Read(bytes) } - if err != nil && err != io.EOF { - logrus.Error(err) - } - r.Close() - options.WaitGroup.Done() }() + return nil } -func journalFormatterWithID(entry *journal.JournalEntry) (string, error) { +func journalFormatterWithID(entry *sdjournal.JournalEntry) (string, error) { output, err := formatterPrefix(entry) if err != nil { return "", err @@ -162,7 +225,7 @@ func journalFormatterWithID(entry *journal.JournalEntry) (string, error) { return output, nil } -func journalFormatter(entry *journal.JournalEntry) (string, error) { +func journalFormatter(entry *sdjournal.JournalEntry) (string, error) { output, err := formatterPrefix(entry) if err != nil { return "", err @@ -176,7 +239,7 @@ func journalFormatter(entry *journal.JournalEntry) (string, error) { return output, nil } -func formatterPrefix(entry *journal.JournalEntry) (string, error) { +func formatterPrefix(entry *sdjournal.JournalEntry) (string, error) { usec := entry.RealtimeTimestamp tsString := time.Unix(0, int64(usec)*int64(time.Microsecond)).Format(logs.LogTimeFormat) output := fmt.Sprintf("%s ", tsString) @@ -202,7 +265,7 @@ func formatterPrefix(entry *journal.JournalEntry) (string, error) { return output, nil } -func formatterMessage(entry *journal.JournalEntry) (string, error) { +func formatterMessage(entry *sdjournal.JournalEntry) (string, error) { // Finally, append the message msg, ok := entry.Fields["MESSAGE"] if !ok { @@ -211,18 +274,3 @@ func formatterMessage(entry *journal.JournalEntry) (string, error) { msg = strings.TrimSuffix(msg, "\n") return msg, nil } - -type journaldFollowBuffer struct { - logChannel chan *logs.LogLine - withID bool -} - -func (f journaldFollowBuffer) Write(p []byte) (int, error) { - bytestr := string(p) - logLine, err := logs.NewJournaldLogLine(bytestr, f.withID) - if err != nil { - return -1, err - } - f.logChannel <- logLine - return len(p), nil -} diff --git a/libpod/runtime.go b/libpod/runtime.go index e551e6fe8..d14048311 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -17,6 +17,7 @@ import ( "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/defaultnet" "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/pkg/sysregistriesv2" is "github.com/containers/image/v5/storage" @@ -458,6 +459,11 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } } + // If we need to make a default network - do so now. + if err := defaultnet.Create(runtime.config.Network.DefaultNetwork, runtime.config.Network.DefaultSubnet, runtime.config.Network.NetworkConfigDir, runtime.config.Engine.StaticDir, runtime.config.Engine.MachineEnabled); err != nil { + logrus.Errorf("Failed to created default CNI network: %v", err) + } + // Set up the CNI net plugin netPlugin, err := ocicni.InitCNI(runtime.config.Network.DefaultNetwork, runtime.config.Network.NetworkConfigDir, runtime.config.Network.CNIPluginDirs...) if err != nil { diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 162a98135..8e9e1fb39 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -62,7 +62,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } // Take body structure and convert to cliopts - cliOpts, args, err := common.ContainerCreateToContainerCLIOpts(body, rtc.Engine.CgroupManager) + cliOpts, args, err := common.ContainerCreateToContainerCLIOpts(body, rtc) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "make cli opts()")) return diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 278f35c22..f41186ae4 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -66,7 +66,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) case "cgroup": return specgen.ParseCgroupNamespace(cfg.Containers.CgroupNS) case "net": - ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS) + ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS, cfg.Containers.RootlessNetworking == "cni") return ns, err } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index f665fc0be..80852930a 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -253,7 +253,7 @@ func ParseUserNamespace(ns string) (Namespace, error) { // ParseNetworkNamespace parses a network namespace specification in string // form. // Returns a namespace and (optionally) a list of CNI networks to join. -func ParseNetworkNamespace(ns string) (Namespace, []string, error) { +func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, []string, error) { toReturn := Namespace{} var cniNetworks []string // Net defaults to Slirp on rootless @@ -264,7 +264,11 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { toReturn.NSMode = FromPod case ns == "" || ns == string(Default) || ns == string(Private): if rootless.IsRootless() { - toReturn.NSMode = Slirp + if rootlessDefaultCNI { + toReturn.NSMode = Bridge + } else { + toReturn.NSMode = Slirp + } } else { toReturn.NSMode = Bridge } diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go index 3051031a5..4d9cbb48b 100644 --- a/test/e2e/logs_test.go +++ b/test/e2e/logs_test.go @@ -163,7 +163,7 @@ var _ = Describe("Podman logs", func() { }) It("podman logs on a created container should result in 0 exit code: "+log, func() { - session := podmanTest.Podman([]string{"create", "-t", "--name", "log", ALPINE}) + session := podmanTest.Podman([]string{"create", "--log-driver", log, "-t", "--name", "log", ALPINE}) session.WaitWithDefaultTimeout() Expect(session).To(Exit(0)) diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go index 37e837b1d..696cec76c 100644 --- a/test/e2e/run_networking_test.go +++ b/test/e2e/run_networking_test.go @@ -786,4 +786,18 @@ var _ = Describe("Podman run networking", func() { Expect(session.ExitCode()).To(BeZero()) Expect(session.OutputToString()).To(ContainSubstring("search dns.podman")) }) + + It("Rootless podman run with --net=bridge works and connects to default network", func() { + // This is harmless when run as root, so we'll just let it run. + ctrName := "testctr" + ctr := podmanTest.Podman([]string{"run", "-d", "--net=bridge", "--name", ctrName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + inspectOut := podmanTest.InspectContainer(ctrName) + Expect(len(inspectOut)).To(Equal(1)) + Expect(len(inspectOut[0].NetworkSettings.Networks)).To(Equal(1)) + _, ok := inspectOut[0].NetworkSettings.Networks["podman"] + Expect(ok).To(BeTrue()) + }) }) diff --git a/test/system/035-logs.bats b/test/system/035-logs.bats index 3dd88e5eb..ccf83df14 100644 --- a/test/system/035-logs.bats +++ b/test/system/035-logs.bats @@ -73,4 +73,56 @@ ${cid[0]} d" "Sequential output from logs" _log_test_multi journald } +@test "podman logs - journald log driver requires journald events backend" { + skip_if_remote "remote does not support --events-backend" + # We can't use journald on RHEL as rootless: rhbz#1895105 + skip_if_journald_unavailable + + run_podman --events-backend=file run --log-driver=journald -d --name test --replace $IMAGE ls / + run_podman --events-backend=file logs test + run_podman 125 --events-backend=file logs --follow test + is "$output" "Error: using --follow with the journald --log-driver but without the journald --events-backend (file) is not supported" "journald logger requires journald eventer" +} + +function _log_test_since() { + local driver=$1 + + s_before="before_$(random_string)_${driver}" + s_after="after_$(random_string)_${driver}" + + before=$(date --iso-8601=seconds) + run_podman run --log-driver=$driver -d --name test $IMAGE sh -c \ + "echo $s_before; trap 'echo $s_after; exit' SIGTERM; while :; do sleep 1; done" + + # sleep a second to make sure the date is after the first echo + sleep 1 + after=$(date --iso-8601=seconds) + run_podman stop test + + run_podman logs test + is "$output" \ + "$s_before +$s_after" + + run_podman logs --since $before test + is "$output" \ + "$s_before +$s_after" + + run_podman logs --since $after test + is "$output" "$s_after" + run_podman rm -f test +} + +@test "podman logs - since k8s-file" { + _log_test_since k8s-file +} + +@test "podman logs - since journald" { + # We can't use journald on RHEL as rootless: rhbz#1895105 + skip_if_journald_unavailable + + _log_test_since journald +} + # vim: filetype=sh diff --git a/test/system/130-kill.bats b/test/system/130-kill.bats index 1b02b4976..3770eac27 100644 --- a/test/system/130-kill.bats +++ b/test/system/130-kill.bats @@ -8,8 +8,7 @@ load helpers @test "podman kill - test signal handling in containers" { # Start a container that will handle all signals by emitting 'got: N' local -a signals=(1 2 3 4 5 6 8 10 12 13 14 15 16 20 21 22 23 24 25 26 64) - # Force the k8s-file driver until #10323 is fixed. - run_podman run --log-driver=k8s-file -d $IMAGE sh -c \ + run_podman run -d $IMAGE sh -c \ "for i in ${signals[*]}; do trap \"echo got: \$i\" \$i; done; echo READY; while ! test -e /stop; do sleep 0.05; done; diff --git a/vendor/github.com/containers/common/pkg/defaultnet/default_network.go b/vendor/github.com/containers/common/pkg/defaultnet/default_network.go new file mode 100644 index 000000000..9b32241d6 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/defaultnet/default_network.go @@ -0,0 +1,222 @@ +package defaultnet + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "regexp" + "text/template" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// TODO: A smarter implementation would make sure cni-podman0 was unused before +// making the default, and adjust if necessary +const networkTemplate = `{ + "cniVersion": "0.4.0", + "name": "{{{{.Name}}}}", + "plugins": [ + { + "type": "bridge", + "bridge": "cni-podman0", + "isGateway": true, + "ipMasq": true, + "hairpinMode": true, + "ipam": { + "type": "host-local", + "routes": [{ "dst": "0.0.0.0/0" }], + "ranges": [ + [ + { + "subnet": "{{{{.Subnet}}}}", + "gateway": "{{{{.Gateway}}}}" + } + ] + ] + } + }, +{{{{- if (eq .Machine true) }}}} + { + "type": "podman-machine", + "capabilities": { + "portMappings": true + } + }, +{{{{- end}}}} + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + }, + { + "type": "firewall" + }, + { + "type": "tuning" + } + ] +} +` + +var ( + // Borrowed from Podman, modified to remove dashes and periods. + nameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_]*$") +) + +// Used to pass info into the template engine +type networkInfo struct { + Name string + Subnet string + Gateway string + Machine bool +} + +// The most trivial definition of a CNI network possible for our use here. +// We need the name, and nothing else. +type network struct { + Name string `json:"name"` +} + +// Create makes the CNI default network, if necessary. +// Accepts the name and subnet of the network to create (a standard template +// will be used, with these values plugged in), the configuration directory +// where CNI configs are stored (to verify if a named configuration already +// exists), an exists directory (where a sentinel file will be stored, to ensure +// the network is only made once), and an isMachine bool (to determine whether +// the machine block will be added to the config). +// Create first checks if a default network has already been created via the +// presence of a sentinel file. If it does exist, it returns immediately without +// error. +// It next checks if a CNI network with the given name already exists. In that +// case, it creates the sentinel file and returns without error. +// If neither of these are true, the default network is created. +func Create(name, subnet, configDir, existsDir string, isMachine bool) error { + // TODO: Should probably regex name to make sure it's valid. + if name == "" || subnet == "" || configDir == "" || existsDir == "" { + return errors.Errorf("must provide values for all arguments to MakeDefaultNetwork") + } + if !nameRegex.MatchString(name) { + return errors.Errorf("invalid default network name %s - letters, numbers, and underscores only", name) + } + + sentinelFile := filepath.Join(existsDir, "defaultCNINetExists") + + // Check if sentinel file exists, return immediately if it does. + if _, err := os.Stat(sentinelFile); err == nil { + return nil + } + + // Create the sentinel file if it doesn't exist, so subsequent checks + // don't need to go further. + file, err := os.Create(sentinelFile) + if err != nil { + return err + } + file.Close() + + // We may need to make the config dir. + if err := os.MkdirAll(configDir, 0755); err != nil && !os.IsExist(err) { + return errors.Wrapf(err, "error creating CNI configuration directory") + } + + // Check all networks in the CNI conflist. + files, err := ioutil.ReadDir(configDir) + if err != nil { + return errors.Wrapf(err, "error reading CNI configuration directory") + } + if len(files) > 0 { + configPaths := make([]string, 0, len(files)) + for _, path := range files { + if !path.IsDir() && filepath.Ext(path.Name()) == ".conflist" { + configPaths = append(configPaths, filepath.Join(configDir, path.Name())) + } + } + for _, config := range configPaths { + configName, err := getConfigName(config) + if err != nil { + logrus.Errorf("Error reading CNI configuration file: %v", err) + continue + } + if configName == name { + return nil + } + } + } + + // We need to make the config. + // Get subnet and gateway. + _, ipNet, err := net.ParseCIDR(subnet) + if err != nil { + return errors.Wrapf(err, "default network subnet %s is invalid", subnet) + } + + ones, bits := ipNet.Mask.Size() + if ones == bits { + return errors.Wrapf(err, "default network subnet %s is to small", subnet) + } + gateway := make(net.IP, len(ipNet.IP)) + // copy the subnet ip to the gateway so we can modify it + copy(gateway, ipNet.IP) + // the default gateway should be the first ip in the subnet + gateway[len(gateway)-1]++ + + netInfo := new(networkInfo) + netInfo.Name = name + netInfo.Gateway = gateway.String() + netInfo.Subnet = ipNet.String() + netInfo.Machine = isMachine + + templ, err := template.New("network_template").Delims("{{{{", "}}}}").Parse(networkTemplate) + if err != nil { + return errors.Wrapf(err, "error compiling template for default network") + } + var output bytes.Buffer + if err := templ.Execute(&output, netInfo); err != nil { + return errors.Wrapf(err, "error executing template for default network") + } + + // Next, we need to place the config on disk. + // Loop through possible indexes, with a limit of 100 attempts. + created := false + for i := 87; i < 187; i++ { + configFile, err := os.OpenFile(filepath.Join(configDir, fmt.Sprintf("%d-%s.conflist", i, name)), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) + if err != nil { + logrus.Infof("Attempt to create default CNI network config file failed: %v", err) + continue + } + defer configFile.Close() + + created = true + + // Success - file is open. Write our buffer to it. + if _, err := configFile.Write(output.Bytes()); err != nil { + return errors.Wrapf(err, "error writing default CNI config to file") + } + break + } + if !created { + return errors.Errorf("no available default network configuration file was found") + } + + return nil +} + +// Get the name of the configuration contained in a given conflist file. Accepts +// the full path of a .conflist CNI configuration. +func getConfigName(file string) (string, error) { + contents, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + config := new(network) + if err := json.Unmarshal(contents, config); err != nil { + return "", errors.Wrapf(err, "error decoding CNI configuration %s", filepath.Base(file)) + } + return config.Name, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b4c2c6330..50f8e7338 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -102,6 +102,7 @@ github.com/containers/common/pkg/cgroupv2 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/filters github.com/containers/common/pkg/manifests github.com/containers/common/pkg/parse |