Skip to content

Commit

Permalink
/etc/hosts (gossip): Stronger logic
Browse files Browse the repository at this point in the history
* Add a mutex around /etc/hosts updates (for a little extra safety)
* Don't write unchanged files
* Recover from out-of-sequence guard lines
* Add tests

Thanks to ryan bonham for the suggestions & finding the issue!
  • Loading branch information
justinsb committed May 6, 2019
1 parent dbae685 commit d5e4da6
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 7 deletions.
9 changes: 8 additions & 1 deletion protokube/pkg/gossip/dns/hosts/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
Expand All @@ -7,3 +7,10 @@ go_library(
visibility = ["//visibility:public"],
deps = ["//vendor/github.com/golang/glog:go_default_library"],
)

go_test(
name = "go_default_test",
srcs = ["hosts_test.go"],
embed = [":go_default_library"],
deps = ["//pkg/diff:go_default_library"],
)
40 changes: 34 additions & 6 deletions protokube/pkg/gossip/dns/hosts/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ limitations under the License.
package hosts

import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"sync"

"github.com/golang/glog"
)
Expand All @@ -32,7 +34,13 @@ const (
GUARD_END = "# End host entries managed by kops"
)

var hostsFileMutex sync.Mutex

func UpdateHostsFileWithRecords(p string, addrToHosts map[string][]string) error {
// For safety / sanity, we avoid concurrent updates from one process
hostsFileMutex.Lock()
defer hostsFileMutex.Unlock()

stat, err := os.Stat(p)
if err != nil {
return fmt.Errorf("error getting file status of %q: %v", p, err)
Expand All @@ -44,19 +52,28 @@ func UpdateHostsFileWithRecords(p string, addrToHosts map[string][]string) error
}

var out []string
depth := 0
inGuardBlock := false
for _, line := range strings.Split(string(data), "\n") {
k := strings.TrimSpace(line)
if k == GUARD_BEGIN {
depth++
if inGuardBlock {
glog.Warningf("/etc/hosts guard-block begin seen while in guard block; will ignore")
}
inGuardBlock = true
}

if depth <= 0 {
if !inGuardBlock {
out = append(out, line)
}

if k == GUARD_END {
depth--
if !inGuardBlock {
glog.Warningf("/etc/hosts guard-block end seen before guard-block start; will ignore end")
// Don't output the line
out = out[:len(out)-1]
}

inGuardBlock = false
}
}

Expand All @@ -75,17 +92,28 @@ func UpdateHostsFileWithRecords(p string, addrToHosts map[string][]string) error
out = append(out, "")

out = append(out, GUARD_BEGIN)
var block []string
for addr, hosts := range addrToHosts {
sort.Strings(hosts)
out = append(out, addr+"\t"+strings.Join(hosts, " "))
block = append(block, addr+"\t"+strings.Join(hosts, " "))
}
// Ensure consistent order to minimize updates
sort.Strings(block)
out = append(out, block...)
out = append(out, GUARD_END)
out = append(out, "")

updated := []byte(strings.Join(out, "\n"))

if bytes.Equal(updated, data) {
glog.V(2).Infof("skipping update of unchanged /etc/hosts")
return nil
}

// Note that because we are bind mounting /etc/hosts, we can't do a normal atomic file write
// (where we write a temp file and rename it)
// TODO: We should just hold the file open while we read & write it
err = ioutil.WriteFile(p, []byte(strings.Join(out, "\n")), stat.Mode().Perm())
err = ioutil.WriteFile(p, updated, stat.Mode().Perm())
if err != nil {
return fmt.Errorf("error writing file %q: %v", p, err)
}
Expand Down
159 changes: 159 additions & 0 deletions protokube/pkg/gossip/dns/hosts/hosts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
Copyright 2019 The Kubernetes 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 hosts

import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"

"k8s.io/kops/pkg/diff"
)

func TestRemovesDuplicateGuardedBlocks(t *testing.T) {
in := `
foo 10.2.3.4
# Begin host entries managed by etcd-manager[etcd] - do not edit
# End host entries managed by etcd-manager[etcd]
# Begin host entries managed by etcd-manager[etcd] - do not edit
# End host entries managed by etcd-manager[etcd]
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
`

expected := `
foo 10.2.3.4
# Begin host entries managed by etcd-manager[etcd] - do not edit
# End host entries managed by etcd-manager[etcd]
# Begin host entries managed by etcd-manager[etcd] - do not edit
# End host entries managed by etcd-manager[etcd]
# Begin host entries managed by kops - do not edit
a\t10.0.1.1 10.0.1.2
b\t10.0.2.1
c\t
# End host entries managed by kops
`

runTest(t, in, expected)
}

func TestRecoversFromBadNesting(t *testing.T) {
in := `
foo 10.2.3.4
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# End host entries managed by kops
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# Begin host entries managed by kops - do not edit
# Begin host entries managed by kops - do not edit
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
# Begin host entries managed by kops - do not edit
# End host entries managed by kops
bar 10.1.2.3
`

expected := `
foo 10.2.3.4
bar 10.1.2.3
# Begin host entries managed by kops - do not edit
a\t10.0.1.1 10.0.1.2
b\t10.0.2.1
c\t
# End host entries managed by kops
`

runTest(t, in, expected)
}

func runTest(t *testing.T, in string, expected string) {
expected = strings.Replace(expected, "\\t", "\t", -1)

dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer func() {
err := os.RemoveAll(dir)
if err != nil {
t.Errorf("failed to remove temp dir %q: %v", dir, err)
}
}()

p := filepath.Join(dir, "hosts")
addrToHosts := map[string][]string{
"a": {"10.0.1.2", "10.0.1.1"},
"b": {"10.0.2.1"},
"c": {},
}

if err := ioutil.WriteFile(p, []byte(in), 0755); err != nil {
t.Fatalf("error writing hosts file: %v", err)
}

// We run it repeatedly to make sure we don't change it accidentally
for i := 0; i < 100; i++ {
if err := UpdateHostsFileWithRecords(p, addrToHosts); err != nil {
t.Fatalf("error updating hosts file: %v", err)
}

b, err := ioutil.ReadFile(p)
if err != nil {
t.Fatalf("error reading output file: %v", err)
}

actual := string(b)
if actual != expected {
diffString := diff.FormatDiff(expected, actual)
t.Logf("diff:\n%s\n", diffString)
t.Errorf("unexpected output. expected=%q, actual=%q", expected, actual)
}
}
}

0 comments on commit d5e4da6

Please sign in to comment.