소스 검색

daemon: build the list of endpoint's DNS names

Instead of special-casing anonymous endpoints in libnetwork, let the
daemon specify what (non fully qualified) DNS names should be associated
to container's endpoints.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Albin Kerouanton 1 년 전
부모
커밋
ab8968437b

+ 15 - 0
api/swagger.yaml

@@ -2530,6 +2530,21 @@ definitions:
         example:
           com.example.some-label: "some-value"
           com.example.some-other-label: "some-other-value"
+      DNSNames:
+        description: |
+          List of all DNS names an endpoint has on a specific network. This
+          list is based on the container name, network aliases, container short
+          ID, and hostname.
+
+          These DNS names are non-fully qualified but can contain several dots.
+          You can get fully qualified DNS names by appending `.<network-name>`.
+          For instance, if container name is `my.ctr` and the network is named
+          `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be
+          `my.ctr.testnet`.
+        type: array
+        items:
+          type: string
+        example: ["foobar", "server_x", "server_y", "my.ctr"]
 
   EndpointIPAMConfig:
     description: |

+ 10 - 1
api/types/network/endpoint.go

@@ -13,7 +13,7 @@ type EndpointSettings struct {
 	// Configurations
 	IPAMConfig *EndpointIPAMConfig
 	Links      []string
-	Aliases    []string
+	Aliases    []string // Aliases holds the list of extra, user-specified DNS names for this endpoint.
 	MacAddress string
 	// Operational data
 	NetworkID           string
@@ -25,6 +25,9 @@ type EndpointSettings struct {
 	GlobalIPv6Address   string
 	GlobalIPv6PrefixLen int
 	DriverOpts          map[string]string
+	// DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to
+	// generate PTR records.
+	DNSNames []string
 }
 
 // Copy makes a deep copy of `EndpointSettings`
@@ -43,6 +46,12 @@ func (es *EndpointSettings) Copy() *EndpointSettings {
 		aliases := make([]string, 0, len(es.Aliases))
 		epCopy.Aliases = append(aliases, es.Aliases...)
 	}
+
+	if len(es.DNSNames) > 0 {
+		epCopy.DNSNames = make([]string, len(es.DNSNames))
+		copy(epCopy.DNSNames, es.DNSNames)
+	}
+
 	return &epCopy
 }
 

+ 27 - 0
daemon/container_operations.go

@@ -19,6 +19,7 @@ import (
 	"github.com/docker/docker/daemon/network"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/internal/multierror"
+	"github.com/docker/docker/internal/sliceutil"
 	"github.com/docker/docker/libnetwork"
 	"github.com/docker/docker/libnetwork/netlabel"
 	"github.com/docker/docker/libnetwork/options"
@@ -650,6 +651,9 @@ func cleanOperationalData(es *network.EndpointSettings) {
 
 func (daemon *Daemon) updateNetworkConfig(container *container.Container, n *libnetwork.Network, endpointConfig *networktypes.EndpointSettings, updateSettings bool) error {
 	if containertypes.NetworkMode(n.Name()).IsUserDefined() {
+		endpointConfig.DNSNames = buildEndpointDNSNames(container, endpointConfig.Aliases)
+
+		// TODO(aker): remove this code once endpoint's DNSNames is used for real.
 		addShortID := true
 		shortID := stringid.TruncateID(container.ID)
 		for _, alias := range endpointConfig.Aliases {
@@ -687,6 +691,29 @@ func (daemon *Daemon) updateNetworkConfig(container *container.Container, n *lib
 	return nil
 }
 
+// buildEndpointDNSNames constructs the list of DNSNames that should be assigned to a given endpoint. The order within
+// the returned slice is important as the first entry will be used to generate the PTR records (for IPv4 and v6)
+// associated to this endpoint.
+func buildEndpointDNSNames(ctr *container.Container, aliases []string) []string {
+	var dnsNames []string
+
+	if ctr.Name != "" {
+		dnsNames = append(dnsNames, strings.TrimPrefix(ctr.Name, "/"))
+	}
+
+	dnsNames = append(dnsNames, aliases...)
+
+	if ctr.ID != "" {
+		dnsNames = append(dnsNames, stringid.TruncateID(ctr.ID))
+	}
+
+	if ctr.Config.Hostname != "" {
+		dnsNames = append(dnsNames, ctr.Config.Hostname)
+	}
+
+	return sliceutil.Dedup(dnsNames)
+}
+
 func (daemon *Daemon) connectToNetwork(cfg *config.Config, container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) {
 	start := time.Now()
 	if container.HostConfig.NetworkMode.IsContainer() {

+ 56 - 0
daemon/container_operations_test.go

@@ -0,0 +1,56 @@
+package daemon
+
+import (
+	"encoding/json"
+	"testing"
+
+	containertypes "github.com/docker/docker/api/types/container"
+	networktypes "github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/container"
+	"github.com/docker/docker/libnetwork"
+	"gotest.tools/v3/assert"
+	is "gotest.tools/v3/assert/cmp"
+)
+
+func TestDNSNamesAreEquivalentToAliases(t *testing.T) {
+	d := &Daemon{}
+	ctr := &container.Container{
+		ID:   "35de8003b19e27f636fc6ecbf4d7072558b872a8544f287fd69ad8182ad59023",
+		Name: "foobar",
+		Config: &containertypes.Config{
+			Hostname: "baz",
+		},
+	}
+	nw := buildNetwork(t, map[string]any{
+		"id":          "1234567890",
+		"name":        "testnet",
+		"networkType": "bridge",
+		"enableIPv6":  false,
+	})
+	epSettings := &networktypes.EndpointSettings{
+		Aliases: []string{"myctr"},
+	}
+
+	if err := d.updateNetworkConfig(ctr, nw, epSettings, false); err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Check(t, is.DeepEqual(epSettings.Aliases, []string{"myctr", "35de8003b19e", "baz"}))
+	assert.Check(t, is.DeepEqual(epSettings.DNSNames, []string{"foobar", "myctr", "35de8003b19e", "baz"}))
+}
+
+func buildNetwork(t *testing.T, config map[string]any) *libnetwork.Network {
+	t.Helper()
+
+	b, err := json.Marshal(config)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	nw := &libnetwork.Network{}
+	if err := nw.UnmarshalJSON(b); err != nil {
+		t.Fatal(err)
+	}
+
+	return nw
+}

+ 3 - 0
daemon/network.go

@@ -821,9 +821,12 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
 			createOptions = append(createOptions, libnetwork.CreateOptionIpam(ip, ip6, ipList, nil))
 		}
 
+		// TODO(aker): remove this loop once endpoint's DNSNames is used for real
 		for _, alias := range epConfig.Aliases {
 			createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias))
 		}
+		createOptions = append(createOptions, libnetwork.CreateOptionDNSNames(epConfig.DNSNames))
+
 		for k, v := range epConfig.DriverOpts {
 			createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
 		}

+ 2 - 0
docs/api/version-history.md

@@ -68,6 +68,8 @@ keywords: "API, Docker, rcli, REST, documentation"
 * The `Container` and `ContainerConfig` fields in the `GET /images/{name}/json`
   response are deprecated and will no longer be included in API v1.45.
 * `GET /info` now includes `status` properties in `Runtimes`.
+* A new field named `DNSNames` and containing all non-fully qualified DNS names
+  a container takes on a specific network has been added to `GET /containers/{name:.*}/json`.
 
 ## v1.43 API changes
 

+ 13 - 0
internal/sliceutil/sliceutil.go

@@ -0,0 +1,13 @@
+package sliceutil
+
+func Dedup[T comparable](slice []T) []T {
+	keys := make(map[T]struct{})
+	out := make([]T, 0, len(slice))
+	for _, s := range slice {
+		if _, ok := keys[s]; !ok {
+			out = append(out, s)
+			keys[s] = struct{}{}
+		}
+	}
+	return out
+}

+ 8 - 0
libnetwork/endpoint.go

@@ -972,6 +972,14 @@ func CreateOptionAnonymous() EndpointOption {
 	}
 }
 
+// CreateOptionDNSNames specifies the list of (non fully qualified) DNS names associated to an endpoint. These will be
+// used to populate the embedded DNS server. Order matters: first name will be used to generate PTR records.
+func CreateOptionDNSNames(names []string) EndpointOption {
+	return func(ep *Endpoint) {
+		ep.dnsNames = names
+	}
+}
+
 // CreateOptionDisableResolution function returns an option setter to indicate
 // this endpoint doesn't want embedded DNS server functionality
 func CreateOptionDisableResolution() EndpointOption {