Browse code

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 authored on 2023/11/04 22:12:20
Showing 8 changed files
... ...
@@ -2530,6 +2530,21 @@ definitions:
2530 2530
         example:
2531 2531
           com.example.some-label: "some-value"
2532 2532
           com.example.some-other-label: "some-other-value"
2533
+      DNSNames:
2534
+        description: |
2535
+          List of all DNS names an endpoint has on a specific network. This
2536
+          list is based on the container name, network aliases, container short
2537
+          ID, and hostname.
2538
+
2539
+          These DNS names are non-fully qualified but can contain several dots.
2540
+          You can get fully qualified DNS names by appending `.<network-name>`.
2541
+          For instance, if container name is `my.ctr` and the network is named
2542
+          `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be
2543
+          `my.ctr.testnet`.
2544
+        type: array
2545
+        items:
2546
+          type: string
2547
+        example: ["foobar", "server_x", "server_y", "my.ctr"]
2533 2548
 
2534 2549
   EndpointIPAMConfig:
2535 2550
     description: |
... ...
@@ -13,7 +13,7 @@ type EndpointSettings struct {
13 13
 	// Configurations
14 14
 	IPAMConfig *EndpointIPAMConfig
15 15
 	Links      []string
16
-	Aliases    []string
16
+	Aliases    []string // Aliases holds the list of extra, user-specified DNS names for this endpoint.
17 17
 	MacAddress string
18 18
 	// Operational data
19 19
 	NetworkID           string
... ...
@@ -25,6 +25,9 @@ type EndpointSettings struct {
25 25
 	GlobalIPv6Address   string
26 26
 	GlobalIPv6PrefixLen int
27 27
 	DriverOpts          map[string]string
28
+	// DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to
29
+	// generate PTR records.
30
+	DNSNames []string
28 31
 }
29 32
 
30 33
 // Copy makes a deep copy of `EndpointSettings`
... ...
@@ -43,6 +46,12 @@ func (es *EndpointSettings) Copy() *EndpointSettings {
43 43
 		aliases := make([]string, 0, len(es.Aliases))
44 44
 		epCopy.Aliases = append(aliases, es.Aliases...)
45 45
 	}
46
+
47
+	if len(es.DNSNames) > 0 {
48
+		epCopy.DNSNames = make([]string, len(es.DNSNames))
49
+		copy(epCopy.DNSNames, es.DNSNames)
50
+	}
51
+
46 52
 	return &epCopy
47 53
 }
48 54
 
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/docker/docker/daemon/network"
20 20
 	"github.com/docker/docker/errdefs"
21 21
 	"github.com/docker/docker/internal/multierror"
22
+	"github.com/docker/docker/internal/sliceutil"
22 23
 	"github.com/docker/docker/libnetwork"
23 24
 	"github.com/docker/docker/libnetwork/netlabel"
24 25
 	"github.com/docker/docker/libnetwork/options"
... ...
@@ -650,6 +651,9 @@ func cleanOperationalData(es *network.EndpointSettings) {
650 650
 
651 651
 func (daemon *Daemon) updateNetworkConfig(container *container.Container, n *libnetwork.Network, endpointConfig *networktypes.EndpointSettings, updateSettings bool) error {
652 652
 	if containertypes.NetworkMode(n.Name()).IsUserDefined() {
653
+		endpointConfig.DNSNames = buildEndpointDNSNames(container, endpointConfig.Aliases)
654
+
655
+		// TODO(aker): remove this code once endpoint's DNSNames is used for real.
653 656
 		addShortID := true
654 657
 		shortID := stringid.TruncateID(container.ID)
655 658
 		for _, alias := range endpointConfig.Aliases {
... ...
@@ -687,6 +691,29 @@ func (daemon *Daemon) updateNetworkConfig(container *container.Container, n *lib
687 687
 	return nil
688 688
 }
689 689
 
690
+// buildEndpointDNSNames constructs the list of DNSNames that should be assigned to a given endpoint. The order within
691
+// the returned slice is important as the first entry will be used to generate the PTR records (for IPv4 and v6)
692
+// associated to this endpoint.
693
+func buildEndpointDNSNames(ctr *container.Container, aliases []string) []string {
694
+	var dnsNames []string
695
+
696
+	if ctr.Name != "" {
697
+		dnsNames = append(dnsNames, strings.TrimPrefix(ctr.Name, "/"))
698
+	}
699
+
700
+	dnsNames = append(dnsNames, aliases...)
701
+
702
+	if ctr.ID != "" {
703
+		dnsNames = append(dnsNames, stringid.TruncateID(ctr.ID))
704
+	}
705
+
706
+	if ctr.Config.Hostname != "" {
707
+		dnsNames = append(dnsNames, ctr.Config.Hostname)
708
+	}
709
+
710
+	return sliceutil.Dedup(dnsNames)
711
+}
712
+
690 713
 func (daemon *Daemon) connectToNetwork(cfg *config.Config, container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) {
691 714
 	start := time.Now()
692 715
 	if container.HostConfig.NetworkMode.IsContainer() {
693 716
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package daemon
1
+
2
+import (
3
+	"encoding/json"
4
+	"testing"
5
+
6
+	containertypes "github.com/docker/docker/api/types/container"
7
+	networktypes "github.com/docker/docker/api/types/network"
8
+	"github.com/docker/docker/container"
9
+	"github.com/docker/docker/libnetwork"
10
+	"gotest.tools/v3/assert"
11
+	is "gotest.tools/v3/assert/cmp"
12
+)
13
+
14
+func TestDNSNamesAreEquivalentToAliases(t *testing.T) {
15
+	d := &Daemon{}
16
+	ctr := &container.Container{
17
+		ID:   "35de8003b19e27f636fc6ecbf4d7072558b872a8544f287fd69ad8182ad59023",
18
+		Name: "foobar",
19
+		Config: &containertypes.Config{
20
+			Hostname: "baz",
21
+		},
22
+	}
23
+	nw := buildNetwork(t, map[string]any{
24
+		"id":          "1234567890",
25
+		"name":        "testnet",
26
+		"networkType": "bridge",
27
+		"enableIPv6":  false,
28
+	})
29
+	epSettings := &networktypes.EndpointSettings{
30
+		Aliases: []string{"myctr"},
31
+	}
32
+
33
+	if err := d.updateNetworkConfig(ctr, nw, epSettings, false); err != nil {
34
+		t.Fatal(err)
35
+	}
36
+
37
+	assert.Check(t, is.DeepEqual(epSettings.Aliases, []string{"myctr", "35de8003b19e", "baz"}))
38
+	assert.Check(t, is.DeepEqual(epSettings.DNSNames, []string{"foobar", "myctr", "35de8003b19e", "baz"}))
39
+}
40
+
41
+func buildNetwork(t *testing.T, config map[string]any) *libnetwork.Network {
42
+	t.Helper()
43
+
44
+	b, err := json.Marshal(config)
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+
49
+	nw := &libnetwork.Network{}
50
+	if err := nw.UnmarshalJSON(b); err != nil {
51
+		t.Fatal(err)
52
+	}
53
+
54
+	return nw
55
+}
... ...
@@ -821,9 +821,12 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
821 821
 			createOptions = append(createOptions, libnetwork.CreateOptionIpam(ip, ip6, ipList, nil))
822 822
 		}
823 823
 
824
+		// TODO(aker): remove this loop once endpoint's DNSNames is used for real
824 825
 		for _, alias := range epConfig.Aliases {
825 826
 			createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias))
826 827
 		}
828
+		createOptions = append(createOptions, libnetwork.CreateOptionDNSNames(epConfig.DNSNames))
829
+
827 830
 		for k, v := range epConfig.DriverOpts {
828 831
 			createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
829 832
 		}
... ...
@@ -68,6 +68,8 @@ keywords: "API, Docker, rcli, REST, documentation"
68 68
 * The `Container` and `ContainerConfig` fields in the `GET /images/{name}/json`
69 69
   response are deprecated and will no longer be included in API v1.45.
70 70
 * `GET /info` now includes `status` properties in `Runtimes`.
71
+* A new field named `DNSNames` and containing all non-fully qualified DNS names
72
+  a container takes on a specific network has been added to `GET /containers/{name:.*}/json`.
71 73
 
72 74
 ## v1.43 API changes
73 75
 
74 76
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package sliceutil
1
+
2
+func Dedup[T comparable](slice []T) []T {
3
+	keys := make(map[T]struct{})
4
+	out := make([]T, 0, len(slice))
5
+	for _, s := range slice {
6
+		if _, ok := keys[s]; !ok {
7
+			out = append(out, s)
8
+			keys[s] = struct{}{}
9
+		}
10
+	}
11
+	return out
12
+}
... ...
@@ -972,6 +972,14 @@ func CreateOptionAnonymous() EndpointOption {
972 972
 	}
973 973
 }
974 974
 
975
+// CreateOptionDNSNames specifies the list of (non fully qualified) DNS names associated to an endpoint. These will be
976
+// used to populate the embedded DNS server. Order matters: first name will be used to generate PTR records.
977
+func CreateOptionDNSNames(names []string) EndpointOption {
978
+	return func(ep *Endpoint) {
979
+		ep.dnsNames = names
980
+	}
981
+}
982
+
975 983
 // CreateOptionDisableResolution function returns an option setter to indicate
976 984
 // this endpoint doesn't want embedded DNS server functionality
977 985
 func CreateOptionDisableResolution() EndpointOption {