Skip to content

Commit

Permalink
add new etchosts package
Browse files Browse the repository at this point in the history
Add a new libnetwork/etchosts package to manage reading/writing hosts files.
This package exports two functions New() and Add(). See the godoc comments
on the functions.

Both podman and buildah should use this function to make sure files are
generated identical.

Signed-off-by: Paul Holzinger <[email protected]>
  • Loading branch information
Luap99 committed Apr 11, 2022
1 parent 3dd66c0 commit 6517fbd
Show file tree
Hide file tree
Showing 2 changed files with 574 additions and 0 deletions.
202 changes: 202 additions & 0 deletions libnetwork/etchosts/hosts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package etchosts

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
)

const (
// DefaultHostsFile is the default path to the hosts file
DefaultHostsFile = "/etc/hosts"
hostContainersInternal = "host.containers.internal"
localhost = "localhost"
)

type HostEntries []HostEntry

type HostEntry struct {
IP string
Names []string
}

// New will create a new hosts file and write this to the target file.
// This function does not prevent any kind of concurency problems, it is
// the callers responsibility to avoid concurent writes to this file.
//
// - baseFile is the file where we read entries from and add entries to
// the target hosts file. If the name is empty it will not read any entries.
// - extraHosts is a slice of entries in the "hostname:ip" format. Optional.
// - containerIPs should contain the main container ipv4 and ipv6 if available
// with the container name and host name as names set. Optional.
// - hostContainersInternalIP is the IP for the host.containers.internal entry. Optional.
// - targetFile where the hosts are written to.
//
// The extraHosts are written first, then the hosts from the file baseFile and the
// containerIps. The container ip entry is only added when the name was not already
// added before.
func New(baseFile string, extraHosts []string, containerIPs HostEntries, hostContainersInternalIP, targetFile string) error {
if err := new(baseFile, true, extraHosts, containerIPs, hostContainersInternalIP, targetFile); err != nil {
return fmt.Errorf("failed to create new hosts file: %w", err)
}
return nil
}

// Add the given entries to the hosts file, entries are only added if they are
// not already present.
// Add is not atomic because it will keep the current file inode. This is
// required to keep bind mounts for containers working.
func Add(file string, entries HostEntries) error {
if err := new(file, false, nil, entries, "", file); err != nil {
return fmt.Errorf("failed to add entry to hosts file: %w", err)
}
return nil
}

// new see comment on New()
// addLocalhost is a extra bool to add localhost (if not already present)
func new(baseFile string, addLocalhost bool, extraHosts []string, containerIPs HostEntries, hostContainersInternalIP, targetFile string) error {
entries, err := parseExtraHosts(extraHosts)
if err != nil {
return err
}
entries2, err := parseHostsFile(baseFile)
if err != nil {
return err
}

if err := writeHostFile(targetFile, append(entries, entries2...), containerIPs, hostContainersInternalIP, addLocalhost); err != nil {
return err
}
return nil
}

// parseExtraHosts converts a slice of "name:ip" string to entries.
// Because podman and buildah both store the extra hosts in this format
// we convert it here instead of having to this on the caller side.
func parseExtraHosts(extraHosts []string) (HostEntries, error) {
entries := make(HostEntries, 0, len(extraHosts))
for _, entry := range extraHosts {
values := strings.SplitN(entry, ":", 2)
if len(values) != 2 {
return nil, fmt.Errorf("unable to parse host entry %q: incorrect format", entry)
}
if values[0] == "" {
return nil, fmt.Errorf("hostname in host entry %q is empty", entry)
}
if values[1] == "" {
return nil, fmt.Errorf("IP address in host entry %q is empty", entry)
}
e := HostEntry{IP: values[1], Names: []string{values[0]}}
entries = append(entries, e)
}
return entries, nil
}

// parseHostsFile parses a given host file and return s all entries in it.
// Note that this will remove all comments and spaces.
func parseHostsFile(file string) (HostEntries, error) {
// empty file is valid, in this case we skip adding entries from the file
if file == "" {
return nil, nil
}

f, err := os.Open(file)
if err != nil {
// do not error when the default hosts file does not exists
// https://github.com/containers/podman/issues/12667
if errors.Is(err, os.ErrNotExist) && file == DefaultHostsFile {
return nil, nil
}
return nil, err
}
defer f.Close()

entries := HostEntries{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// split of the comments
line := strings.SplitN(scanner.Text(), "#", 2)[0]
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
// if we only have a ip without names we skip it
if len(fields) < 2 {
continue
}

e := HostEntry{IP: fields[0], Names: fields[1:]}
entries = append(entries, e)
}

return entries, scanner.Err()
}

// writeHostFile write the entries to the given file
func writeHostFile(file string, userEntries, containerIPs HostEntries, hostContainersInternalIP string, addLocalhost bool) error {
f, err := os.Create(file)
if err != nil {
return err
}
defer f.Close()

names := make(map[string]struct{})
for _, entry := range userEntries {
for _, name := range entry.Names {
names[name] = struct{}{}
}
_, err = f.WriteString(entry.IP + "\t" + strings.Join(entry.Names, " ") + "\n")
if err != nil {
return err
}
}

if addLocalhost {
// if localhost was not added we add it
// https://github.com/containers/podman/issues/11411
if _, ok := names[localhost]; !ok {
_, err = f.WriteString("127.0.0.1\t" + localhost + "\n::1\t" + localhost + "\n")
if err != nil {
return err
}
}
}

// add host.containers.internal entry
if hostContainersInternalIP != "" {
// make sure it is not already set
if _, ok := names[hostContainersInternal]; !ok {
_, err = f.WriteString(hostContainersInternalIP + "\t" + hostContainersInternal + "\n")
if err != nil {
return err
}
}
}

return addEntriesIfNotExists(f, containerIPs, names)
}

// addEntriesIfNotExists only adds the entries for names that are not already
// in the hosts file, otherwise we start overwritting user entries
func addEntriesIfNotExists(f io.StringWriter, containerIPs HostEntries, names map[string]struct{}) error {
for _, entry := range containerIPs {
freeNames := make([]string, 0, len(entry.Names))
for _, name := range entry.Names {
if _, ok := names[name]; !ok {
freeNames = append(freeNames, name)
}
}
if len(freeNames) > 0 {
_, err := f.WriteString(entry.IP + "\t" + strings.Join(freeNames, " ") + "\n")
if err != nil {
return err
}
}
}
return nil
}
Loading

0 comments on commit 6517fbd

Please sign in to comment.