forked from containers/common
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
2 changed files
with
574 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.