package host import ( "context" "encoding/json" "errors" "net/http" "testing" "github.com/carlosframework/platform/internal/release" ) // staticAddresses is a fixed AddressSource. type staticAddresses BoxAddresses func (a staticAddresses) BoxAddresses() (BoxAddresses, bool) { return BoxAddresses(a), true } // 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, 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 } func envOf(m map[string]string) func(string) string { return func(k string) string { return m[k] } } // The core source works with no provider adapter at all (§25). func TestEnvAddressesWithoutAProvider(t *testing.T) { 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 := addrs((envAddresses{getenv: envOf(nil)})); got != (BoxAddresses{}) { t.Fatalf("nothing set: %+v", got) } // CARLOS_PRIVATE_IP needs CARLOS_PRIVATE_CIDR beside it. 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 = 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) } } 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 := 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 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 = 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 := addrs((envAddresses{getenv: envOf(nil), provider: bad})); got.Private != "" { t.Fatalf("an out-of-network provider address: %+v", got) } } func TestIMDSAddressesReadsTheLeavesAndTheVPCNetwork(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": "172.31.0.0/16\n10.0.0.0/16", } s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) { if token != "tok" { t.Fatalf("token %q", token) } v, ok := leaves[path] if !ok { return "", http.StatusNotFound } return v, http.StatusOK }} 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 := 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 := addrs(s); got.Private != "" { t.Fatalf("no network: %+v", got) } // 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 := addrs(s); got.Public != "" { t.Fatalf("no public leaf: %+v", got) } noToken := imdsAddresses{token: func() string { return "" }, leaf: s.leaf} if got, ok := noToken.BoxAddresses(); ok || got != (BoxAddresses{}) { t.Fatalf("no token: %+v, ok %v; want unknown", got, ok) } } func readEdgesFile(t *testing.T, s release.Store, box string) edgesFile { t.Helper() body, _, err := s.Get(context.Background(), edgesFileKey(box)) if err != nil { t.Fatal(err) } var f edgesFile if err := json.Unmarshal(body, &f); err != nil { t.Fatal(err) } return f } // Region HA spec 2 §6 (A1-12): with region HA on, every publication reads // the address source afresh, and a blank read publishes no address. The // last published EIP is never held: it may be the region's, which the // load balancer has taken. func TestEdgesFilePublishesOnlyWhatTheSourceReports(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.5", PrivateCIDR: "10.0.0.0/16"}, }} claims.addrs = src var last []byte ident := edgesFile{Box: "annie", BoxID: "annie"} // The loop's own reading (the eip argument) is not what is published. if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "198.51.100.1", &last, false, claims); err != nil { t.Fatal(err) } if f := readEdgesFile(t, s, "annie"); f.EIP != "203.0.113.9" || f.PrivateIP != "10.0.1.5" || f.PrivateCIDR != "10.0.0.0/16" { t.Fatalf("first publication: %+v", f) } if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, false, claims); err != nil { t.Fatal(err) } if f := readEdgesFile(t, s, "annie"); f.EIP != "" || f.PrivateIP != "10.0.1.5" { t.Fatalf("after a blank public read: eip %q private %q, want no public address", f.EIP, f.PrivateIP) } if src.reads != 2 { t.Fatalf("source read %d times, want once per publication", src.reads) } } // With region HA off there is no claims cache, and the publisher is // main's: no private address, and a blank probe holds the last EIP. func TestEdgesFileWithRegionHAOffKeepsTheEIPHold(t *testing.T) { s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t) seedRoute(t, reg, "app.example.com") var last []byte ident := edgesFile{Box: "annie"} if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "203.0.113.9", &last, false, nil); err != nil { t.Fatal(err) } if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, nil); err != nil { t.Fatal(err) } if f := readEdgesFile(t, s, "annie"); f.EIP != "203.0.113.9" || f.PrivateIP != "" || f.PrivateCIDR != "" { 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) } } // 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) } }