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>
This commit is contained in:
Albin Kerouanton 2023-11-04 14:12:20 +01:00
parent dc1e73cbbf
commit ab8968437b
No known key found for this signature in database
GPG key ID: 630B8E1DCBDB1864
8 changed files with 134 additions and 1 deletions

View file

@ -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: |

View file

@ -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
}

View file

@ -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() {

View file

@ -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
}

View file

@ -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}))
}

View file

@ -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

View file

@ -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
}

View file

@ -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 {