Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions pkg/plugins/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package plugins

import (
"bytes"
"os"
"path/filepath"
"strings"

"github.com/mudler/yip/pkg/logger"
Expand All @@ -12,21 +12,20 @@ import (

func DNS(l logger.Interface, s schema.Stage, fs vfs.FS, console Console) error {
if len(s.Dns.Nameservers) != 0 {
return applyDNS(s)
return applyDNS(s, fs)
}
return nil
}

func applyDNS(s schema.Stage) error {
func applyDNS(s schema.Stage, fs vfs.FS) error {
path := s.Dns.Path
if path == "" {
path = "/etc/resolv.conf"
}
err := Build(path, s.Dns.Nameservers, s.Dns.DnsSearch, s.Dns.DnsOptions)
return err
return Build(path, s.Dns.Nameservers, s.Dns.DnsSearch, s.Dns.DnsOptions, fs)
}

func Build(path string, nameservers, dnsSearch, dnsOptions []string) error {
func Build(path string, nameservers, dnsSearch, dnsOptions []string, fs vfs.FS) error {
content := bytes.NewBuffer(nil)
if len(dnsSearch) > 0 {
if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." {
Expand All @@ -48,7 +47,24 @@ func Build(path string, nameservers, dnsSearch, dnsOptions []string) error {
}
}

if err := os.WriteFile(path, content.Bytes(), 0o644); err != nil {
dir := filepath.Dir(path)
if err := vfs.MkdirAll(fs, dir, 0755); err != nil {
return err
}

// Most systemd distributions ship /etc/resolv.conf as a symlink into
// systemd-resolved's runtime directory, and Kairos creates that link itself.
// Writing through it lands the nameservers in a file resolved owns and
// regenerates, on a tmpfs that does not exist yet in the initramfs. Write a
// sibling and rename over the path instead: rename acts on the link rather
// than its target, so the caller gets a regular file where it asked for one
// and no reader ever sees a half-written resolv.conf.
tmp := filepath.Join(dir, "."+filepath.Base(path)+".yip")
if err := fs.WriteFile(tmp, content.Bytes(), 0644); err != nil {
return err
}
if err := fs.Rename(tmp, path); err != nil {
fs.Remove(tmp)
return err
}
return nil
Expand Down
125 changes: 118 additions & 7 deletions pkg/plugins/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
package plugins_test

import (
"io/ioutil"
"os"

. "github.com/mudler/yip/pkg/plugins"
"github.com/mudler/yip/pkg/schema"
consoletests "github.com/mudler/yip/tests/console"
"github.com/sirupsen/logrus"
"github.com/twpayne/go-vfs/v5"
"github.com/twpayne/go-vfs/v5/vfst"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -32,25 +32,136 @@ var _ = Describe("Dns", func() {
Context("parsing yip file", func() {
testConsole := consoletests.TestConsole{}

var fs vfs.FS
var cleanup func()
var err error

BeforeEach(func() {
testConsole.Reset()
})

AfterEach(func() {
if cleanup != nil {
cleanup()
}
})

It("sets dns", func() {
fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{"/tmp/test/bar": "boo"})
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/tmp/test/bar": "boo"})
Expect(err).Should(BeNil())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Path: "/tmp/test/foo", Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

b, err := fs.ReadFile("/tmp/test/foo")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 8.8.8.8\n"))
})

It("writes search and options too", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/tmp/test/bar": "boo"})
Expect(err).Should(BeNil())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{
Path: "/tmp/test/foo",
Nameservers: []string{"8.8.8.8", "8.8.4.4"},
DnsSearch: []string{"kairos.io"},
DnsOptions: []string{"ndots:1"},
}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

b, err := fs.ReadFile("/tmp/test/foo")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("search kairos.io\nnameserver 8.8.8.8\nnameserver 8.8.4.4\noptions ndots:1\n"))
})

// The plugin is handed a vfs.FS and used to ignore it, writing the host's
// real /etc/resolv.conf instead of the one inside the filesystem it was
// given.
It("writes inside the filesystem it is given", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/etc/resolv.conf": "nameserver 127.0.0.53\n"})
Expect(err).Should(BeNil())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

b, err := fs.ReadFile("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 8.8.8.8\n"))

// The host file must not have been touched.
host, err := os.ReadFile("/etc/resolv.conf")
if err == nil {
Expect(string(host)).ShouldNot(Equal("nameserver 8.8.8.8\n"))
}
})

// Kairos points /etc/resolv.conf at /run/systemd/resolve/resolv.conf, so
// following the symlink writes a file systemd-resolved owns and
// regenerates, and the nameservers never stick.
It("replaces a symlink instead of writing through it", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{
"/run/systemd/resolve/resolv.conf": "nameserver 127.0.0.53\n",
})
Expect(err).Should(BeNil())
temp := fs.TempDir()
Expect(fs.Mkdir("/etc", 0o755)).Should(Succeed())
Expect(fs.Symlink("/run/systemd/resolve/resolv.conf", "/etc/resolv.conf")).Should(Succeed())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

defer cleanup()
info, err := fs.Lstat("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(info.Mode() & os.ModeSymlink).Should(Equal(os.FileMode(0)))

b, err := fs.ReadFile("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 8.8.8.8\n"))

// systemd-resolved's own file is left alone.
b, err = fs.ReadFile("/run/systemd/resolve/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 127.0.0.53\n"))
})

// In the initramfs /run/systemd/resolve does not exist yet, so following
// the dangling symlink failed the whole stage with ENOENT.
It("replaces a dangling symlink", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/tmp/test/bar": "boo"})
Expect(err).Should(BeNil())
Expect(fs.Mkdir("/etc", 0o755)).Should(Succeed())
Expect(fs.Symlink("/run/systemd/resolve/resolv.conf", "/etc/resolv.conf")).Should(Succeed())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Path: temp + "/foo", Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())
file, err := os.Open(temp + "/foo")

b, err := fs.ReadFile("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 8.8.8.8\n"))
})

b, err := ioutil.ReadAll(file)
It("creates the parent directory when it is missing", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/tmp/test/bar": "boo"})
Expect(err).Should(BeNil())

err = DNS(logrus.New(), schema.Stage{Dns: schema.DNS{Path: "/etc/resolv.conf", Nameservers: []string{"8.8.8.8"}}}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

b, err := fs.ReadFile("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 8.8.8.8\n"))
})

It("does nothing without nameservers", func() {
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{"/etc/resolv.conf": "nameserver 127.0.0.53\n"})
Expect(err).Should(BeNil())

err = DNS(logrus.New(), schema.Stage{}, fs, &testConsole)
Expect(err).ShouldNot(HaveOccurred())

b, err := fs.ReadFile("/etc/resolv.conf")
Expect(err).ShouldNot(HaveOccurred())
Expect(string(b)).Should(Equal("nameserver 127.0.0.53\n"))
})
})
})
Loading