| 1 | package status |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "io" |
| 6 | "net/http" |
| 7 | "strings" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | // IMDSBase is the link-local instance metadata service every EC2 machine |
| 12 | // answers on. It is a var only so the tests can point the reader at an |
| 13 | // httptest server; nothing else reassigns it. |
| 14 | var IMDSBase = "http://169.254.169.254" |
| 15 | |
| 16 | // imdsTimeout bounds the whole two-request exchange. It is deliberately |
| 17 | // short: off-cloud the address is unroutable and the only honest answer is |
| 18 | // "no instance id", which must not cost the status loop its tick. The |
| 19 | // link-local service answers in single-digit milliseconds when it answers |
| 20 | // at all. |
| 21 | const imdsTimeout = 2 * time.Second |
| 22 | |
| 23 | // InstanceID is the id of the EC2 instance this process runs on, or "" for |
| 24 | // anything that is not one — a Pi on a desk, a laptop, a container, another |
| 25 | // cloud. |
| 26 | // |
| 27 | // It exists because the console's architecture page has to answer "which |
| 28 | // machine is this box?", and every other way of asking is a naming |
| 29 | // convention that can drift: the flagship's instances are tagged |
| 30 | // carlos-flagship-ie while its boxes call themselves annie-welding, so a |
| 31 | // Name-tag match found nothing at all. The box is the only thing that knows |
| 32 | // which machine it is without being told, and this is it asking. |
| 33 | // |
| 34 | // The empty string is a real answer, not a failure: it is what tells the |
| 35 | // console this is somebody else's hardware rather than a machine it looked |
| 36 | // for and missed. |
| 37 | func InstanceID() string { return instanceIDFrom(IMDSBase, imdsTimeout) } |
| 38 | |
| 39 | // instanceIDFrom speaks IMDSv2 — a PUT for a short-lived token, then a GET |
| 40 | // carrying it. IMDSv1 (a bare GET) is not attempted: every box this |
| 41 | // platform provisions has v2 enforced, and falling back would only make an |
| 42 | // unreachable service take twice as long to say nothing. |
| 43 | func instanceIDFrom(base string, timeout time.Duration) string { |
| 44 | ctx, cancel := context.WithTimeout(context.Background(), timeout) |
| 45 | defer cancel() |
| 46 | client := &http.Client{Timeout: timeout} |
| 47 | |
| 48 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, base+"/latest/api/token", nil) |
| 49 | if err != nil { |
| 50 | return "" |
| 51 | } |
| 52 | req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60") |
| 53 | resp, err := client.Do(req) |
| 54 | if err != nil { |
| 55 | return "" |
| 56 | } |
| 57 | token, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) |
| 58 | resp.Body.Close() |
| 59 | if err != nil || resp.StatusCode != http.StatusOK || len(token) == 0 { |
| 60 | return "" |
| 61 | } |
| 62 | |
| 63 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, base+"/latest/meta-data/instance-id", nil) |
| 64 | if err != nil { |
| 65 | return "" |
| 66 | } |
| 67 | req.Header.Set("X-aws-ec2-metadata-token", string(token)) |
| 68 | resp, err = client.Do(req) |
| 69 | if err != nil { |
| 70 | return "" |
| 71 | } |
| 72 | body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) |
| 73 | resp.Body.Close() |
| 74 | if err != nil || resp.StatusCode != http.StatusOK { |
| 75 | return "" |
| 76 | } |
| 77 | // Trimmed because a stray newline would make the console's join miss by |
| 78 | // one byte, which is the most annoying possible way for this to fail. |
| 79 | return strings.TrimSpace(string(body)) |
| 80 | } |
| 81 | |