| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "net/http" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | // staticAddresses is a fixed AddressSource. |
| 14 | type staticAddresses BoxAddresses |
| 15 | |
| 16 | func (a staticAddresses) BoxAddresses() (BoxAddresses, bool) { return BoxAddresses(a), true } |
| 17 | |
| 18 | // countingAddresses counts reads, answering each from its list in turn. A |
| 19 | // read whose index is in unknown reports that the source could not tell. |
| 20 | type countingAddresses struct { |
| 21 | answers []BoxAddresses |
| 22 | unknown map[int]bool |
| 23 | reads int |
| 24 | } |
| 25 | |
| 26 | func (c *countingAddresses) BoxAddresses() (BoxAddresses, bool) { |
| 27 | i := c.reads |
| 28 | c.reads++ |
| 29 | if c.unknown[i] { |
| 30 | return BoxAddresses{}, false |
| 31 | } |
| 32 | return c.answers[min(i, len(c.answers)-1)], true |
| 33 | } |
| 34 | |
| 35 | // addrs is the answer of a source that is known to have answered. |
| 36 | func addrs(s AddressSource) BoxAddresses { |
| 37 | a, ok := s.BoxAddresses() |
| 38 | if !ok { |
| 39 | panic("address source did not answer") |
| 40 | } |
| 41 | return a |
| 42 | } |
| 43 | |
| 44 | func envOf(m map[string]string) func(string) string { return func(k string) string { return m[k] } } |
| 45 | |
| 46 | // The core source works with no provider adapter at all (§25). |
| 47 | func TestEnvAddressesWithoutAProvider(t *testing.T) { |
| 48 | 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"})}) |
| 49 | if got != (BoxAddresses{Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "198.51.100.7"}) { |
| 50 | t.Fatalf("got %+v", got) |
| 51 | } |
| 52 | if got := addrs((envAddresses{getenv: envOf(nil)})); got != (BoxAddresses{}) { |
| 53 | t.Fatalf("nothing set: %+v", got) |
| 54 | } |
| 55 | // CARLOS_PRIVATE_IP needs CARLOS_PRIVATE_CIDR beside it. |
| 56 | if got := addrs((envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "10.0.1.5"})})); got.Private != "" { |
| 57 | t.Fatalf("a private address with no network: %+v", got) |
| 58 | } |
| 59 | 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"})}) |
| 60 | if got != (BoxAddresses{}) { |
| 61 | t.Fatalf("invalid settings: %+v", got) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestEnvAddressesOverrideTheProviderFieldByField(t *testing.T) { |
| 66 | provider := &countingAddresses{answers: []BoxAddresses{{Private: "10.0.9.9", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"}}} |
| 67 | got := addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider}) |
| 68 | if got != (BoxAddresses{Private: "10.0.9.9", PrivateCIDR: "10.0.0.0/16", Public: "198.51.100.7"}) { |
| 69 | t.Fatalf("got %+v", got) |
| 70 | } |
| 71 | // Both set: the provider is not asked. |
| 72 | provider.reads = 0 |
| 73 | 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}) |
| 74 | if provider.reads != 0 { |
| 75 | t.Fatalf("provider asked %d times with both settings present", provider.reads) |
| 76 | } |
| 77 | // A bad private setting publishes no private address; the provider's |
| 78 | // answer does not replace it. |
| 79 | got = addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_IP": "not-an-ip", "CARLOS_PRIVATE_CIDR": "10.0.0.0/16"}), provider: provider}) |
| 80 | if got != (BoxAddresses{Public: "203.0.113.9"}) { |
| 81 | t.Fatalf("bad setting: %+v", got) |
| 82 | } |
| 83 | // The provider's answer is validated too. |
| 84 | bad := &countingAddresses{answers: []BoxAddresses{{Private: "10.9.9.9", PrivateCIDR: "10.0.0.0/16"}}} |
| 85 | if got := addrs((envAddresses{getenv: envOf(nil), provider: bad})); got.Private != "" { |
| 86 | t.Fatalf("an out-of-network provider address: %+v", got) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func TestIMDSAddressesReadsTheLeavesAndTheVPCNetwork(t *testing.T) { |
| 91 | leaves := map[string]string{ |
| 92 | "/latest/meta-data/local-ipv4": "10.0.1.5", |
| 93 | "/latest/meta-data/public-ipv4": "203.0.113.4", |
| 94 | "/latest/meta-data/mac": "0e:12:34:56:78:9a", |
| 95 | "/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", |
| 96 | } |
| 97 | s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) { |
| 98 | if token != "tok" { |
| 99 | t.Fatalf("token %q", token) |
| 100 | } |
| 101 | v, ok := leaves[path] |
| 102 | if !ok { |
| 103 | return "", http.StatusNotFound |
| 104 | } |
| 105 | return v, http.StatusOK |
| 106 | }} |
| 107 | if got := addrs(s); got != (BoxAddresses{Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.4"}) { |
| 108 | t.Fatalf("got %+v", got) |
| 109 | } |
| 110 | // An address outside the VPC's blocks is not published. |
| 111 | leaves["/latest/meta-data/local-ipv4"] = "10.9.1.5" |
| 112 | if got := addrs(s); got.Private != "" || got.PrivateCIDR != "" { |
| 113 | t.Fatalf("out of network: %+v", got) |
| 114 | } |
| 115 | // No VPC network answer: no private address. |
| 116 | leaves["/latest/meta-data/local-ipv4"] = "10.0.1.5" |
| 117 | delete(leaves, "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks") |
| 118 | if got := addrs(s); got.Private != "" { |
| 119 | t.Fatalf("no network: %+v", got) |
| 120 | } |
| 121 | // A 404 on public-ipv4 is IMDS saying this instance holds no public |
| 122 | // address (the EIP gone to the load balancer, A1-12): an authoritative |
| 123 | // blank, published as none. |
| 124 | delete(leaves, "/latest/meta-data/public-ipv4") |
| 125 | if got := addrs(s); got.Public != "" { |
| 126 | t.Fatalf("no public leaf: %+v", got) |
| 127 | } |
| 128 | noToken := imdsAddresses{token: func() string { return "" }, leaf: s.leaf} |
| 129 | if got, ok := noToken.BoxAddresses(); ok || got != (BoxAddresses{}) { |
| 130 | t.Fatalf("no token: %+v, ok %v; want unknown", got, ok) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | func readEdgesFile(t *testing.T, s release.Store, box string) edgesFile { |
| 135 | t.Helper() |
| 136 | body, _, err := s.Get(context.Background(), edgesFileKey(box)) |
| 137 | if err != nil { |
| 138 | t.Fatal(err) |
| 139 | } |
| 140 | var f edgesFile |
| 141 | if err := json.Unmarshal(body, &f); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | return f |
| 145 | } |
| 146 | |
| 147 | // Region HA spec 2 §6 (A1-12): with region HA on, every publication reads |
| 148 | // the address source afresh, and a blank read publishes no address. The |
| 149 | // last published EIP is never held: it may be the region's, which the |
| 150 | // load balancer has taken. |
| 151 | func TestEdgesFilePublishesOnlyWhatTheSourceReports(t *testing.T) { |
| 152 | s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t) |
| 153 | seedRoute(t, reg, "app.example.com") |
| 154 | claims := newEdgesClaims("annie", (&claimsBook{}).read) |
| 155 | src := &countingAddresses{answers: []BoxAddresses{ |
| 156 | {Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"}, |
| 157 | {Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16"}, |
| 158 | }} |
| 159 | claims.addrs = src |
| 160 | var last []byte |
| 161 | ident := edgesFile{Box: "annie", BoxID: "annie"} |
| 162 | // The loop's own reading (the eip argument) is not what is published. |
| 163 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "198.51.100.1", &last, false, claims); err != nil { |
| 164 | t.Fatal(err) |
| 165 | } |
| 166 | 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" { |
| 167 | t.Fatalf("first publication: %+v", f) |
| 168 | } |
| 169 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, false, claims); err != nil { |
| 170 | t.Fatal(err) |
| 171 | } |
| 172 | if f := readEdgesFile(t, s, "annie"); f.EIP != "" || f.PrivateIP != "10.0.1.5" { |
| 173 | t.Fatalf("after a blank public read: eip %q private %q, want no public address", f.EIP, f.PrivateIP) |
| 174 | } |
| 175 | if src.reads != 2 { |
| 176 | t.Fatalf("source read %d times, want once per publication", src.reads) |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | // With region HA off there is no claims cache, and the publisher is |
| 181 | // main's: no private address, and a blank probe holds the last EIP. |
| 182 | func TestEdgesFileWithRegionHAOffKeepsTheEIPHold(t *testing.T) { |
| 183 | s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t) |
| 184 | seedRoute(t, reg, "app.example.com") |
| 185 | var last []byte |
| 186 | ident := edgesFile{Box: "annie"} |
| 187 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "203.0.113.9", &last, false, nil); err != nil { |
| 188 | t.Fatal(err) |
| 189 | } |
| 190 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, nil); err != nil { |
| 191 | t.Fatal(err) |
| 192 | } |
| 193 | if f := readEdgesFile(t, s, "annie"); f.EIP != "203.0.113.9" || f.PrivateIP != "" || f.PrivateCIDR != "" { |
| 194 | t.Fatalf("%+v", f) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // Anything but a 200 or a 404 from a leaf, and a failed token, is unknown, |
| 199 | // not blank (the census-fix ruling): a hiccup must not read as "no address". |
| 200 | func TestIMDSAddressesUnknownIsNotBlank(t *testing.T) { |
| 201 | leaves := map[string]string{ |
| 202 | "/latest/meta-data/local-ipv4": "10.0.1.5", |
| 203 | "/latest/meta-data/public-ipv4": "203.0.113.4", |
| 204 | "/latest/meta-data/mac": "0e:12:34:56:78:9a", |
| 205 | "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks": "10.0.0.0/16", |
| 206 | } |
| 207 | for _, bad := range []struct { |
| 208 | name, path string |
| 209 | code int |
| 210 | }{ |
| 211 | {"public-ipv4 times out", "/latest/meta-data/public-ipv4", 0}, |
| 212 | {"public-ipv4 5xx", "/latest/meta-data/public-ipv4", http.StatusServiceUnavailable}, |
| 213 | {"local-ipv4 401", "/latest/meta-data/local-ipv4", http.StatusUnauthorized}, |
| 214 | {"mac fails", "/latest/meta-data/mac", 0}, |
| 215 | {"vpc blocks 500", "/latest/meta-data/network/interfaces/macs/0e:12:34:56:78:9a/vpc-ipv4-cidr-blocks", http.StatusInternalServerError}, |
| 216 | } { |
| 217 | t.Run(bad.name, func(t *testing.T) { |
| 218 | s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) { |
| 219 | if path == bad.path { |
| 220 | return "", bad.code |
| 221 | } |
| 222 | return leaves[path], http.StatusOK |
| 223 | }} |
| 224 | if got, ok := s.BoxAddresses(); ok || got != (BoxAddresses{}) { |
| 225 | t.Fatalf("got %+v, ok %v; want unknown", got, ok) |
| 226 | } |
| 227 | }) |
| 228 | } |
| 229 | // A malformed mac never reaches a request path. |
| 230 | s := imdsAddresses{token: func() string { return "tok" }, leaf: func(token, path string) (string, int) { |
| 231 | if path == "/latest/meta-data/mac" { |
| 232 | return "../../x", http.StatusOK |
| 233 | } |
| 234 | return leaves[path], http.StatusOK |
| 235 | }} |
| 236 | if got, ok := s.BoxAddresses(); !ok || got.Private != "" || got.Public != "203.0.113.4" { |
| 237 | t.Fatalf("bad mac: %+v, ok %v", got, ok) |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // An unknown provider read is unknown to the whole source, unless host.env |
| 242 | // answered every field. |
| 243 | func TestEnvAddressesPassUnknownThrough(t *testing.T) { |
| 244 | provider := &countingAddresses{unknown: map[int]bool{0: true, 1: true}} |
| 245 | if got, ok := (envAddresses{getenv: envOf(map[string]string{"CARLOS_PUBLIC_IP": "198.51.100.7"}), provider: provider}).BoxAddresses(); ok || got != (BoxAddresses{}) { |
| 246 | t.Fatalf("got %+v, ok %v; want unknown", got, ok) |
| 247 | } |
| 248 | 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"} |
| 249 | if _, ok := (envAddresses{getenv: envOf(full), provider: provider}).BoxAddresses(); !ok { |
| 250 | t.Fatal("host.env answered every field; the provider was not needed") |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | // CARLOS_PRIVATE_CIDR alone is not an address. |
| 255 | func TestEnvAddressesCIDRWithoutIP(t *testing.T) { |
| 256 | got := addrs(envAddresses{getenv: envOf(map[string]string{"CARLOS_PRIVATE_CIDR": "10.0.0.0/16", "CARLOS_PUBLIC_IP": "198.51.100.7"})}) |
| 257 | if got != (BoxAddresses{Public: "198.51.100.7"}) { |
| 258 | t.Fatalf("got %+v", got) |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | // An unknown read (a metadata hiccup) writes nothing: the previous file |
| 263 | // stands, the pass reports an error so the loop retries, and a later good |
| 264 | // read publishes again. |
| 265 | func TestEdgesFileUnknownAddressesLeaveThePublishedFileStanding(t *testing.T) { |
| 266 | s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t) |
| 267 | seedRoute(t, reg, "app.example.com") |
| 268 | claims := newEdgesClaims("annie", (&claimsBook{}).read) |
| 269 | src := &countingAddresses{ |
| 270 | answers: []BoxAddresses{ |
| 271 | {Private: "10.0.1.5", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"}, |
| 272 | {}, |
| 273 | {Private: "10.0.1.6", PrivateCIDR: "10.0.0.0/16", Public: "203.0.113.9"}, |
| 274 | }, |
| 275 | unknown: map[int]bool{1: true}, |
| 276 | } |
| 277 | claims.addrs = src |
| 278 | var last []byte |
| 279 | ident := edgesFile{Box: "annie", BoxID: "annie"} |
| 280 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, false, claims); err != nil { |
| 281 | t.Fatal(err) |
| 282 | } |
| 283 | before, _, err := s.Get(context.Background(), edgesFileKey("annie")) |
| 284 | if err != nil { |
| 285 | t.Fatal(err) |
| 286 | } |
| 287 | lastBefore := string(last) |
| 288 | // A forced timed pass, which would rewrite an unchanged body. |
| 289 | err = edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, claims) |
| 290 | if !errors.Is(err, errBoxAddressesUnknown) { |
| 291 | t.Fatalf("unknown read: err %v, want errBoxAddressesUnknown", err) |
| 292 | } |
| 293 | after, _, err := s.Get(context.Background(), edgesFileKey("annie")) |
| 294 | if err != nil { |
| 295 | t.Fatal(err) |
| 296 | } |
| 297 | if string(after) != string(before) || string(last) != lastBefore { |
| 298 | t.Fatalf("an unknown read changed the published file:\nbefore %s\nafter %s", before, after) |
| 299 | } |
| 300 | if f := readEdgesFile(t, s, "annie"); f.EIP != "203.0.113.9" || f.PrivateIP != "10.0.1.5" { |
| 301 | t.Fatalf("published file lost its addresses: %+v", f) |
| 302 | } |
| 303 | // The retry reads again and publishes what it now reports. |
| 304 | if err := edgesFileWriteClaims(context.Background(), s, reg, ident, "", &last, true, claims); err != nil { |
| 305 | t.Fatal(err) |
| 306 | } |
| 307 | if f := readEdgesFile(t, s, "annie"); f.PrivateIP != "10.0.1.6" { |
| 308 | t.Fatalf("after the retry: %+v", f) |
| 309 | } |
| 310 | if src.reads != 3 { |
| 311 | t.Fatalf("source read %d times, want once per attempt", src.reads) |
| 312 | } |
| 313 | } |
| 314 | |