carlos / platform Public

region HA 2a: an unknown metadata read leaves the published edges file standing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 2d38c8b3e40c16f92a56c987093a9e195b4daad4 parent 9952239
7 files changed, +271 −46
  • internal/host/awsaddr.go +39 −10
  • internal/host/boxaddress.go +13 −4
  • internal/host/boxaddress_test.go +181 −19
  • internal/host/edgesclaims.go +4 −2
  • internal/host/edgesfile.go +26 −7
  • internal/provision/plan14_root_reads_test.go +3 −1
  • internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv +5 −3
diff --git a/internal/host/awsaddr.go b/internal/host/awsaddr.go
index 812f53759..4a042fe6a 100644
--- a/internal/host/awsaddr.go
+++ b/internal/host/awsaddr.go
@@ -1,6 +1,7 @@
package host
import (
+ "net"
"net/http"
"strings"
"time"
@@ -27,31 +28,59 @@ func newIMDSAddresses() imdsAddresses {
}
}
-func (s imdsAddresses) BoxAddresses() BoxAddresses {
+func (s imdsAddresses) BoxAddresses() (BoxAddresses, bool) {
token := s.token()
if token == "" {
- return BoxAddresses{}
+ return BoxAddresses{}, false
}
+ // read is a leaf's answer: its value on 200, blank on 404 (IMDS says the
+ // instance has no such leaf: authoritative), and unknown on anything
+ // else, a failed or timed-out request included.
+ unknown := false
read := func(path string) string {
v, code := s.leaf(token, path)
- if code != http.StatusOK {
- return ""
+ switch code {
+ case http.StatusOK:
+ return v
+ case http.StatusNotFound:
+ default:
+ unknown = true
}
- return v
+ return ""
}
out := BoxAddresses{Public: read("/latest/meta-data/public-ipv4")}
ip := read("/latest/meta-data/local-ipv4")
mac := read("/latest/meta-data/mac")
- if ip == "" || mac == "" || strings.ContainsAny(mac, "/\\ ") {
- return out
+ if unknown {
+ return BoxAddresses{}, false
+ }
+ if ip == "" || mac == "" {
+ return out, true
+ }
+ if _, err := net.ParseMAC(mac); err != nil {
+ return out, true
}
blocks := strings.Fields(read("/latest/meta-data/network/interfaces/macs/" + mac + "/vpc-ipv4-cidr-blocks"))
+ if unknown {
+ return BoxAddresses{}, false
+ }
out.Private, out.PrivateCIDR = edgesfile.PrivateAddress(ip, blocks)
- return out
+ return out, true
}
// boxAddressSource is what the edges file publisher reads with region HA
-// on: host.env's settings, then IMDS on EC2.
+// on: host.env's settings, then IMDS on EC2. Whether this is an EC2 box is
+// decided once, here, when the loop arms (as readPlacementRegion is): a box
+// where IMDS does not answer a token request now reads host.env only, and
+// makes no metadata request on any later pass. An EC2 box whose IMDS fails
+// after this reports unknown on that pass instead.
func boxAddressSource(getenv func(string) string) AddressSource {
- return envAddresses{getenv: getenv, provider: newIMDSAddresses()}
+ return boxAddressSourceFrom(getenv, newIMDSAddresses())
+}
+
+func boxAddressSourceFrom(getenv func(string) string, imds imdsAddresses) AddressSource {
+ if imds.token() == "" {
+ return envAddresses{getenv: getenv}
+ }
+ return envAddresses{getenv: getenv, provider: imds}
}
diff --git a/internal/host/boxaddress.go b/internal/host/boxaddress.go
index 94ea6d6f4..2e605a5d1 100644
--- a/internal/host/boxaddress.go
+++ b/internal/host/boxaddress.go
@@ -25,8 +25,14 @@ type BoxAddresses struct {
// it reports, so an address the box no longer holds is never republished.
// Every source is root-configured: the cloud's metadata service, or
// host.env. None reads a file the agent can write.
+//
+// ok is false when the source could not tell: a metadata request that
+// failed, timed out or answered something other than "here it is" or "there
+// is none". Unknown is not blank. A caller that gets !ok writes nothing and
+// leaves what it published before standing; only an authoritative answer,
+// blank fields included, changes the published address.
type AddressSource interface {
- BoxAddresses() BoxAddresses
+ BoxAddresses() (BoxAddresses, bool)
}
// envAddresses is the provider-neutral source (§25), from host.env:
@@ -41,7 +47,7 @@ type envAddresses struct {
provider AddressSource
}
-func (s envAddresses) BoxAddresses() BoxAddresses {
+func (s envAddresses) BoxAddresses() (BoxAddresses, bool) {
ip := strings.TrimSpace(s.getenv("CARLOS_PRIVATE_IP"))
cidr := strings.TrimSpace(s.getenv("CARLOS_PRIVATE_CIDR"))
public := strings.TrimSpace(s.getenv("CARLOS_PUBLIC_IP"))
@@ -54,7 +60,10 @@ func (s envAddresses) BoxAddresses() BoxAddresses {
out.Public = ipv4Only(public)
}
if (!privateSet || !publicSet) && s.provider != nil {
- p := s.provider.BoxAddresses()
+ p, ok := s.provider.BoxAddresses()
+ if !ok {
+ return BoxAddresses{}, false
+ }
if !privateSet {
out.Private, out.PrivateCIDR = edgesfile.PrivateAddress(p.Private, []string{p.PrivateCIDR})
}
@@ -62,7 +71,7 @@ func (s envAddresses) BoxAddresses() BoxAddresses {
out.Public = ipv4Only(p.Public)
}
}
- return out
+ return out, true
}
// ipv4Only is s when it is an IPv4 address, in its canonical form, and ""
diff --git a/internal/host/boxaddress_test.go b/internal/host/boxaddress_test.go
index 8a36599f2..8328e3e0e 100644
--- a/internal/host/boxaddress_test.go
+++ b/internal/host/boxaddress_test.go
@@ -3,6 +3,7 @@ package host
import (
"context"
"encoding/json"
+ "errors"
"net/http"
"testing"
@@ -12,17 +13,31 @@ import (
// staticAddresses is a fixed AddressSource.
type staticAddresses BoxAddresses
-func (a staticAddresses) BoxAddresses() BoxAddresses { return BoxAddresses(a) }
+func (a staticAddresses) BoxAddresses() (BoxAddresses, bool) { return BoxAddresses(a), true }
-// countingAddresses counts reads, answering each from its list in turn.
+// countingAddresses counts reads, answering each from its list in turn. A
+// read whose index is in unknown reports that the source could not tell.
type countingAddresses struct {
answers []BoxAddresses
+ unknown map[int]bool
reads int
}
-func (c *countingAddresses) BoxAddresses() BoxAddresses {
- a := c.answers[min(c.reads, len(c.answers)-1)]
+func (c *countingAddresses) BoxAddresses() (BoxAddresses, bool) {
+ i := c.reads
c.reads++
+ if c.unknown[i] {
+ return BoxAddresses{}, false
+ }
+ return c.answers[min(i, len(c.answers)-1)], true
+}
+
+// addrs is the answer of a source that is known to have answered.
+func addrs(s AddressSource) BoxAddresses {
+ a, ok := s.BoxAddresses()
+ if !ok {
+ panic("address source did not answer")
+ }
return a
}
@@ -30,18 +45,18 @@ func envOf(m map[string]string) func(string) string { return func(k string) stri
// The core source works with no provider adapter at all (§25).
func TestEnvAddressesWithoutAProvider(t *testing.T) {
- got := envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": " 10.0.1.5 ", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"})}.BoxAddresses()
+ got := addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": " 10.0.1.5 ", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"})})
if got != (BoxAddresses{Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "198.51.100.7"}) {
t.Fatalf("got %+v", got)
}
- if got := (envAddresses{getenv: envOf(nil)}).BoxAddresses(); got != (BoxAddresses{}) {
+ if got := addrs((envAddresses{getenv: envOf(nil)})); got != (BoxAddresses{}) {
t.Fatalf("nothing set: %+v", got)
}
// CARLOS_PRIVATE_IP needs CARLOS_PRIVATE_CIDR beside it.
- if got := (envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5"})}).BoxAddresses(); got.Private != "" {
+ if got := addrs((envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5"})})); got.Private != "" {
t.Fatalf("a private address with no network: %+v", got)
}
- got = envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "169.254.169.254", "CARLOS_PRIVATE_CIDR": "169.254.0.0/16", "CARLOS_PUBLIC_IP": "box.example.com"})}.BoxAddresses()
+ got = addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "169.254.169.254", "CARLOS_PRIVATE_CIDR": "169.254.0.0/16", "CARLOS_PUBLIC_IP": "box.example.com"})})
if got != (BoxAddresses{}) {
t.Fatalf("invalid settings: %+v", got)
}
@@ -49,25 +64,25 @@ func TestEnvAddressesWithoutAProvider(t *testing.T) {
func TestEnvAddressesOverrideTheProviderFieldByField(t *testing.T) {
provider := &countingAddresses{answers: []BoxAddresses{{Private: "10.0.9.9", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"}}}
- got := envAddresses{getenv: envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider}.BoxAddresses()
+ got := addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider})
if got != (BoxAddresses{Private: "10.0.9.9", PrivateCIDR: "10.0.0.0/16", Public: "198.51.100.7"}) {
t.Fatalf("got %+v", got)
}
// Both set: the provider is not asked.
provider.reads = 0
- envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider}.BoxAddresses()
+ addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider})
if provider.reads != 0 {
t.Fatalf("provider asked %d times with both settings present", provider.reads)
}
// A bad private setting publishes no private address; the provider's
// answer does not replace it.
- got = envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "not-an-ip", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16"}), provider: provider}.BoxAddresses()
+ got = addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "not-an-ip", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16"}), provider: provider})
if got != (BoxAddresses{Public: "203.0.113.9"}) {
t.Fatalf("bad setting: %+v", got)
}
// The provider's answer is validated too.
bad := &countingAddresses{answers: []BoxAddresses{{Private: "10.9.9.9", PrivateCIDR: "10.0.0.0/16"}}}
- if got := (envAddresses{getenv: envOf(nil), provider: bad}).BoxAddresses(); got.Private != "" {
+ if got := addrs((envAddresses{getenv: envOf(nil), provider: bad})); got.Private != "" {
t.Fatalf("an out-of-network provider address: %+v", got)
}
}
@@ -89,28 +104,30 @@ func TestIMDSAddressesReadsTheLeavesAndTheVPCNetwork(t *testing.T) {
}
return v, http.StatusOK
}}
- if got := s.BoxAddresses(); got != (BoxAddresses{Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.4"}) {
+ if got := addrs(s); got != (BoxAddresses{Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.4"}) {
t.Fatalf("got %+v", got)
}
// An address outside the VPC's blocks is not published.
leaves["/latest/meta-data/local-ipv4"] = "10.9.1.5"
- if got := s.BoxAddresses(); got.Private != "" || got.PrivateCIDR != "" {
+ if got := addrs(s); got.Private != "" || got.PrivateCIDR != "" {
t.Fatalf("out of network: %+v", got)
}
// No VPC network answer: no private address.
leaves["/latest/meta-data/local-ipv4"] = "10.0.1.5"
delete(leaves, "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks")
- if got := s.BoxAddresses(); got.Private != "" {
+ if got := addrs(s); got.Private != "" {
t.Fatalf("no network: %+v", got)
}
- // No public address (a box without one, or the EIP gone): blank.
+ // A 404 on public-ipv4 is IMDS saying this instance holds no public
+ // address (the EIP gone to the load balancer, A1-12): an authoritative
+ // blank, published as none.
delete(leaves, "/latest/meta-data/public-ipv4")
- if got := s.BoxAddresses(); got.Public != "" {
+ if got := addrs(s); got.Public != "" {
t.Fatalf("no public leaf: %+v", got)
}
noToken := imdsAddresses{token: func() string { return "" }, leaf: s.leaf}
- if got := noToken.BoxAddresses(); got != (BoxAddresses{}) {
- t.Fatalf("no token: %+v", got)
+ if got, ok := noToken.BoxAddresses(); ok || got != (BoxAddresses{}) {
+ t.Fatalf("no token: %+v, ok %v; want unknown", got, ok)
}
}
@@ -177,3 +194,148 @@ func TestEdgesFileWithRegionHAOffKeepsTheEIPHold(t *testing.T) {
t.Fatalf("%+v", f)
}
}
+
+// Anything but a 200 or a 404 from a leaf, and a failed token, is unknown,
+// not blank (the census-fix ruling): a hiccup must not read as "no address".
+func TestIMDSAddressesUnknownIsNotBlank(t *testing.T) {
+ leaves := map[string]string{
+ "/latest/meta-data/local-ipv4": "10.0.1.5",
+ "/latest/meta-data/public-ipv4": "203.0.113.4",
+ "/latest/meta-data/mac": "0e:12:34:56:78:9a",
+ "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks": "10.0.0.0/16",
+ }
+ for _, bad := range []struct {
+ name, path string
+ code int
+ }{
+ {"public-ipv4 times out", "/latest/meta-data/public-ipv4", 0},
+ {"public-ipv4 5xx", "/latest/meta-data/public-ipv4", http.StatusServiceUnavailable},
+ {"local-ipv4 401", "/latest/meta-data/local-ipv4", http.StatusUnauthorized},
+ {"mac fails", "/latest/meta-data/mac", 0},
+ {"vpc blocks 500", "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks", http.StatusInternalServerError},
+ } {
+ t.Run(bad.name, func(t *testing.T) {
+ s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) {
+ if path == bad.path {
+ return "", bad.code
+ }
+ return leaves[path], http.StatusOK
+ }}
+ if got, ok := s.BoxAddresses(); ok || got != (BoxAddresses{}) {
+ t.Fatalf("got %+v, ok %v; want unknown", got, ok)
+ }
+ })
+ }
+ // A malformed mac never reaches a request path.
+ s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) {
+ if path == "/latest/meta-data/mac" {
+ return "../../x", http.StatusOK
+ }
+ return leaves[path], http.StatusOK
+ }}
+ if got, ok := s.BoxAddresses(); !ok || got.Private != "" || got.Public != "203.0.113.4" {
+ t.Fatalf("bad mac: %+v, ok %v", got, ok)
+ }
+}
+
+// An unknown provider read is unknown to the whole source, unless host.env
+// answered every field.
+func TestEnvAddressesPassUnknownThrough(t *testing.T) {
+ provider := &countingAddresses{unknown: map[int]bool{0: true, 1: true}}
+ if got, ok := (envAddresses{getenv: envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider}).BoxAddresses(); ok || got != (BoxAddresses{}) {
+ t.Fatalf("got %+v, ok %v; want unknown", got, ok)
+ }
+ full := map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"}
+ if _, ok := (envAddresses{getenv: envOf(full), provider: provider}).BoxAddresses(); !ok {
+ t.Fatal("host.env answered every field; the provider was not needed")
+ }
+}
+
+// CARLOS_PRIVATE_CIDR alone is not an address.
+func TestEnvAddressesCIDRWithoutIP(t *testing.T) {
+ got := addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"})})
+ if got != (BoxAddresses{Public: "198.51.100.7"}) {
+ t.Fatalf("got %+v", got)
+ }
+}
+
+// IMDS is decided once, when the loop arms. A box where it does not answer
+// reads host.env only and makes no metadata request on any later pass.
+func TestBoxAddressSourceWithNoIMDSAtArmNeverQueriesIt(t *testing.T) {
+ tokens, leaves := 0, 0
+ imds := imdsAddresses{
+ token: func() string { tokens++; return "" },
+ leaf: func(token, path string) (string, int) { leaves++; return "10.0.1.5", http.StatusOK },
+ }
+ src := boxAddressSourceFrom(envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), imds)
+ for range 3 {
+ if got := addrs(src); got != (BoxAddresses{Public: "198.51.100.7"}) {
+ t.Fatalf("got %+v", got)
+ }
+ }
+ if tokens != 1 || leaves != 0 {
+ t.Fatalf("token requests %d (want the one at arm), leaf requests %d (want 0)", tokens, leaves)
+ }
+ // And a box where it does answer asks on every pass.
+ tokens = 0
+ imds.token = func() string { tokens++; return "tok" }
+ src = boxAddressSourceFrom(envOf(nil), imds)
+ addrs(src)
+ addrs(src)
+ if tokens != 3 {
+ t.Fatalf("token requests %d, want 1 at arm and 1 per pass", tokens)
+ }
+}
+
+// An unknown read (a metadata hiccup) writes nothing: the previous file
+// stands, the pass reports an error so the loop retries, and a later good
+// read publishes again.
+func TestEdgesFileUnknownAddressesLeaveThePublishedFileStanding(t *testing.T) {
+ s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
+ seedRoute(t, reg, "app.example.com")
+ claims := newEdgesClaims("annie", (&claimsBook{}).read)
+ src := &countingAddresses{
+ answers: []BoxAddresses{
+ {Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"},
+ {},
+ {Private: "10.0.1.6", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"},
+ },
+ unknown: map[int]bool{1: true},
+ }
+ claims.addrs = src
+ var last []byte
+ ident := edgesFile{Box: "annie", BoxID: "annie"}
+ if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, false, claims); err != nil {
+ t.Fatal(err)
+ }
+ before, _, err := s.Get(context.Background(), edgesFileKey("annie"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ lastBefore := string(last)
+ // A forced timed pass, which would rewrite an unchanged body.
+ err = edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, claims)
+ if !errors.Is(err, errBoxAddressesUnknown) {
+ t.Fatalf("unknown read: err %v, want errBoxAddressesUnknown", err)
+ }
+ after, _, err := s.Get(context.Background(), edgesFileKey("annie"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(after) != string(before) || string(last) != lastBefore {
+ t.Fatalf("an unknown read changed the published file:\nbefore %s\nafter %s", before, after)
+ }
+ if f := readEdgesFile(t, s, "annie"); f.EIP != "203.0.113.9" || f.PrivateIP != "10.0.1.5" {
+ t.Fatalf("published file lost its addresses: %+v", f)
+ }
+ // The retry reads again and publishes what it now reports.
+ if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, claims); err != nil {
+ t.Fatal(err)
+ }
+ if f := readEdgesFile(t, s, "annie"); f.PrivateIP != "10.0.1.6" {
+ t.Fatalf("after the retry: %+v", f)
+ }
+ if src.reads != 3 {
+ t.Fatalf("source read %d times, want once per attempt", src.reads)
+ }
+}
diff --git a/internal/host/edgesclaims.go b/internal/host/edgesclaims.go
index 96749ffdc..9fac525e4 100644
--- a/internal/host/edgesclaims.go
+++ b/internal/host/edgesclaims.go
@@ -74,13 +74,15 @@ type edgesClaims struct {
failingRun int // consecutive passes with a failed claim read
// addrs is the box's own addresses, read before every publication
// (region HA spec 2 §6). It rides here because this cache exists
- // exactly when region HA is on.
+ // exactly when region HA is on. It starts as host.env alone, so
+ // building the cache makes no metadata request; the loop sets
+ // boxAddressSource when it arms.
addrs AddressSource
}
func newEdgesClaims(self string, read func(context.Context, string) (hostclaim.Observed, error)) *edgesClaims {
return &edgesClaims{self: self, read: read, now: time.Now, entries: map[string]edgesClaimEntry{},
- passDeadline: edgesClaimPassDeadline, logf: log.Printf, addrs: boxAddressSource(os.Getenv)}
+ passDeadline: edgesClaimPassDeadline, logf: log.Printf, addrs: envAddresses{getenv: os.Getenv}}
}
// relocatableRow is spec 1 §2's relocatable host as a registry row shows
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index c188a054a..384ccbe95 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"log"
"math/rand/v2"
@@ -380,6 +381,10 @@ func edgesFileWrite(ctx context.Context, s release.Store, reg *registry.Registry
return edgesFileWriteClaims(ctx, s, reg, ident, eip, last, refresh, nil)
}
+// errBoxAddressesUnknown fails an edges file pass whose address source could
+// not tell what this box holds (region HA on). Nothing has been written.
+var errBoxAddressesUnknown = errors.New("edges file: the box's addresses could not be read")
+
// edgesFileWriteClaims is edgesFileWrite with the claim reader region HA
// adds (edgesclaims.go). claims is nil unless CARLOS_REGION_HA is on, and
// nil publishes exactly what edgesFileWrite always has.
@@ -601,10 +606,17 @@ func edgesFileWriteClaims(ctx context.Context, s release.Store, reg *registry.Re
var private BoxAddresses
if claims != nil && claims.addrs != nil {
// Region HA spec 2 §6 (A1-12): publish only what the box's address
- // source reports now. A blank read publishes no address, never the
- // last one: that may be the region's EIP, which the load balancer
- // has since taken.
- private = claims.addrs.BoxAddresses()
+ // source reports now. An authoritative blank publishes no address,
+ // never the last one: that may be the region's EIP, which the load
+ // balancer has since taken. An unknown read (a metadata hiccup)
+ // writes nothing: the previous file stands and the loop retries,
+ // so one failed request does not hide a healthy box from its
+ // readers.
+ read, ok := claims.addrs.BoxAddresses()
+ if !ok {
+ return errBoxAddressesUnknown
+ }
+ private = read
eip = private.Public
} else if eip == "" && *last != nil {
var prior edgesFile
@@ -823,8 +835,15 @@ func edgesFileLoop(reg *registry.Registry) {
fpErrs, passErrs := 0, 0
// The EIP cannot change under a running box, so it is read once here and
// re-read only on the timer's passes, never on every change pass: off
- // EC2 each read can block for the probe's whole timeout.
- eip := readPublicIP()
+ // EC2 each read can block for the probe's whole timeout. With claims
+ // (region HA on) the address source supplies it afresh on every
+ // publication and this reading is never used, so it is not taken.
+ eip := ""
+ if claims == nil {
+ eip = readPublicIP()
+ } else {
+ claims.addrs = boxAddressSource(os.Getenv)
+ }
for now := range tick.C {
fp, err := reg.Fingerprint()
if err != nil {
@@ -840,7 +859,7 @@ func edgesFileLoop(reg *registry.Registry) {
if !state.due(now, fp, err == nil) {
continue
}
- if state.timed() {
+ if claims == nil && state.timed() {
eip = readPublicIP()
}
ident.Headroom = headroomFor(ident.Capabilities, dataDir, reg)
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 4c5bc4c89..24e963023 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -797,7 +797,9 @@ func TestPlan14RootReadCensus(t *testing.T) {
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
- seal = "5d1b9b6addf6c9964ed18b417ee5b3d0d49fd43e1acabb775e054bba4b6e314e"
+ // Task 7's fix round re-digests edgesFileLoop at its accepted disposition
+ // (class, task, basis and read sites unchanged), so the seal moves.
+ seal = "ede89d37d408863fd01a395afe791d587cbafac40e9b5db03d5b7089e6c4787a"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv
index 0088c44b0..d6e4160ad 100644
--- a/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv
+++ b/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv
@@ -4,11 +4,13 @@
# Task 4: the placement policy (internal/boxchoice policy.go; headroom totals and the edges file's generation carried through Eligible and edgesfile.Snapshot.BoxChoice). Envelope-only: no root-read unit changed or was added, and the legacy seal stands.
# Task 5: the edges file's headroom carries the box's total memory and data volume size (headroomFor and capacityMiB, internal/host/headroom.go; edgeHeadroom's two additive fields). headroomFor calls the existing readMemBytes and diskBytes and adds no read of its own. Envelope-only: readHeadroom and readMemBytes are unchanged, no root-read unit changed or was added, and the legacy seal stands.
# Task 6: hostops.ExecutorChooser applies spec 2's policy in Choose and Permits (creates and relocation targets) and names the placeable set in PlaceableBoxes and Capacity. ExecutorChooser.eligible, the accepted-legacy unit (merge ruling 2), is unchanged: the policy runs over its result. Envelope-only, and the legacy seal stands.
-# Task 7: the box's own addresses (internal/host boxaddress.go, the provider-neutral source over CARLOS_PRIVATE_IP with CARLOS_PRIVATE_CIDR, and CARLOS_PUBLIC_IP, all from root-owned host.env; awsaddr.go, the IMDS adapter, which reads the VPC's CIDR blocks too) and the edges file's private_ip and private_cidr, validated before they are written by edgesfile.PrivateAddress (census ruling region-ha-2a, condition 1). edgesFileWriteClaims (authority, F,J1) is re-digested with its class, task, basis and sites unchanged: with region HA on it reads the address source before every publication and no longer holds a blank EIP. It adds no registry, /data, release or claim read, and nothing runs as root that did not before; the legacy seal stands.
+# Task 7: the box's own addresses (internal/host boxaddress.go, the provider-neutral source over CARLOS_PRIVATE_IP with CARLOS_PRIVATE_CIDR, and CARLOS_PUBLIC_IP, all from root-owned host.env; awsaddr.go, the IMDS adapter, which reads the VPC's CIDR blocks too) and the edges file's private_ip and private_cidr, validated before they are written by edgesfile.PrivateAddress (census ruling region-ha-2a, condition 1). edgesFileWriteClaims (authority, F,J1) is re-digested with its class, task, basis and sites unchanged: with region HA on it reads the address source before every publication and no longer holds a blank EIP. It adds no registry, /data, release or claim read, and nothing runs as root that did not before. Fix round 1: edgesFileLoop (accepted-legacy, unchanged class, task, basis and kinds; digest and legacy fingerprint re-pinned, so the legacy seal moves) arms the address source once and no longer reads the public address per pass when region HA is on; the source reports unknown separately from an authoritative blank, and an unknown read fails the pass without writing.
@coverage internal/lbhealth ad1211a01a4eb3d02cd8ed95cbff0f8b17b3976ce9c0279ebd2cae2f20c07124
@coverage internal/hostactors 6736698380a247764ada687f2476f89382fc32a1183a50c06afe75967d716eab
@coverage internal/boxchoice 1bfd561bcb48b51e60e63f99e086fd44d3b1be2160e28b3ed0253a14bbb9ff66
@coverage internal/edgesfile f62c3973bb48fa36f561d26f496c0ba9a14fe882fe5843867e9f7a9eaf0abbdf
-@coverage internal/host 38011bf89a668507ccd9bfac2430c431dd4b3d9ca6ad4ebfe88efc7be41b453b
+@coverage internal/host 3fedcb518fb123d97a8d90775f1303048768c5f32756fab62575a325b8d19602
@coverage internal/hostops 3dc23aaff7bdae0341a8c9a6fbac076a2f57ca1e4676d83543433ff9ad7d1c79
-internal/host/edgesfile.go edgesFileWriteClaims authority F,J1 root-route;kept=F;scope=protected-decision-only registry:Account@10835,registry:Alias@5297,registry:Alias@5332,registry:Alias@5754,registry:Alias@5911,registry:Alias@5981,registry:Alias@6013,registry:Alias@6482,registry:Alias@8772,registry:App@10847,registry:DNSStatus@5429,registry:DNSStatus@5920,registry:Host@10480,registry:Host@2878,registry:Host@5389,registry:Host@6234,registry:Host@6567,registry:Host@8819,registry:Host@8866,registry:Host@8916,registry:Host@8951,registry:Host@9000,registry:Platform@2921,registry:Provisioned@2935,registry:Replica@2959,registry:Status@877,whole:Route@3212,whole:Route@323,whole:Route@3505,whole:Route@351,whole:RouteAlias@5282,whole:RouteAlias@8725,whole:RoutePath@3520,whole:WildcardDomain@8905 48bad9a5f49c5fca53fcf7bd6a241576fd2cd332974114e463b2cb3832e3598d
+internal/host/edgesfile.go edgesFileWriteClaims authority F,J1 root-route;kept=F;scope=protected-decision-only registry:Account@10835,registry:Alias@5297,registry:Alias@5332,registry:Alias@5754,registry:Alias@5911,registry:Alias@5981,registry:Alias@6013,registry:Alias@6482,registry:Alias@8772,registry:App@10847,registry:DNSStatus@5429,registry:DNSStatus@5920,registry:Host@10480,registry:Host@2878,registry:Host@5389,registry:Host@6234,registry:Host@6567,registry:Host@8819,registry:Host@8866,registry:Host@8916,registry:Host@8951,registry:Host@9000,registry:Platform@2921,registry:Provisioned@2935,registry:Replica@2959,registry:Status@877,whole:Route@3212,whole:Route@323,whole:Route@3505,whole:Route@351,whole:RouteAlias@5282,whole:RouteAlias@8725,whole:RoutePath@3520,whole:WildcardDomain@8905 8c7cdffba66886b92f9a2ac663a86dbdd5f80699afd2e6afb4571da916c727ad
+@legacy 8cd94c1313fbc3c08923f74a4e9f23528e953ddeb81b77699c195fd0751b2db0 internal/host/edgesfile.go edgesFileLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with the box's seeds fields added to the edges file it publishes (withSeedsFields); a report, not a decision file:Read@2587 02247880844913b821a4cad9d024ee164bd705e3197b8a2fa6b76b02fb6253d4
+internal/host/edgesfile.go edgesFileLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with the box's seeds fields added to the edges file it publishes (withSeedsFields); a report, not a decision file:Read@2587 02247880844913b821a4cad9d024ee164bd705e3197b8a2fa6b76b02fb6253d4