carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package status
2
3import (
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.
14var 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.
21const 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.
37func 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.
43func 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