diff --git a/vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf.go b/executor/oci/internal/resolvconf/resolvconf.go similarity index 67% rename from vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf.go rename to executor/oci/internal/resolvconf/resolvconf.go index 4261844cc17c..c00a50c7d914 100644 --- a/vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf.go +++ b/executor/oci/internal/resolvconf/resolvconf.go @@ -1,6 +1,3 @@ -// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 - // Package resolvconf is used to generate a container's /etc/resolv.conf file. // // Constructor Load and Parse read a resolv.conf file from the filesystem or @@ -21,19 +18,15 @@ import ( "bufio" "bytes" "context" - "fmt" "io" - "io/fs" "net/netip" "os" + "slices" "strconv" "strings" - "text/template" - "github.com/containerd/log" - "github.com/moby/sys/atomicwriter" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" + "github.com/moby/buildkit/errdefs" + "github.com/moby/buildkit/util/bklog" ) // Fallback nameservers, to use if none can be obtained from the host or command @@ -70,7 +63,7 @@ type ExtDNSEntry struct { func (ed ExtDNSEntry) String() string { if ed.HostLoopback { - return fmt.Sprintf("host(%s)", ed.Addr) + return "host(" + ed.Addr.String() + ")" } return ed.Addr.String() } @@ -119,7 +112,7 @@ func Parse(reader io.Reader, path string) (ResolvConf, error) { rc.processLine(scanner.Text()) } if err := scanner.Err(); err != nil { - return ResolvConf{}, errSystem{err} + return ResolvConf{}, errdefs.Internal(err) } if _, ok := rc.Option("ndots"); ok { rc.md.NDotsFrom = "host" @@ -141,7 +134,7 @@ func (rc *ResolvConf) SetHeader(c string) { // NameServers returns addresses used in nameserver directives. func (rc *ResolvConf) NameServers() []netip.Addr { - return append([]netip.Addr(nil), rc.nameServers...) + return slices.Clone(rc.nameServers) } // OverrideNameServers replaces the current set of nameservers. @@ -152,7 +145,7 @@ func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) { // Search returns the current DNS search domains. func (rc *ResolvConf) Search() []string { - return append([]string(nil), rc.search...) + return slices.Clone(rc.search) } // OverrideSearch replaces the current DNS search domains. @@ -169,7 +162,7 @@ func (rc *ResolvConf) OverrideSearch(search []string) { // Options returns the current options. func (rc *ResolvConf) Options() []string { - return append([]string(nil), rc.options...) + return slices.Clone(rc.options) } // Option finds the last option named search, and returns (value, true) if @@ -181,7 +174,7 @@ func (rc *ResolvConf) Options() []string { // Option("ndots") -> ("1", true) // Option("edns0") -> ("", true) func (rc *ResolvConf) Option(search string) (string, bool) { - for i := len(rc.options) - 1; i >= 0; i -= 1 { + for i := len(rc.options) - 1; i >= 0; i-- { k, v, _ := strings.Cut(rc.options[i], ":") if k == search { return v, true @@ -192,7 +185,7 @@ func (rc *ResolvConf) Option(search string) (string, bool) { // OverrideOptions replaces the current DNS options. func (rc *ResolvConf) OverrideOptions(options []string) { - rc.options = append([]string(nil), options...) + rc.options = slices.Clone(options) rc.md.NDotsFrom = "" if _, exists := rc.Option("ndots"); exists { rc.md.NDotsFrom = "override" @@ -227,7 +220,7 @@ func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) { } rc.nameServers = filtered if len(rc.nameServers) == 0 { - log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers") + bklog.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers") rc.nameServers = defaultNSAddrs(ipv6) rc.md.Warnings = append(rc.md.Warnings, "Used default nameservers.") } @@ -283,145 +276,123 @@ func (rc *ResolvConf) TransformForIntNS( if len(rc.md.ExtNameServers) == 0 { rc.md.Warnings = append(rc.md.Warnings, "NO EXTERNAL NAMESERVERS DEFINED") } - return append([]ExtDNSEntry(nil), rc.md.ExtNameServers...), nil + return slices.Clone(rc.md.ExtNameServers), nil } // Generate returns content suitable for writing to a resolv.conf file. If comments // is true, the file will include header information if supplied, and a trailing // comment that describes how the file was constructed and lists external resolvers. func (rc *ResolvConf) Generate(comments bool) ([]byte, error) { - s := struct { - Md *metadata - NameServers []netip.Addr - Search []string - Options []string - Other []string - Overrides []string - Comments bool - }{ - Md: &rc.md, - NameServers: rc.nameServers, - Search: rc.search, - Options: rc.options, - Other: rc.other, - Comments: comments, - } - if rc.md.NSOverride { - s.Overrides = append(s.Overrides, "nameservers") - } - if rc.md.SearchOverride { - s.Overrides = append(s.Overrides, "search") - } - if rc.md.OptionsOverride { - s.Overrides = append(s.Overrides, "options") - } - - const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}} - -{{end}}{{end}}{{range .NameServers -}} -nameserver {{.}} -{{end}}{{with .Search -}} -search {{join . " "}} -{{end}}{{with .Options -}} -options {{join . " "}} -{{end}}{{with .Other -}} -{{join . "\n"}} -{{end}}{{if .Comments}} -# Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}} -{{range .Md.Warnings -}} -# {{.}} -{{end -}} -{{with .Md.ExtNameServers -}} -# ExtServers: {{.}} -{{end -}} -{{with .Md.InvalidNSs -}} -# Invalid nameservers: {{.}} -{{end -}} -# Overrides: {{.Overrides}} -{{with .Md.NDotsFrom -}} -# Option ndots from: {{.}} -{{end -}} -{{end -}} -` - - funcs := template.FuncMap{"join": strings.Join} - var buf bytes.Buffer - templ, err := template.New("summary").Funcs(funcs).Parse(templateText) - if err != nil { - return nil, errSystem{err} + var b bytes.Buffer + b.Grow(512) // estimated size for a regular resolv.conf we produce. + + if comments && rc.md.Header != "" { + b.WriteString(rc.md.Header + "\n") + b.WriteByte('\n') + } + for _, ns := range rc.nameServers { + b.WriteString("nameserver ") + b.WriteString(ns.String()) + b.WriteByte('\n') + } + if len(rc.search) > 0 { + b.WriteString("search ") + for i, s := range rc.search { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(s) + } + b.WriteByte('\n') } - if err := templ.Execute(&buf, s); err != nil { - return nil, errSystem{err} + if len(rc.options) > 0 { + b.WriteString("options ") + for i, s := range rc.options { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(s) + } + b.WriteByte('\n') } - return buf.Bytes(), nil -} - -// WriteFile generates content and writes it to path. If hashPath is non-zero, it -// also writes a file containing a hash of the content, to enable UserModified() -// to determine whether the file has been modified. -func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error { - content, err := rc.Generate(true) - if err != nil { - return err + for _, o := range rc.other { + b.WriteString(o) + b.WriteByte('\n') } - // Write the resolv.conf file - it's bind-mounted into the container, so can't - // move a temp file into place, just have to truncate and write it. - if err := os.WriteFile(path, content, perm); err != nil { - return errSystem{err} - } + if comments { + b.WriteByte('\n') + b.WriteString("# Based on host file: '" + rc.md.SourcePath + "'") + if rc.md.Transform != "" { + b.WriteString(" (" + rc.md.Transform + ")") + } + b.WriteByte('\n') + for _, w := range rc.md.Warnings { + b.WriteString("# ") + b.WriteString(w) + b.WriteByte('\n') + } + if len(rc.md.ExtNameServers) > 0 { + b.WriteString("# ExtServers: [") + for i, ext := range rc.md.ExtNameServers { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(ext.String()) + } + b.WriteByte(']') + b.WriteByte('\n') + } + if len(rc.md.InvalidNSs) > 0 { + b.WriteString("# Invalid nameservers: [") + for i, ext := range rc.md.InvalidNSs { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(ext) + } + b.WriteByte(']') + b.WriteByte('\n') + } - // Write the hash file. - if hashPath != "" { - hashFile, err := atomicwriter.New(hashPath, perm) - if err != nil { - return errSystem{err} + b.WriteString("# Overrides: [") + var overrides int + if rc.md.NSOverride { + b.WriteString("nameservers") + overrides++ + } + if rc.md.SearchOverride { + if overrides > 0 { + b.WriteByte(' ') + } + b.WriteString("search") + overrides++ } - defer hashFile.Close() + if rc.md.OptionsOverride { + if overrides > 0 { + b.WriteByte(' ') + } + b.WriteString("options") + } + b.WriteByte(']') + b.WriteByte('\n') - if _, err = hashFile.Write([]byte(digest.FromBytes(content))); err != nil { - return err + if rc.md.NDotsFrom != "" { + b.WriteString("# Option ndots from: " + rc.md.NDotsFrom + "\n") } } - return nil + return b.Bytes(), nil } -// UserModified can be used to determine whether the resolv.conf file has been -// modified since it was generated. It returns false with no error if the file -// matches the hash, true with no error if the file no longer matches the hash, -// and false with an error if the result cannot be determined. -func UserModified(rcPath, rcHashPath string) (bool, error) { - currRCHash, err := os.ReadFile(rcHashPath) - if err != nil { - // If the hash file doesn't exist, can only assume it hasn't been written - // yet (so, the user hasn't modified the file it hashes). - if errors.Is(err, fs.ErrNotExist) { - return false, nil - } - return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath) - } - expected, err := digest.Parse(string(currRCHash)) - if err != nil { - return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath) - } - v := expected.Verifier() - currRC, err := os.Open(rcPath) - if err != nil { - return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath) - } - defer currRC.Close() - if _, err := io.Copy(v, currRC); err != nil { - return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath) +func (rc *ResolvConf) processLine(line string) { + // Strip blank lines and comments. + if line == "" || line[0] == '#' || line[0] == ';' { + return } - return !v.Verified(), nil -} -func (rc *ResolvConf) processLine(line string) { fields := strings.Fields(line) - - // Strip blank lines and comments. - if len(fields) == 0 || fields[0][0] == '#' || fields[0][0] == ';' { + if len(fields) == 0 { return } @@ -470,8 +441,11 @@ func defaultNSAddrs(ipv6 bool) []netip.Addr { func removeInvalidNDots(options []string) []string { n := 0 for _, opt := range options { - k, v, _ := strings.Cut(opt, ":") + k, v, hasSep := strings.Cut(opt, ":") if k == "ndots" { + if !hasSep || v == "" { + continue + } ndots, err := strconv.Atoi(v) if err != nil || ndots < 0 { continue @@ -483,16 +457,3 @@ func removeInvalidNDots(options []string) []string { clear(options[n:]) // Zero out the obsolete elements, for GC. return options[:n] } - -// errSystem implements [github.com/docker/docker/errdefs.ErrSystem]. -// -// We don't use the errdefs helpers here, because the resolvconf package -// is imported in BuildKit, and this is the only location that used the -// errdefs package outside of the client. -type errSystem struct{ error } - -func (errSystem) System() {} - -func (e errSystem) Unwrap() error { - return e.error -} diff --git a/vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf_path.go b/executor/oci/internal/resolvconf/resolvconf_path.go similarity index 91% rename from vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf_path.go rename to executor/oci/internal/resolvconf/resolvconf_path.go index 65d0fe14098d..69e40c12b0f1 100644 --- a/vendor/github.com/docker/docker/libnetwork/internal/resolvconf/resolvconf_path.go +++ b/executor/oci/internal/resolvconf/resolvconf_path.go @@ -5,7 +5,7 @@ import ( "net/netip" "sync" - "github.com/containerd/log" + "github.com/moby/buildkit/util/bklog" ) const ( @@ -49,7 +49,7 @@ func Path() string { ns := rc.nameServers if len(ns) == 1 && ns[0] == netip.MustParseAddr("127.0.0.53") { pathAfterSystemdDetection = alternatePath - log.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath) + bklog.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath) } }) return pathAfterSystemdDetection diff --git a/executor/oci/internal/resolvconf/resolvconf_test.go b/executor/oci/internal/resolvconf/resolvconf_test.go new file mode 100644 index 000000000000..446b4210d222 --- /dev/null +++ b/executor/oci/internal/resolvconf/resolvconf_test.go @@ -0,0 +1,512 @@ +package resolvconf + +import ( + "bytes" + "net/netip" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRCOption(t *testing.T) { + testcases := []struct { + name string + options string + search string + expFound bool + expValue string + }{ + { + name: "Empty options", + options: "", + search: "ndots", + }, + { + name: "Not found", + options: "ndots:0 edns0", + search: "trust-ad", + }, + { + name: "Found with value", + options: "ndots:0 edns0", + search: "ndots", + expFound: true, + expValue: "0", + }, + { + name: "Found without value", + options: "ndots:0 edns0", + search: "edns0", + expFound: true, + expValue: "", + }, + { + name: "Found last value", + options: "ndots:0 edns0 ndots:1", + search: "ndots", + expFound: true, + expValue: "1", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + rc, err := Parse(bytes.NewBufferString("options "+tc.options), "") + require.NoError(t, err) + value, found := rc.Option(tc.search) + assert.Equal(t, tc.expFound, found) + assert.Equal(t, tc.expValue, value) + }) + } +} + +var ( + a2s = sliceutilMapper(netip.Addr.String) + s2a = sliceutilMapper(netip.MustParseAddr) +) + +// Test that a resolv.conf file can be modified using OverrideXXX() methods +// to modify nameservers/search/options directives, and tha options can be +// added via AddOption(). +func TestRCModify(t *testing.T) { + testcases := []struct { + name string + inputNS []string + inputSearch []string + inputOptions []string + noOverrides bool // Whether to apply overrides (empty lists are valid overrides). + overrideNS []string + overrideSearch []string + overrideOptions []string + addOption string + }{ + { + name: "No content no overrides", + inputNS: []string{}, + }, + { + name: "No overrides", + noOverrides: true, + inputNS: []string{"1.2.3.4"}, + inputSearch: []string{"invalid"}, + inputOptions: []string{"ndots:0"}, + }, + { + name: "Empty overrides", + inputNS: []string{"1.2.3.4"}, + inputSearch: []string{"invalid"}, + inputOptions: []string{"ndots:0"}, + }, + { + name: "Overrides", + inputNS: []string{"1.2.3.4"}, + inputSearch: []string{"invalid"}, + inputOptions: []string{"ndots:0"}, + overrideNS: []string{"2.3.4.5", "fdba:acdd:587c::53"}, + overrideSearch: []string{"com", "invalid", "example"}, + overrideOptions: []string{"ndots:1", "edns0", "trust-ad"}, + }, + { + name: "Add option no overrides", + noOverrides: true, + inputNS: []string{"1.2.3.4"}, + inputSearch: []string{"invalid"}, + inputOptions: []string{"ndots:0"}, + addOption: "attempts:3", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + var input string + if len(tc.inputNS) != 0 { + for _, ns := range tc.inputNS { + input += "nameserver " + ns + "\n" + } + } + if len(tc.inputSearch) != 0 { + input += "search " + strings.Join(tc.inputSearch, " ") + "\n" + } + if len(tc.inputOptions) != 0 { + input += "options " + strings.Join(tc.inputOptions, " ") + "\n" + } + rc, err := Parse(bytes.NewBufferString(input), "") + require.NoError(t, err) + assert.Equal(t, tc.inputNS, a2s(rc.NameServers())) + assert.Equal(t, tc.inputSearch, rc.Search()) + assert.Equal(t, tc.inputOptions, rc.Options()) + + if !tc.noOverrides { + overrideNS := s2a(tc.overrideNS) + rc.OverrideNameServers(overrideNS) + rc.OverrideSearch(tc.overrideSearch) + rc.OverrideOptions(tc.overrideOptions) + + assert.Equal(t, overrideNS, rc.NameServers()) + assert.Equal(t, tc.overrideSearch, rc.Search()) + assert.Equal(t, tc.overrideOptions, rc.Options()) + } + + if tc.addOption != "" { + options := rc.Options() + rc.AddOption(tc.addOption) + assert.Equal(t, append(options, tc.addOption), rc.Options()) + } + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) + }) + } +} + +func TestRCTransformForLegacyNw(t *testing.T) { + testcases := []struct { + name string + input string + ipv6 bool + overrideNS []string + }{ + { + name: "Routable IPv4 only", + input: "nameserver 10.0.0.1", + }, + { + name: "Routable IPv4 and IPv6, ipv6 enabled", + input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", + ipv6: true, + }, + { + name: "Routable IPv4 and IPv6, ipv6 disabled", + input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", + ipv6: false, + }, + { + name: "IPv4 localhost, ipv6 disabled", + input: "nameserver 127.0.0.53", + ipv6: false, + }, + { + name: "IPv4 localhost, ipv6 enabled", + input: "nameserver 127.0.0.53", + ipv6: true, + }, + { + name: "IPv4 and IPv6 localhost, ipv6 disabled", + input: "nameserver 127.0.0.53\nnameserver ::1", + ipv6: false, + }, + { + name: "IPv4 and IPv6 localhost, ipv6 enabled", + input: "nameserver 127.0.0.53\nnameserver ::1", + ipv6: true, + }, + { + name: "IPv4 localhost, IPv6 routeable, ipv6 enabled", + input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", + ipv6: true, + }, + { + name: "IPv4 localhost, IPv6 routeable, ipv6 disabled", + input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", + ipv6: false, + }, + { + name: "Override nameservers", + input: "nameserver 127.0.0.53", + overrideNS: []string{"127.0.0.1", "::1"}, + ipv6: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + rc, err := Parse(bytes.NewBufferString(tc.input), "/etc/resolv.conf") + require.NoError(t, err) + if tc.overrideNS != nil { + rc.OverrideNameServers(s2a(tc.overrideNS)) + } + + rc.TransformForLegacyNw(tc.ipv6) + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) + }) + } +} + +func TestRCTransformForIntNS(t *testing.T) { + mke := func(addr string, hostLoopback bool) ExtDNSEntry { + return ExtDNSEntry{ + Addr: netip.MustParseAddr(addr), + HostLoopback: hostLoopback, + } + } + + testcases := []struct { + name string + input string + intNameServer string + overrideNS []string + overrideOptions []string + reqdOptions []string + expExtServers []ExtDNSEntry + expErr string + }{ + { + name: "IPv4 only", + input: "nameserver 10.0.0.1", + expExtServers: []ExtDNSEntry{mke("10.0.0.1", true)}, + }, + { + name: "IPv4 and IPv6, ipv6 enabled", + input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", + expExtServers: []ExtDNSEntry{ + mke("10.0.0.1", true), + mke("fdb6:b8fe:b528::1", true), + }, + }, + { + name: "IPv4 localhost", + input: "nameserver 127.0.0.53", + expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, + }, + { + // Overriding the nameserver with a localhost address means use the container's + // loopback interface, not the host's. + name: "IPv4 localhost override", + input: "nameserver 10.0.0.1", + overrideNS: []string{"127.0.0.53"}, + expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)}, + }, + { + name: "IPv6 only", + input: "nameserver fd14:6e0e:f855::1", + expExtServers: []ExtDNSEntry{mke("fd14:6e0e:f855::1", true)}, + }, + { + name: "IPv4 and IPv6 localhost", + input: "nameserver 127.0.0.53\nnameserver ::1", + expExtServers: []ExtDNSEntry{ + mke("127.0.0.53", true), + mke("::1", true), + }, + }, + { + name: "ndots present and required", + input: "nameserver 127.0.0.53\noptions ndots:1", + reqdOptions: []string{"ndots:0"}, + expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, + }, + { + name: "ndots missing but required", + input: "nameserver 127.0.0.53", + reqdOptions: []string{"ndots:0"}, + expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, + }, + { + name: "ndots host, override and required", + input: "nameserver 127.0.0.53", + reqdOptions: []string{"ndots:0"}, + overrideOptions: []string{"ndots:2"}, + expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, + }, + { + name: "Extra required options", + input: "nameserver 127.0.0.53\noptions trust-ad", + reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"}, + expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, + }, + { + name: "No config", + input: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + rc, err := Parse(bytes.NewBufferString(tc.input), "/etc/resolv.conf") + require.NoError(t, err) + + if tc.intNameServer == "" { + tc.intNameServer = "127.0.0.11" + } + if len(tc.overrideNS) > 0 { + rc.OverrideNameServers(s2a(tc.overrideNS)) + } + if len(tc.overrideOptions) > 0 { + rc.OverrideOptions(tc.overrideOptions) + } + intNS := netip.MustParseAddr(tc.intNameServer) + extNameServers, err := rc.TransformForIntNS(intNS, tc.reqdOptions) + if tc.expErr != "" { + assert.ErrorContains(t, err, tc.expErr) + return + } + require.NoError(t, err) + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) + assert.Equal(t, tc.expExtServers, extNameServers) + }) + } +} + +// Check that invalid ndots options in the host's file are ignored, unless +// starting the internal resolver (which requires an ndots option), in which +// case invalid ndots should be replaced. +func TestRCTransformForIntNSInvalidNdots(t *testing.T) { + testcases := []struct { + name string + options string + reqdOptions []string + expVal string + expOptions []string + expNDotsFrom string + }{ + { + name: "Negative value", + options: "options ndots:-1", + expOptions: []string{"ndots:-1"}, + expVal: "-1", + expNDotsFrom: "host", + }, + { + name: "Invalid values with reqd ndots", + options: "options ndots:-1 foo:bar ndots ndots:", + reqdOptions: []string{"ndots:2"}, + expVal: "2", + expNDotsFrom: "internal", + expOptions: []string{"foo:bar", "ndots:2"}, + }, + { + name: "Valid value with reqd ndots", + options: "options ndots:1 foo:bar ndots ndots:", + reqdOptions: []string{"ndots:2"}, + expVal: "1", + expNDotsFrom: "host", + expOptions: []string{"ndots:1", "foo:bar"}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + content := "nameserver 8.8.8.8\n" + tc.options + rc, err := Parse(bytes.NewBufferString(content), "/etc/resolv.conf") + require.NoError(t, err) + _, err = rc.TransformForIntNS(netip.MustParseAddr("127.0.0.11"), tc.reqdOptions) + require.NoError(t, err) + + val, found := rc.Option("ndots") + assert.True(t, found) + assert.Equal(t, tc.expVal, val) + assert.Equal(t, tc.expNDotsFrom, rc.md.NDotsFrom) + assert.Equal(t, tc.expOptions, rc.options) + }) + } +} + +func TestRCRead(t *testing.T) { + d := t.TempDir() + path := filepath.Join(d, "resolv.conf") + + // Try to read a nonexistent file, equivalent to an empty file. + _, err := Load(path) + require.ErrorIs(t, err, os.ErrNotExist) + + err = os.WriteFile(path, []byte("options edns0"), 0o644) + require.NoError(t, err) + + // Read that file in the constructor. + rc, err := Load(path) + require.NoError(t, err) + assert.Equal(t, []string{"edns0"}, rc.Options()) + + // Pass in an os.File, check the path is extracted. + file, err := os.Open(path) + require.NoError(t, err) + rc, err = Parse(file, "") + _ = file.Close() + require.NoError(t, err) + assert.Equal(t, path, rc.md.SourcePath) +} + +func TestRCInvalidNS(t *testing.T) { + // A resolv.conf with an invalid nameserver address. + rc, err := Parse(bytes.NewBufferString("nameserver 1.2.3.4.5"), "") + require.NoError(t, err) + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) +} + +func TestRCSetHeader(t *testing.T) { + rc, err := Parse(bytes.NewBufferString("nameserver 127.0.0.53"), "/etc/resolv.conf") + require.NoError(t, err) + + rc.SetHeader("# This is a comment.") + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) +} + +func TestRCUnknownDirectives(t *testing.T) { + const input = ` +something unexpected +nameserver 127.0.0.53 +options ndots:1 +unrecognised thing +` + rc, err := Parse(bytes.NewBufferString(input), "/etc/resolv.conf") + require.NoError(t, err) + + content, err := rc.Generate(true) + require.NoError(t, err) + assertGolden(t, t.Name()+".golden", string(content)) +} + +func BenchmarkGenerate(b *testing.B) { + rc := &ResolvConf{ + nameServers: []netip.Addr{ + netip.MustParseAddr("8.8.8.8"), + netip.MustParseAddr("1.1.1.1"), + }, + search: []string{"example.com", "svc.local"}, + options: []string{"ndots:1", "ndots:2", "ndots:3"}, + other: []string{"something", "something else", "something else"}, + md: metadata{ + Header: `# Generated by Docker Engine. +# This file can be edited; Docker Engine will not make further changes once it +# has been modified.`, + NSOverride: true, + SearchOverride: true, + OptionsOverride: true, + NDotsFrom: "host", + ExtNameServers: []ExtDNSEntry{ + {Addr: netip.MustParseAddr("127.0.0.53"), HostLoopback: true}, + {Addr: netip.MustParseAddr("2.2.2.2"), HostLoopback: false}, + }, + InvalidNSs: []string{"256.256.256.256"}, + Warnings: []string{"bad nameserver ignored"}, + }, + } + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := rc.Generate(true) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/executor/oci/internal/resolvconf/testdata/.gitattributes b/executor/oci/internal/resolvconf/testdata/.gitattributes new file mode 100644 index 000000000000..6313b56c5784 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/executor/oci/internal/resolvconf/testdata/TestRCInvalidNS.golden b/executor/oci/internal/resolvconf/testdata/TestRCInvalidNS.golden new file mode 100644 index 000000000000..34c9172b32ee --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCInvalidNS.golden @@ -0,0 +1,4 @@ + +# Based on host file: '' +# Invalid nameservers: [1.2.3.4.5] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCModify/Add_option_no_overrides.golden b/executor/oci/internal/resolvconf/testdata/TestRCModify/Add_option_no_overrides.golden new file mode 100644 index 000000000000..e4e5b287d841 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCModify/Add_option_no_overrides.golden @@ -0,0 +1,7 @@ +nameserver 1.2.3.4 +search invalid +options ndots:0 attempts:3 + +# Based on host file: '' +# Overrides: [] +# Option ndots from: host diff --git a/executor/oci/internal/resolvconf/testdata/TestRCModify/Empty_overrides.golden b/executor/oci/internal/resolvconf/testdata/TestRCModify/Empty_overrides.golden new file mode 100644 index 000000000000..75a0159f2c66 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCModify/Empty_overrides.golden @@ -0,0 +1,3 @@ + +# Based on host file: '' +# Overrides: [nameservers search options] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCModify/No_content_no_overrides.golden b/executor/oci/internal/resolvconf/testdata/TestRCModify/No_content_no_overrides.golden new file mode 100644 index 000000000000..75a0159f2c66 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCModify/No_content_no_overrides.golden @@ -0,0 +1,3 @@ + +# Based on host file: '' +# Overrides: [nameservers search options] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCModify/No_overrides.golden b/executor/oci/internal/resolvconf/testdata/TestRCModify/No_overrides.golden new file mode 100644 index 000000000000..6ce29386fcc0 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCModify/No_overrides.golden @@ -0,0 +1,7 @@ +nameserver 1.2.3.4 +search invalid +options ndots:0 + +# Based on host file: '' +# Overrides: [] +# Option ndots from: host diff --git a/executor/oci/internal/resolvconf/testdata/TestRCModify/Overrides.golden b/executor/oci/internal/resolvconf/testdata/TestRCModify/Overrides.golden new file mode 100644 index 000000000000..62d6ef61dbd6 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCModify/Overrides.golden @@ -0,0 +1,8 @@ +nameserver 2.3.4.5 +nameserver fdba:acdd:587c::53 +search com invalid example +options ndots:1 edns0 trust-ad + +# Based on host file: '' +# Overrides: [nameservers search options] +# Option ndots from: override diff --git a/executor/oci/internal/resolvconf/testdata/TestRCSetHeader.golden b/executor/oci/internal/resolvconf/testdata/TestRCSetHeader.golden new file mode 100644 index 000000000000..beed1a765806 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCSetHeader.golden @@ -0,0 +1,6 @@ +# This is a comment. + +nameserver 127.0.0.53 + +# Based on host file: '/etc/resolv.conf' +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/Extra_required_options.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/Extra_required_options.golden new file mode 100644 index 000000000000..68fd46720747 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/Extra_required_options.golden @@ -0,0 +1,7 @@ +nameserver 127.0.0.11 +options trust-ad ndots:0 attempts:3 edns0 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53)] +# Overrides: [] +# Option ndots from: internal diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6,_ipv6_enabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6,_ipv6_enabled.golden new file mode 100644 index 000000000000..04308659f30f --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6,_ipv6_enabled.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(10.0.0.1) host(fdb6:b8fe:b528::1)] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6_localhost.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6_localhost.golden new file mode 100644 index 000000000000..387cc713b7b3 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_and_IPv6_localhost.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53) host(::1)] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost.golden new file mode 100644 index 000000000000..926d44d49a12 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53)] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost_override.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost_override.golden new file mode 100644 index 000000000000..da55aad0caf3 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost_override.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [127.0.0.53] +# Overrides: [nameservers] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_only.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_only.golden new file mode 100644 index 000000000000..58dadc47fb7e --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_only.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(10.0.0.1)] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv6_only.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv6_only.golden new file mode 100644 index 000000000000..3dd9cf69e88f --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv6_only.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(fd14:6e0e:f855::1)] +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/No_config.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/No_config.golden new file mode 100644 index 000000000000..253fecad24c9 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/No_config.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.11 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# NO EXTERNAL NAMESERVERS DEFINED +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_host,_override_and_required.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_host,_override_and_required.golden new file mode 100644 index 000000000000..dc469d58cda0 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_host,_override_and_required.golden @@ -0,0 +1,7 @@ +nameserver 127.0.0.11 +options ndots:2 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53)] +# Overrides: [options] +# Option ndots from: override diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_missing_but_required.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_missing_but_required.golden new file mode 100644 index 000000000000..827ecd295cb2 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_missing_but_required.golden @@ -0,0 +1,7 @@ +nameserver 127.0.0.11 +options ndots:0 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53)] +# Overrides: [] +# Option ndots from: internal diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_present_and_required.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_present_and_required.golden new file mode 100644 index 000000000000..d005fa38b6b3 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForIntNS/ndots_present_and_required.golden @@ -0,0 +1,7 @@ +nameserver 127.0.0.11 +options ndots:1 + +# Based on host file: '/etc/resolv.conf' (internal resolver) +# ExtServers: [host(127.0.0.53)] +# Overrides: [] +# Option ndots from: host diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_disabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_disabled.golden new file mode 100644 index 000000000000..165eafe282b9 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_disabled.golden @@ -0,0 +1,6 @@ +nameserver 8.8.8.8 +nameserver 8.8.4.4 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Used default nameservers. +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_enabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_enabled.golden new file mode 100644 index 000000000000..fd66dad64e48 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_and_IPv6_localhost,_ipv6_enabled.golden @@ -0,0 +1,8 @@ +nameserver 8.8.8.8 +nameserver 8.8.4.4 +nameserver 2001:4860:4860::8888 +nameserver 2001:4860:4860::8844 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Used default nameservers. +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_disabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_disabled.golden new file mode 100644 index 000000000000..165eafe282b9 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_disabled.golden @@ -0,0 +1,6 @@ +nameserver 8.8.8.8 +nameserver 8.8.4.4 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Used default nameservers. +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_enabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_enabled.golden new file mode 100644 index 000000000000..c188830fcd88 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_IPv6_routeable,_ipv6_enabled.golden @@ -0,0 +1,4 @@ +nameserver fd3e:2d1a:1f5a::1 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_disabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_disabled.golden new file mode 100644 index 000000000000..165eafe282b9 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_disabled.golden @@ -0,0 +1,6 @@ +nameserver 8.8.8.8 +nameserver 8.8.4.4 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Used default nameservers. +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_enabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_enabled.golden new file mode 100644 index 000000000000..fd66dad64e48 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/IPv4_localhost,_ipv6_enabled.golden @@ -0,0 +1,8 @@ +nameserver 8.8.8.8 +nameserver 8.8.4.4 +nameserver 2001:4860:4860::8888 +nameserver 2001:4860:4860::8844 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Used default nameservers. +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Override_nameservers.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Override_nameservers.golden new file mode 100644 index 000000000000..74f7abf96873 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Override_nameservers.golden @@ -0,0 +1,5 @@ +nameserver 127.0.0.1 +nameserver ::1 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Overrides: [nameservers] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_disabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_disabled.golden new file mode 100644 index 000000000000..cdeaadf88cd1 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_disabled.golden @@ -0,0 +1,4 @@ +nameserver 10.0.0.1 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_enabled.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_enabled.golden new file mode 100644 index 000000000000..e00cad933355 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_and_IPv6,_ipv6_enabled.golden @@ -0,0 +1,5 @@ +nameserver 10.0.0.1 +nameserver fdb6:b8fe:b528::1 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_only.golden b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_only.golden new file mode 100644 index 000000000000..cdeaadf88cd1 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_only.golden @@ -0,0 +1,4 @@ +nameserver 10.0.0.1 + +# Based on host file: '/etc/resolv.conf' (legacy) +# Overrides: [] diff --git a/executor/oci/internal/resolvconf/testdata/TestRCUnknownDirectives.golden b/executor/oci/internal/resolvconf/testdata/TestRCUnknownDirectives.golden new file mode 100644 index 000000000000..4fc961c558b4 --- /dev/null +++ b/executor/oci/internal/resolvconf/testdata/TestRCUnknownDirectives.golden @@ -0,0 +1,8 @@ +nameserver 127.0.0.53 +options ndots:1 +something unexpected +unrecognised thing + +# Based on host file: '/etc/resolv.conf' +# Overrides: [] +# Option ndots from: host diff --git a/executor/oci/internal/resolvconf/utils_test.go b/executor/oci/internal/resolvconf/utils_test.go new file mode 100644 index 000000000000..b982557c9236 --- /dev/null +++ b/executor/oci/internal/resolvconf/utils_test.go @@ -0,0 +1,42 @@ +package resolvconf + +import ( + "flag" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// allow running "go test -v -update ." to update golden files. +var updateGolden = flag.Bool("update", false, "update golden files") + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func assertGolden(t *testing.T, goldenFile string, actual string) { + t.Helper() + goldenFile = filepath.Join("testdata", goldenFile) + + if *updateGolden { + require.NoError(t, os.WriteFile(goldenFile, []byte(actual), 0644)) + } + expected, err := os.ReadFile(goldenFile) + require.NoError(t, err) + assert.Equal(t, string(expected), actual) +} + +// taken from https://github.com/moby/moby/blob/9af9d2742cc751c38900efaa385968c75ff3fdd7/internal/sliceutil/sliceutil.go#L26-L34 +func sliceutilMapper[In, Out any](fn func(In) Out) func([]In) []Out { + return func(s []In) []Out { + res := make([]Out, len(s)) + for i, v := range s { + res[i] = fn(v) + } + return res + } +} diff --git a/executor/oci/resolvconf.go b/executor/oci/resolvconf.go index 8354d101ccc5..936fa7671317 100644 --- a/executor/oci/resolvconf.go +++ b/executor/oci/resolvconf.go @@ -2,10 +2,11 @@ package oci import ( "context" + "net/netip" "os" "path/filepath" - "github.com/docker/docker/libnetwork/resolvconf" + "github.com/moby/buildkit/executor/oci/internal/resolvconf" "github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/util/flightcontrol" "github.com/moby/sys/user" @@ -76,41 +77,40 @@ func GetResolvConf(ctx context.Context, stateDir string, idmap *user.IdentityMap return struct{}{}, nil } - dt, err := os.ReadFile(resolvconfPath(netMode)) + rc, err := resolvconf.Load(resolvconfPath(netMode)) if err != nil && !errors.Is(err, os.ErrNotExist) { return struct{}{}, errors.WithStack(err) } - tmpPath := p + ".tmp" if dns != nil { - var ( - dnsNameservers = dns.Nameservers - dnsSearchDomains = dns.SearchDomains - dnsOptions = dns.Options - ) - if len(dns.Nameservers) == 0 { - dnsNameservers = resolvconf.GetNameservers(dt, resolvconf.IP) + if len(dns.Nameservers) > 0 { + var ns []netip.Addr + for _, addr := range dns.Nameservers { + ipAddr, err := netip.ParseAddr(addr) + if err != nil { + return struct{}{}, errors.WithStack(errors.Wrap(err, "bad nameserver address")) + } + ns = append(ns, ipAddr) + } + rc.OverrideNameServers(ns) } - if len(dns.SearchDomains) == 0 { - dnsSearchDomains = resolvconf.GetSearchDomains(dt) + if len(dns.SearchDomains) > 0 { + rc.OverrideSearch(dns.SearchDomains) } - if len(dns.Options) == 0 { - dnsOptions = resolvconf.GetOptions(dt) + if len(dns.Options) > 0 { + rc.OverrideOptions(dns.Options) } + } - f, err := resolvconf.Build(tmpPath, dnsNameservers, dnsSearchDomains, dnsOptions) - if err != nil { - return struct{}{}, errors.WithStack(err) - } - dt = f.Content + if netMode != pb.NetMode_HOST || len(rc.NameServers()) == 0 { + rc.TransformForLegacyNw(true) } - if netMode != pb.NetMode_HOST || len(resolvconf.GetNameservers(dt, resolvconf.IP)) == 0 { - f, err := resolvconf.FilterResolvDNS(dt, true) - if err != nil { - return struct{}{}, errors.WithStack(err) - } - dt = f.Content + tmpPath := p + ".tmp" + + dt, err := rc.Generate(false) + if err != nil { + return struct{}{}, errors.WithStack(err) } if err := os.WriteFile(tmpPath, dt, 0644); err != nil { @@ -124,6 +124,7 @@ func GetResolvConf(ctx context.Context, stateDir string, idmap *user.IdentityMap } } + // TODO(thaJeztah): can we avoid the write -> chown -> rename? if err := os.Rename(tmpPath, p); err != nil { return struct{}{}, errors.WithStack(err) } diff --git a/go.mod b/go.mod index b3865fde504d..9e8b6c7bc5db 100644 --- a/go.mod +++ b/go.mod @@ -160,7 +160,6 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/mount v0.3.4 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 1b5fd19cf7dd..28c9146a6511 100644 --- a/go.sum +++ b/go.sum @@ -281,8 +281,6 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= diff --git a/vendor/github.com/docker/docker/libnetwork/resolvconf/resolvconf.go b/vendor/github.com/docker/docker/libnetwork/resolvconf/resolvconf.go deleted file mode 100644 index d0c94074ad38..000000000000 --- a/vendor/github.com/docker/docker/libnetwork/resolvconf/resolvconf.go +++ /dev/null @@ -1,159 +0,0 @@ -// Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf -package resolvconf - -import ( - "bytes" - "fmt" - "net/netip" - "os" - - "github.com/docker/docker/libnetwork/internal/resolvconf" - "github.com/opencontainers/go-digest" -) - -// constants for the IP address type -const ( - IP = iota // IPv4 and IPv6 - IPv4 - IPv6 -) - -// File contains the resolv.conf content and its hash -type File struct { - Content []byte - Hash []byte -} - -func Path() string { - return resolvconf.Path() -} - -// Get returns the contents of /etc/resolv.conf and its hash -func Get() (*File, error) { - return GetSpecific(Path()) -} - -// GetSpecific returns the contents of the user specified resolv.conf file and its hash -func GetSpecific(path string) (*File, error) { - resolv, err := os.ReadFile(path) - if err != nil { - return nil, err - } - hash := digest.FromBytes(resolv) - return &File{Content: resolv, Hash: []byte(hash)}, nil -} - -// FilterResolvDNS cleans up the config in resolvConf. It has two main jobs: -// 1. It looks for localhost (127.*|::1) entries in the provided -// resolv.conf, removing local nameserver entries, and, if the resulting -// cleaned config has no defined nameservers left, adds default DNS entries -// 2. Given the caller provides the enable/disable state of IPv6, the filter -// code will remove all IPv6 nameservers if it is not enabled for containers -func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { - rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") - if err != nil { - return nil, err - } - rc.TransformForLegacyNw(ipv6Enabled) - content, err := rc.Generate(false) - if err != nil { - return nil, err - } - hash := digest.FromBytes(content) - return &File{Content: content, Hash: []byte(hash)}, nil -} - -// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf -func GetNameservers(resolvConf []byte, kind int) []string { - rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") - if err != nil { - return nil - } - nsAddrs := rc.NameServers() - var nameservers []string - for _, addr := range nsAddrs { - if kind == IP { - nameservers = append(nameservers, addr.String()) - } else if kind == IPv4 && addr.Is4() { - nameservers = append(nameservers, addr.String()) - } else if kind == IPv6 && addr.Is6() { - nameservers = append(nameservers, addr.String()) - } - } - return nameservers -} - -// GetNameserversAsPrefix returns nameservers (if any) listed in -// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") -func GetNameserversAsPrefix(resolvConf []byte) []netip.Prefix { - rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") - if err != nil { - return nil - } - nsAddrs := rc.NameServers() - nameservers := make([]netip.Prefix, 0, len(nsAddrs)) - for _, addr := range nsAddrs { - nameservers = append(nameservers, netip.PrefixFrom(addr, addr.BitLen())) - } - return nameservers -} - -// GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf -// If more than one search line is encountered, only the contents of the last -// one is returned. -func GetSearchDomains(resolvConf []byte) []string { - rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") - if err != nil { - return nil - } - return rc.Search() -} - -// GetOptions returns options (if any) listed in /etc/resolv.conf -// If more than one options line is encountered, only the contents of the last -// one is returned. -func GetOptions(resolvConf []byte) []string { - rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") - if err != nil { - return nil - } - return rc.Options() -} - -// Build generates and writes a configuration file to path containing a nameserver -// entry for every element in nameservers, a "search" entry for every element in -// dnsSearch, and an "options" entry for every element in dnsOptions. It returns -// a File containing the generated content and its (sha256) hash. -// -// Note that the resolv.conf file is written, but the hash file is not. -func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, error) { - var ns []netip.Addr - for _, addr := range nameservers { - ipAddr, err := netip.ParseAddr(addr) - if err != nil { - return nil, fmt.Errorf("bad nameserver address: %w", err) - } - ns = append(ns, ipAddr) - } - rc := resolvconf.ResolvConf{} - rc.OverrideNameServers(ns) - rc.OverrideSearch(dnsSearch) - rc.OverrideOptions(dnsOptions) - - content, err := rc.Generate(false) - if err != nil { - return nil, err - } - - // Write the resolv.conf file - it's bind-mounted into the container, so can't - // move a temp file into place, just have to truncate and write it. - // - // TODO(thaJeztah): the Build function is currently only used by BuildKit, which only uses "File.Content", and doesn't require the file to be written. - if err := os.WriteFile(path, content, 0o644); err != nil { - return nil, err - } - - // TODO(thaJeztah): the Build function is currently only used by BuildKit, which does not use the Hash - hash := digest.FromBytes(content) - return &File{Content: content, Hash: []byte(hash)}, nil -} diff --git a/vendor/github.com/moby/sys/atomicwriter/LICENSE b/vendor/github.com/moby/sys/atomicwriter/LICENSE deleted file mode 100644 index d64569567334..000000000000 --- a/vendor/github.com/moby/sys/atomicwriter/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/vendor/github.com/moby/sys/atomicwriter/atomicwriter.go b/vendor/github.com/moby/sys/atomicwriter/atomicwriter.go deleted file mode 100644 index d0d3be88e124..000000000000 --- a/vendor/github.com/moby/sys/atomicwriter/atomicwriter.go +++ /dev/null @@ -1,245 +0,0 @@ -// Package atomicwriter provides utilities to perform atomic writes to a -// file or set of files. -package atomicwriter - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "syscall" - - "github.com/moby/sys/sequential" -) - -func validateDestination(fileName string) error { - if fileName == "" { - return errors.New("file name is empty") - } - if dir := filepath.Dir(fileName); dir != "" && dir != "." && dir != ".." { - di, err := os.Stat(dir) - if err != nil { - return fmt.Errorf("invalid output path: %w", err) - } - if !di.IsDir() { - return fmt.Errorf("invalid output path: %w", &os.PathError{Op: "stat", Path: dir, Err: syscall.ENOTDIR}) - } - } - - // Deliberately using Lstat here to match the behavior of [os.Rename], - // which is used when completing the write and does not resolve symlinks. - fi, err := os.Lstat(fileName) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to stat output path: %w", err) - } - - switch mode := fi.Mode(); { - case mode.IsRegular(): - return nil // Regular file - case mode&os.ModeDir != 0: - return errors.New("cannot write to a directory") - case mode&os.ModeSymlink != 0: - return errors.New("cannot write to a symbolic link directly") - case mode&os.ModeNamedPipe != 0: - return errors.New("cannot write to a named pipe (FIFO)") - case mode&os.ModeSocket != 0: - return errors.New("cannot write to a socket") - case mode&os.ModeDevice != 0: - if mode&os.ModeCharDevice != 0 { - return errors.New("cannot write to a character device file") - } - return errors.New("cannot write to a block device file") - case mode&os.ModeSetuid != 0: - return errors.New("cannot write to a setuid file") - case mode&os.ModeSetgid != 0: - return errors.New("cannot write to a setgid file") - case mode&os.ModeSticky != 0: - return errors.New("cannot write to a sticky bit file") - default: - return fmt.Errorf("unknown file mode: %[1]s (%#[1]o)", mode) - } -} - -// New returns a WriteCloser so that writing to it writes to a -// temporary file and closing it atomically changes the temporary file to -// destination path. Writing and closing concurrently is not allowed. -// NOTE: umask is not considered for the file's permissions. -// -// New uses [sequential.CreateTemp] to use sequential file access on Windows, -// avoiding depleting the standby list un-necessarily. On Linux, this equates to -// a regular [os.CreateTemp]. Refer to the [Win32 API documentation] for details -// on sequential file access. -// -// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN -func New(filename string, perm os.FileMode) (io.WriteCloser, error) { - if err := validateDestination(filename); err != nil { - return nil, err - } - abspath, err := filepath.Abs(filename) - if err != nil { - return nil, err - } - - f, err := sequential.CreateTemp(filepath.Dir(abspath), ".tmp-"+filepath.Base(filename)) - if err != nil { - return nil, err - } - return &atomicFileWriter{ - f: f, - fn: abspath, - perm: perm, - }, nil -} - -// WriteFile atomically writes data to a file named by filename and with the -// specified permission bits. The given filename is created if it does not exist, -// but the destination directory must exist. It can be used as a drop-in replacement -// for [os.WriteFile], but currently does not allow the destination path to be -// a symlink. WriteFile is implemented using [New] for its implementation. -// -// NOTE: umask is not considered for the file's permissions. -func WriteFile(filename string, data []byte, perm os.FileMode) error { - f, err := New(filename, perm) - if err != nil { - return err - } - n, err := f.Write(data) - if err == nil && n < len(data) { - err = io.ErrShortWrite - f.(*atomicFileWriter).writeErr = err - } - if err1 := f.Close(); err == nil { - err = err1 - } - return err -} - -type atomicFileWriter struct { - f *os.File - fn string - writeErr error - written bool - perm os.FileMode -} - -func (w *atomicFileWriter) Write(dt []byte) (int, error) { - w.written = true - n, err := w.f.Write(dt) - if err != nil { - w.writeErr = err - } - return n, err -} - -func (w *atomicFileWriter) Close() (retErr error) { - defer func() { - if err := os.Remove(w.f.Name()); !errors.Is(err, os.ErrNotExist) && retErr == nil { - retErr = err - } - }() - if err := w.f.Sync(); err != nil { - _ = w.f.Close() - return err - } - if err := w.f.Close(); err != nil { - return err - } - if err := os.Chmod(w.f.Name(), w.perm); err != nil { - return err - } - if w.writeErr == nil && w.written { - return os.Rename(w.f.Name(), w.fn) - } - return nil -} - -// WriteSet is used to atomically write a set -// of files and ensure they are visible at the same time. -// Must be committed to a new directory. -type WriteSet struct { - root string -} - -// NewWriteSet creates a new atomic write set to -// atomically create a set of files. The given directory -// is used as the base directory for storing files before -// commit. If no temporary directory is given the system -// default is used. -func NewWriteSet(tmpDir string) (*WriteSet, error) { - td, err := os.MkdirTemp(tmpDir, "write-set-") - if err != nil { - return nil, err - } - - return &WriteSet{ - root: td, - }, nil -} - -// WriteFile writes a file to the set, guaranteeing the file -// has been synced. -func (ws *WriteSet) WriteFile(filename string, data []byte, perm os.FileMode) error { - f, err := ws.FileWriter(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) - if err != nil { - return err - } - n, err := f.Write(data) - if err == nil && n < len(data) { - err = io.ErrShortWrite - } - if err1 := f.Close(); err == nil { - err = err1 - } - return err -} - -type syncFileCloser struct { - *os.File -} - -func (w syncFileCloser) Close() error { - err := w.File.Sync() - if err1 := w.File.Close(); err == nil { - err = err1 - } - return err -} - -// FileWriter opens a file writer inside the set. The file -// should be synced and closed before calling commit. -// -// FileWriter uses [sequential.OpenFile] to use sequential file access on Windows, -// avoiding depleting the standby list un-necessarily. On Linux, this equates to -// a regular [os.OpenFile]. Refer to the [Win32 API documentation] for details -// on sequential file access. -// -// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN -func (ws *WriteSet) FileWriter(name string, flag int, perm os.FileMode) (io.WriteCloser, error) { - f, err := sequential.OpenFile(filepath.Join(ws.root, name), flag, perm) - if err != nil { - return nil, err - } - return syncFileCloser{f}, nil -} - -// Cancel cancels the set and removes all temporary data -// created in the set. -func (ws *WriteSet) Cancel() error { - return os.RemoveAll(ws.root) -} - -// Commit moves all created files to the target directory. The -// target directory must not exist and the parent of the target -// directory must exist. -func (ws *WriteSet) Commit(target string) error { - return os.Rename(ws.root, target) -} - -// String returns the location the set is writing to. -func (ws *WriteSet) String() string { - return ws.root -} diff --git a/vendor/modules.txt b/vendor/modules.txt index b5f0795e59d0..ac5e47c03534 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -489,8 +489,6 @@ github.com/docker/cli/cli/config/types github.com/docker/cli/cli/connhelper/commandconn # github.com/docker/docker v28.3.0+incompatible ## explicit -github.com/docker/docker/libnetwork/internal/resolvconf -github.com/docker/docker/libnetwork/resolvconf github.com/docker/docker/profiles/seccomp # github.com/docker/docker-credential-helpers v0.9.3 ## explicit; go 1.21 @@ -632,9 +630,6 @@ github.com/moby/locker ## explicit; go 1.19 github.com/moby/patternmatcher github.com/moby/patternmatcher/ignorefile -# github.com/moby/sys/atomicwriter v0.1.0 -## explicit; go 1.18 -github.com/moby/sys/atomicwriter # github.com/moby/sys/mount v0.3.4 ## explicit; go 1.17 github.com/moby/sys/mount