Merge pull request 'a container may name its resolvers and its own address' (#26) from feat/a-container-may-name-its-resolver-and-its-address into main
This commit was merged in pull request #26.
This commit is contained in:
@@ -1508,6 +1508,12 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner,
|
|||||||
if r.Network != "" {
|
if r.Network != "" {
|
||||||
args = append(args, "--network", r.Network)
|
args = append(args, "--network", r.Network)
|
||||||
}
|
}
|
||||||
|
for _, d := range r.Dns {
|
||||||
|
args = append(args, "--dns", d)
|
||||||
|
}
|
||||||
|
if r.IP != "" {
|
||||||
|
args = append(args, "--ip", r.IP)
|
||||||
|
}
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"--label", specLabel+"="+want, "--label", idLabel+"="+r.ID)
|
"--label", specLabel+"="+want, "--label", idLabel+"="+r.ID)
|
||||||
for _, k := range sortedKeys(r.Env) {
|
for _, k := range sortedKeys(r.Env) {
|
||||||
|
|||||||
@@ -403,3 +403,35 @@ func TestAContainerIsGivenItsEnvironmentFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A container may name its resolvers and its own address — the shape a module shipping its own
|
||||||
|
// validating DNS needs: the resolver pinned where its siblings can find it, the siblings pointed
|
||||||
|
// at it. Both flags take addresses, so both reach the runtime verbatim.
|
||||||
|
func TestAContainerIsGivenItsResolverAndItsAddress(t *testing.T) {
|
||||||
|
var ran []string
|
||||||
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||||
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||||
|
if len(args) > 0 && args[0] == "container" {
|
||||||
|
return "", fmt.Errorf("no such container")
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
d := declare(t, `{"id":"imap","type":"container","name":"mailu-imap",`+
|
||||||
|
`"image":"dovecot@sha256:0000000000000000000000000000000000000000000000000000000000000000",`+
|
||||||
|
`"network":"mailu","dns":["192.168.203.254"],"ip":"192.168.203.7"}`)
|
||||||
|
|
||||||
|
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{},
|
||||||
|
store.OriginDeclared, run, nil, nil)
|
||||||
|
|
||||||
|
var started string
|
||||||
|
for _, line := range ran {
|
||||||
|
if strings.Contains(line, "run ") {
|
||||||
|
started = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{"--dns 192.168.203.254", "--ip 192.168.203.7"} {
|
||||||
|
if !strings.Contains(started, want) {
|
||||||
|
t.Errorf("the container was started without %q:\n%s", want, started)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -807,6 +809,23 @@ type Container struct {
|
|||||||
// worse failure mode.
|
// worse failure mode.
|
||||||
Network string `json:"network,omitempty"`
|
Network string `json:"network,omitempty"`
|
||||||
|
|
||||||
|
// Dns is the resolvers this container asks, passed to the runtime unchanged.
|
||||||
|
//
|
||||||
|
// **Because some software refuses to run behind the runtime's forwarding resolver.** A mail
|
||||||
|
// server's admin demands a DNSSEC-validating resolver, and the runtime's own (127.0.0.11)
|
||||||
|
// forwards to whatever the machine has — so a module that ships its own validating resolver
|
||||||
|
// must be able to point its other containers at it. Addresses, not names: the runtime's flag
|
||||||
|
// takes only addresses, which is also why IP below exists — the resolver has to be somewhere
|
||||||
|
// its siblings can name before any of them can resolve anything.
|
||||||
|
Dns []string `json:"dns,omitempty"`
|
||||||
|
|
||||||
|
// IP is this container's address on its network, passed to the runtime unchanged.
|
||||||
|
//
|
||||||
|
// Only meaningful on a user-defined network, and refused by the runtime elsewhere. Exists for
|
||||||
|
// exactly one shape: a container others must reach *before* name resolution works — a
|
||||||
|
// module's own DNS resolver being the case that forced it (see Dns).
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
|
||||||
// RestartOn names resources whose change means this container must be recreated — the same
|
// RestartOn names resources whose change means this container must be recreated — the same
|
||||||
// field a service has, for the same reason (novox/hq 04-ISSUES/009). A container reads a
|
// field a service has, for the same reason (novox/hq 04-ISSUES/009). A container reads a
|
||||||
// mounted file once at start; a changed file leaves the running process holding the old value,
|
// mounted file once at start; a changed file leaves the running process holding the old value,
|
||||||
@@ -875,6 +894,23 @@ func (c *Container) validate(where string, _ bool) []string {
|
|||||||
problems = append(problems, where+": "+err.Error())
|
problems = append(problems, where+": "+err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The runtime's flags take addresses, and a name here would be handed to it verbatim and
|
||||||
|
// refused at create — after the old container was already removed. Refused on arrival instead.
|
||||||
|
for _, d := range c.Dns {
|
||||||
|
if net.ParseIP(d) == nil {
|
||||||
|
problems = append(problems, where+": dns "+strconv.Quote(d)+" is not an address; "+
|
||||||
|
"the runtime's resolver flag takes only addresses")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.IP != "" {
|
||||||
|
if net.ParseIP(c.IP) == nil {
|
||||||
|
problems = append(problems, where+": ip "+strconv.Quote(c.IP)+" is not an address")
|
||||||
|
}
|
||||||
|
if c.Network == "" {
|
||||||
|
problems = append(problems, where+": an ip needs a network; the runtime refuses a "+
|
||||||
|
"static address anywhere but a user-defined one")
|
||||||
|
}
|
||||||
|
}
|
||||||
return append(problems, checkImage(where, c.Image)...)
|
return append(problems, checkImage(where, c.Image)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,3 +427,27 @@ func TestASecretTheContentNeverUsesIsRefused(t *testing.T) {
|
|||||||
t.Fatal("a secret the content never mentions was accepted")
|
t.Fatal("a secret the content never mentions was accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The runtime's resolver and address flags take only addresses; a name would be refused at
|
||||||
|
// create, after the old container was already gone. Refused on arrival instead — and an address
|
||||||
|
// without a user-defined network is refused for the same reason.
|
||||||
|
func TestAContainersResolverAndAddressAreAddressesOrRefused(t *testing.T) {
|
||||||
|
refused := func(body string) []string {
|
||||||
|
_, err := Parse([]byte(`{"declaration":1,"resources":[` + body + `]}`))
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{err.Error()}
|
||||||
|
}
|
||||||
|
base := `"id":"c","type":"container","name":"x",` +
|
||||||
|
`"image":"a@sha256:0000000000000000000000000000000000000000000000000000000000000000"`
|
||||||
|
if p := refused(`{` + base + `,"network":"m","dns":["resolver.local"]}`); len(p) == 0 {
|
||||||
|
t.Error("a resolver named by name was accepted; the runtime takes only addresses")
|
||||||
|
}
|
||||||
|
if p := refused(`{` + base + `,"ip":"192.168.203.7"}`); len(p) == 0 {
|
||||||
|
t.Error("a static address with no network was accepted; the runtime refuses it")
|
||||||
|
}
|
||||||
|
if p := refused(`{` + base + `,"network":"m","dns":["192.168.203.254"],"ip":"192.168.203.7"}`); len(p) != 0 {
|
||||||
|
t.Errorf("a well-formed resolver and address were refused: %v", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user