docker-etchosts/etchosts.go
2018-08-25 22:28:04 +02:00

138 lines
3.5 KiB
Go

package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
log "github.com/sirupsen/logrus"
)
const (
banner = "# !!! managed by docker-etchosts !!!"
)
func writeToEtcHosts(ipsToNames ipsToNamesMap, config ConfigSpec) error {
// We do not want to create the hosts file; if it's not there, we probably have the wrong path.
// Open RW because we might have to write to it (see movePreservePerms)
etcHosts, err := os.OpenFile(config.EtcHostsPath, os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("could not open %s for reading: %s", config.EtcHostsPath, err)
}
defer etcHosts.Close()
// create tmpfile in same folder as
tmp, err := ioutil.TempFile(path.Dir(config.EtcHostsPath), "docker-etchosts")
if err != nil {
return fmt.Errorf("could not create tempfile")
}
// remove tempfile; this might fail if we managed to move it, which is ok
defer func(file *os.File) {
file.Close()
if err := os.Remove(file.Name()); err != nil && !os.IsNotExist(err) {
log.Warnf("unexpected error trying to remove temp file %s: %s", file.Name(), err)
}
}(tmp)
// go through file and update existing entries/prune nonexistent entries
managedLine := false
scanner := bufio.NewScanner(etcHosts)
for scanner.Scan() {
line := scanner.Text()
if line == banner {
managedLine = true
continue
}
if managedLine {
managedLine = false
tokens := strings.Fields(line)
if len(tokens) < 1 {
continue // remove empty managed line
}
ip := tokens[0]
if names, ok := ipsToNames[ip]; ok {
err = writeEntryWithBanner(tmp, ip, names)
if err != nil {
return err
}
delete(ipsToNames, ip) // otherwise we'll append it again below
}
} else {
// keep original unmanaged line
fmt.Fprintf(tmp, "%s\n", line)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading %s: %s", config.EtcHostsPath, err)
}
// append remaining entries to file
for ip, names := range ipsToNames {
err = writeEntryWithBanner(tmp, ip, names)
if err != nil {
return err
}
}
err = movePreservePerms(tmp, etcHosts)
if err != nil {
return err
}
return nil
}
func writeEntryWithBanner(tmp io.Writer, ip string, names []string) error {
if ip != "" && len(names) > 0 {
log.Debugf("writing entry for %s (%s)", ip, names)
if _, err := fmt.Fprintf(tmp, "%s\n%s\t%s\n", banner, ip, strings.Join(names, " ")); err != nil {
return fmt.Errorf("error writing entry for %s: %s", ip, err)
}
}
return nil
}
func movePreservePerms(src, dst *os.File) error {
if err := src.Sync(); err != nil {
return fmt.Errorf("could not sync changes to %s: %s", src.Name(), err)
}
etcHostsInfo, err := dst.Stat()
if err != nil {
return fmt.Errorf("could not stat %s: %s", dst.Name(), err)
}
// We try moving first because it's atomic; the fallback strategy is copying the content, which might generate a
// broken hosts file if some other process writes to it at the same time.
err = os.Rename(src.Name(), dst.Name())
if err != nil {
log.Infof("could not rename to %s; falling back to less safe direct-write (%s)", dst.Name(), err)
if _, err := src.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := dst.Seek(0, io.SeekStart); err != nil {
return err
}
if err := dst.Truncate(0); err != nil {
return err
}
_, err = io.Copy(dst, src)
return err
}
// ensure we're not running with some umask that might break things
err = src.Chmod(etcHostsInfo.Mode())
if err != nil {
return fmt.Errorf("could not chmod %s: %s", src.Name(), err)
}
// TODO: also keep user?
return nil
}