mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 03:11:46 +00:00
feat(imds): serve network/interfaces/macs/<mac> subtree (single-NIC) (#410)
This commit is contained in:
+1
-1
@@ -628,7 +628,7 @@ curl -i http://169.254.169.254/latest/meta-data/instance-id
|
||||
| `/latest/dynamic/instance-identity` | GET | Lists `document` (signed forms listed when the signing key lands) | **DONE** |
|
||||
| `/latest/dynamic/instance-identity/document` | GET | Unsigned identity document from resolved ENI + instance facts | **DONE** |
|
||||
| `/latest/dynamic/instance-identity/{signature,pkcs7,rsa2048}` | GET | Signed forms; need a per-cluster signing key | **NOT STARTED** (404; lands with EKS IRSA) |
|
||||
| `/latest/meta-data/network/interfaces/macs/<mac>/...` | GET | Multi-ENI rendering (subnet-id, vpc-id, ipv4s, security-group-ids, …) | **NOT STARTED** (404) |
|
||||
| `/latest/meta-data/network/interfaces/macs/<mac>/...` | GET | Primary ENI subtree: `mac`, `device-number`, `interface-id`, `owner-id`, `subnet-id`, `vpc-id`, `local-ipv4s`, `local-hostname`, `security-group-ids`, `security-groups`, `subnet-ipv4-cidr-block`, `vpc-ipv4-cidr-block(s)`, and `public-ipv4s`/`public-hostname` when an EIP is attached | **DONE** (single-NIC; multi-ENI deferred) |
|
||||
| `/latest/meta-data/tags/instance/<key>` | GET | Instance-tag metadata; gated on `InstanceMetadataTags` enablement | **NOT STARTED** (404) |
|
||||
| `/latest/meta-data/block-device-mapping/...` | GET | `ami`/`root`/`ebsN`/`ephemeralN` device map | **NOT STARTED** (404) |
|
||||
| `/latest/meta-data/placement/{group-name,partition-number,availability-zone-id,host-id}` | GET | Placement extras beyond `availability-zone`/`region` | **NOT STARTED** (404) |
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,6 +26,7 @@ const (
|
||||
pathSecurityCredsDir = "/latest/meta-data/iam/security-credentials" //nolint:gosec // URL path, not a credential
|
||||
prefixPublicKeys = "/latest/meta-data/public-keys/"
|
||||
pathPublicKeysDir = "/latest/meta-data/public-keys"
|
||||
prefixNetworkMacs = "/latest/meta-data/network/interfaces/macs/"
|
||||
|
||||
prefixDynamic = "/latest/dynamic"
|
||||
pathIdentityDir = "/latest/dynamic/instance-identity"
|
||||
@@ -148,6 +150,11 @@ func (s *IMDSServiceImpl) dispatch(w http.ResponseWriter, r *http.Request, eni *
|
||||
return
|
||||
}
|
||||
|
||||
if sub, ok := strings.CutPrefix(path, prefixNetworkMacs); ok {
|
||||
s.serveNetworkInterface(w, eni, sub) // sub: "", "<mac>", "<mac>/", "<mac>/<key>"
|
||||
return
|
||||
}
|
||||
|
||||
switch path {
|
||||
case "/":
|
||||
writeText(w, strings.Join(supportedVersions, "\n"))
|
||||
@@ -160,7 +167,7 @@ func (s *IMDSServiceImpl) dispatch(w http.ResponseWriter, r *http.Request, eni *
|
||||
case pathIdentityDocument:
|
||||
s.serveInstanceIdentityDocument(w, eni)
|
||||
case pathMetaDataRoot, prefixMetaData:
|
||||
writeText(w, "ami-id\nami-launch-index\nhostname\niam/\ninstance-id\ninstance-life-cycle\ninstance-type\nlocal-hostname\nlocal-ipv4\nmac\nplacement/\npublic-hostname\npublic-ipv4\npublic-keys/\nreservation-id\nsecurity-groups\nservices/")
|
||||
writeText(w, "ami-id\nami-launch-index\nhostname\niam/\ninstance-id\ninstance-life-cycle\ninstance-type\nlocal-hostname\nlocal-ipv4\nmac\nnetwork/\nplacement/\npublic-hostname\npublic-ipv4\npublic-keys/\nreservation-id\nsecurity-groups\nservices/")
|
||||
case prefixMetaData + "instance-id":
|
||||
writeText(w, eni.instanceID)
|
||||
case prefixMetaData + "instance-life-cycle":
|
||||
@@ -177,6 +184,12 @@ func (s *IMDSServiceImpl) dispatch(w http.ResponseWriter, r *http.Request, eni *
|
||||
writeText(w, eni.publicIP) // mirror public-ipv4 until public DNS exists
|
||||
case prefixMetaData + "mac":
|
||||
writeText(w, eni.mac)
|
||||
case prefixMetaData + "network", prefixMetaData + "network/":
|
||||
writeText(w, "interfaces/")
|
||||
case prefixMetaData + "network/interfaces", prefixMetaData + "network/interfaces/":
|
||||
writeText(w, "macs/")
|
||||
case prefixMetaData + "network/interfaces/macs":
|
||||
writeText(w, eni.mac+"/")
|
||||
case prefixMetaData + "security-groups":
|
||||
writeText(w, strings.Join(s.resolver.resolveSGNames(eni.accountID, eni.securityGroupIDs), "\n"))
|
||||
case prefixMetaData + "hostname", prefixMetaData + "local-hostname":
|
||||
@@ -388,6 +401,111 @@ func (s *IMDSServiceImpl) servePublicKeys(w http.ResponseWriter, eni *eniFacts,
|
||||
}
|
||||
}
|
||||
|
||||
// serveNetworkInterface serves the single primary ENI's subtree under
|
||||
// /network/interfaces/macs/. sub is the path after the macs/ prefix. An empty sub
|
||||
// is the macs/ directory listing; a MAC that is not the caller's is 404, since the
|
||||
// per-tap responder only ever resolves its own ENI (single-NIC; multi-ENI deferred).
|
||||
func (s *IMDSServiceImpl) serveNetworkInterface(w http.ResponseWriter, eni *eniFacts, sub string) {
|
||||
if sub == "" {
|
||||
writeText(w, eni.mac+"/")
|
||||
return
|
||||
}
|
||||
mac, key, _ := strings.Cut(sub, "/")
|
||||
if mac != eni.mac {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch key {
|
||||
case "": // "<mac>" or "<mac>/" — the per-interface key listing
|
||||
keys, err := s.macKeys(eni)
|
||||
if err != nil {
|
||||
slog.Error("IMDS: network-interface listing failed", "account_id", eni.accountID, "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeText(w, strings.Join(keys, "\n"))
|
||||
case "mac":
|
||||
writeText(w, eni.mac)
|
||||
case "device-number":
|
||||
writeText(w, "0") // primary ENI
|
||||
case "interface-id":
|
||||
writeText(w, eni.eniID)
|
||||
case "owner-id":
|
||||
writeText(w, eni.accountID)
|
||||
case "subnet-id":
|
||||
writeText(w, eni.subnetID)
|
||||
case "vpc-id":
|
||||
writeText(w, eni.vpcID)
|
||||
case "local-ipv4s":
|
||||
writeText(w, eni.privateIP)
|
||||
case "local-hostname":
|
||||
writeText(w, synthHostname(eni.privateIP, regionFromAZ(eni.availabilityZone)))
|
||||
case "security-group-ids":
|
||||
writeText(w, strings.Join(eni.securityGroupIDs, "\n"))
|
||||
case "security-groups":
|
||||
writeText(w, strings.Join(s.resolver.resolveSGNames(eni.accountID, eni.securityGroupIDs), "\n"))
|
||||
case "subnet-ipv4-cidr-block":
|
||||
s.serveCIDR(w, eni.accountID, eni.subnetID, s.resolver.resolveSubnetCIDR)
|
||||
case "vpc-ipv4-cidr-block", "vpc-ipv4-cidr-blocks":
|
||||
s.serveCIDR(w, eni.accountID, eni.vpcID, s.resolver.resolveVPCCIDR)
|
||||
case "public-ipv4s", "public-hostname":
|
||||
if eni.publicIP == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeText(w, eni.publicIP)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// macKeys lists exactly the leaf keys served under macs/<mac>/ so cloud-init's
|
||||
// recursive crawl never lists a key that 404s: CIDR keys appear only when the CIDR
|
||||
// resolves, public keys only with a public IP. A resolver fault is propagated so the
|
||||
// listing 500s like the leaf, never silently dropping a key on a transient KV blip.
|
||||
func (s *IMDSServiceImpl) macKeys(eni *eniFacts) ([]string, error) {
|
||||
keys := []string{
|
||||
"device-number", "interface-id", "local-hostname", "local-ipv4s",
|
||||
"mac", "owner-id", "security-group-ids", "security-groups",
|
||||
"subnet-id", "vpc-id",
|
||||
}
|
||||
subnetCIDR, err := s.resolver.resolveSubnetCIDR(eni.accountID, eni.subnetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subnetCIDR != "" {
|
||||
keys = append(keys, "subnet-ipv4-cidr-block")
|
||||
}
|
||||
vpcCIDR, err := s.resolver.resolveVPCCIDR(eni.accountID, eni.vpcID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vpcCIDR != "" {
|
||||
keys = append(keys, "vpc-ipv4-cidr-block", "vpc-ipv4-cidr-blocks")
|
||||
}
|
||||
if eni.publicIP != "" {
|
||||
keys = append(keys, "public-hostname", "public-ipv4s")
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// serveCIDR resolves and writes a subnet/VPC CIDR: 404 on miss, 500 on a backend
|
||||
// fault, so a guest never renders network config from an empty CIDR.
|
||||
func (s *IMDSServiceImpl) serveCIDR(w http.ResponseWriter, accountID, id string, resolve func(string, string) (string, error)) {
|
||||
cidr, err := resolve(accountID, id)
|
||||
if err != nil {
|
||||
slog.Error("IMDS: network-interface CIDR resolution failed", "account_id", accountID, "id", id, "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if cidr == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeText(w, cidr)
|
||||
}
|
||||
|
||||
// instanceFor resolves the instance record for an ENI, writing the appropriate
|
||||
// HTTP error and returning nil on miss/failure so callers can early-return.
|
||||
func (s *IMDSServiceImpl) instanceFor(w http.ResponseWriter, eni *eniFacts) *instanceFacts {
|
||||
|
||||
@@ -22,11 +22,15 @@ import (
|
||||
// ----- fakes -------------------------------------------------------------
|
||||
|
||||
type fakeResolver struct {
|
||||
eni *eniFacts
|
||||
eniErr error
|
||||
inst *instanceFacts
|
||||
instErr error
|
||||
sgNames map[string]string // sg-id → group name; absent IDs fall back to the ID
|
||||
eni *eniFacts
|
||||
eniErr error
|
||||
inst *instanceFacts
|
||||
instErr error
|
||||
sgNames map[string]string // sg-id → group name; absent IDs fall back to the ID
|
||||
subnetCIDR string
|
||||
subnetErr error
|
||||
vpcCIDR string
|
||||
vpcErr error
|
||||
}
|
||||
|
||||
func (f *fakeResolver) resolveENIByID(_ string) (*eniFacts, error) { return f.eni, f.eniErr }
|
||||
@@ -46,6 +50,11 @@ func (f *fakeResolver) resolveSGNames(_ string, sgIDs []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func (f *fakeResolver) resolveSubnetCIDR(_, _ string) (string, error) {
|
||||
return f.subnetCIDR, f.subnetErr
|
||||
}
|
||||
func (f *fakeResolver) resolveVPCCIDR(_, _ string) (string, error) { return f.vpcCIDR, f.vpcErr }
|
||||
|
||||
type fakeIAM struct {
|
||||
profile *handlers_iam.InstanceProfile
|
||||
profileErr error
|
||||
@@ -445,6 +454,7 @@ func TestHTTP_DirectoryListing(t *testing.T) {
|
||||
"local-hostname",
|
||||
"local-ipv4",
|
||||
"mac",
|
||||
"network/",
|
||||
"placement/",
|
||||
"public-hostname",
|
||||
"public-ipv4",
|
||||
@@ -471,10 +481,12 @@ func TestHTTP_OutOfScopePaths404(t *testing.T) {
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
for _, p := range []string{
|
||||
pathIdentityDir + "/pkcs7", // signed forms stay deferred until the signing key lands
|
||||
pathIdentityDir + "/rsa2048", //
|
||||
pathIdentityDir + "/signature", //
|
||||
prefixMetaData + "network/interfaces/macs",
|
||||
pathIdentityDir + "/pkcs7", // signed forms stay deferred until the signing key lands
|
||||
pathIdentityDir + "/rsa2048", //
|
||||
pathIdentityDir + "/signature", //
|
||||
prefixNetworkMacs + testENI().mac + "/ipv4-associations", // IPv6/associations stay deferred
|
||||
prefixNetworkMacs + testENI().mac + "/ipv6s", //
|
||||
prefixNetworkMacs + "02:00:00:00:00:00/subnet-id", // a MAC that is not the caller's
|
||||
prefixMetaData + "nonsense",
|
||||
} {
|
||||
rec := get(t, h, p, token)
|
||||
@@ -482,6 +494,169 @@ func TestHTTP_OutOfScopePaths404(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ----- network interfaces ------------------------------------------------
|
||||
|
||||
const (
|
||||
testSubnetCIDR = "10.0.1.0/24"
|
||||
testVPCCIDR = "10.0.0.0/16"
|
||||
)
|
||||
|
||||
// macPath builds a /network/interfaces/macs/<mac>[/<key>] request path.
|
||||
func macPath(mac, key string) string {
|
||||
p := prefixNetworkMacs + mac
|
||||
if key != "" {
|
||||
p += "/" + key
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// The intermediate directory listings walk down to the caller's single MAC.
|
||||
func TestHTTP_NetworkInterfacesListings(t *testing.T) {
|
||||
svc, _ := newTestService(&fakeResolver{eni: testENI()}, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
mac := testENI().mac
|
||||
cases := []struct{ path, want string }{
|
||||
{prefixMetaData + "network", "interfaces/"},
|
||||
{prefixMetaData + "network/", "interfaces/"},
|
||||
{prefixMetaData + "network/interfaces", "macs/"},
|
||||
{prefixMetaData + "network/interfaces/", "macs/"},
|
||||
{prefixMetaData + "network/interfaces/macs", mac + "/"},
|
||||
{prefixMetaData + "network/interfaces/macs/", mac + "/"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rec := get(t, h, c.path, token)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "path=%s", c.path)
|
||||
assert.Equal(t, c.want, rec.Body.String(), "path=%s", c.path)
|
||||
}
|
||||
}
|
||||
|
||||
// macs/<mac> and macs/<mac>/ list exactly the leaves served — CIDR and public keys
|
||||
// included only when they resolve, so cloud-init's crawl never lists a key that 404s.
|
||||
func TestHTTP_NetworkInterfaceMacDirListing(t *testing.T) {
|
||||
res := &fakeResolver{eni: testENI(), subnetCIDR: testSubnetCIDR, vpcCIDR: testVPCCIDR}
|
||||
svc, _ := newTestService(res, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
want := strings.Join([]string{
|
||||
"device-number",
|
||||
"interface-id",
|
||||
"local-hostname",
|
||||
"local-ipv4s",
|
||||
"mac",
|
||||
"owner-id",
|
||||
"public-hostname",
|
||||
"public-ipv4s",
|
||||
"security-group-ids",
|
||||
"security-groups",
|
||||
"subnet-id",
|
||||
"subnet-ipv4-cidr-block",
|
||||
"vpc-id",
|
||||
"vpc-ipv4-cidr-block",
|
||||
"vpc-ipv4-cidr-blocks",
|
||||
}, "\n")
|
||||
mac := testENI().mac
|
||||
for _, p := range []string{macPath(mac, ""), macPath(mac, "") + "/"} {
|
||||
rec := get(t, h, p, token)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "path=%s", p)
|
||||
assert.Equal(t, want, rec.Body.String(), "path=%s", p)
|
||||
}
|
||||
}
|
||||
|
||||
// Every served leaf resolves from eni + resolver facts.
|
||||
func TestHTTP_NetworkInterfaceLeaves(t *testing.T) {
|
||||
res := &fakeResolver{
|
||||
eni: testENI(),
|
||||
sgNames: map[string]string{"sg-1": "web-sg", "sg-2": "db-sg"},
|
||||
subnetCIDR: testSubnetCIDR,
|
||||
vpcCIDR: testVPCCIDR,
|
||||
}
|
||||
svc, _ := newTestService(res, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
mac := testENI().mac
|
||||
cases := []struct{ key, want string }{
|
||||
{"mac", mac},
|
||||
{"device-number", "0"},
|
||||
{"interface-id", "eni-aaa"},
|
||||
{"owner-id", "111122223333"},
|
||||
{"subnet-id", "subnet-1"},
|
||||
{"vpc-id", testVPC},
|
||||
{"local-ipv4s", testIP},
|
||||
{"local-hostname", "ip-10-0-1-5.ap-southeast-2.compute.internal"},
|
||||
{"security-group-ids", "sg-1\nsg-2"},
|
||||
{"security-groups", "web-sg\ndb-sg"},
|
||||
{"subnet-ipv4-cidr-block", testSubnetCIDR},
|
||||
{"vpc-ipv4-cidr-block", testVPCCIDR},
|
||||
{"vpc-ipv4-cidr-blocks", testVPCCIDR},
|
||||
{"public-ipv4s", "203.0.113.7"},
|
||||
{"public-hostname", "203.0.113.7"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rec := get(t, h, macPath(mac, c.key), token)
|
||||
assert.Equal(t, http.StatusOK, rec.Code, "key=%s", c.key)
|
||||
assert.Equal(t, c.want, rec.Body.String(), "key=%s", c.key)
|
||||
}
|
||||
}
|
||||
|
||||
// With no public IP, public-ipv4s and public-hostname 404 and drop from the listing.
|
||||
func TestHTTP_NetworkInterfacePublicAbsent(t *testing.T) {
|
||||
eni := testENI()
|
||||
eni.publicIP = ""
|
||||
res := &fakeResolver{eni: eni, subnetCIDR: testSubnetCIDR, vpcCIDR: testVPCCIDR}
|
||||
svc, _ := newTestService(res, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), eni)
|
||||
token := issueToken(t, h)
|
||||
|
||||
listing := get(t, h, macPath(eni.mac, ""), token).Body.String()
|
||||
assert.NotContains(t, listing, "public-ipv4s")
|
||||
assert.NotContains(t, listing, "public-hostname")
|
||||
|
||||
for _, key := range []string{"public-ipv4s", "public-hostname"} {
|
||||
rec := get(t, h, macPath(eni.mac, key), token)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code, "key=%s", key)
|
||||
}
|
||||
}
|
||||
|
||||
// On a CIDR miss the CIDR leaves 404 and drop from the listing; a resolver error
|
||||
// is a 500 on both the leaf and the listing, never an empty/dropped CIDR a guest
|
||||
// would mis-render into its network config.
|
||||
func TestHTTP_NetworkInterfaceCidrMiss(t *testing.T) {
|
||||
res := &fakeResolver{eni: testENI()} // no canned CIDRs → miss
|
||||
svc, _ := newTestService(res, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
mac := testENI().mac
|
||||
|
||||
listing := get(t, h, macPath(mac, ""), token).Body.String()
|
||||
assert.NotContains(t, listing, "subnet-ipv4-cidr-block")
|
||||
assert.NotContains(t, listing, "vpc-ipv4-cidr-block")
|
||||
for _, key := range []string{"subnet-ipv4-cidr-block", "vpc-ipv4-cidr-block", "vpc-ipv4-cidr-blocks"} {
|
||||
rec := get(t, h, macPath(mac, key), token)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code, "key=%s", key)
|
||||
}
|
||||
|
||||
errRes := &fakeResolver{eni: testENI(), subnetErr: errors.New("kv unavailable"), vpcErr: errors.New("kv unavailable")}
|
||||
errSvc, _ := newTestService(errRes, &fakeIAM{}, &fakeAssumer{})
|
||||
errH := withTapENI(errSvc.httpHandler(), testENI())
|
||||
errToken := issueToken(t, errH)
|
||||
for _, key := range []string{"subnet-ipv4-cidr-block", "vpc-ipv4-cidr-block", "vpc-ipv4-cidr-blocks"} {
|
||||
rec := get(t, errH, macPath(mac, key), errToken)
|
||||
assert.Equal(t, http.StatusInternalServerError, rec.Code, "key=%s", key)
|
||||
}
|
||||
listingRec := get(t, errH, macPath(mac, ""), errToken)
|
||||
assert.Equal(t, http.StatusInternalServerError, listingRec.Code, "listing under resolver error")
|
||||
}
|
||||
|
||||
// A MAC that is not the caller's is 404: the per-tap responder only serves its own ENI.
|
||||
func TestHTTP_NetworkInterfaceForeignMac404(t *testing.T) {
|
||||
svc, _ := newTestService(&fakeResolver{eni: testENI()}, &fakeIAM{}, &fakeAssumer{})
|
||||
h := withTapENI(svc.httpHandler(), testENI())
|
||||
token := issueToken(t, h)
|
||||
rec := get(t, h, macPath("02:00:00:00:00:00", "subnet-id"), token)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
}
|
||||
|
||||
// ----- public keys -------------------------------------------------------
|
||||
|
||||
func TestHTTP_PublicKeys(t *testing.T) {
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
// kvBucketENIs and kvBucketSecurityGroups are duplicated as literals to avoid an import cycle.
|
||||
const kvBucketENIs = "spinifex-vpc-enis"
|
||||
const kvBucketSecurityGroups = "spinifex-vpc-security-groups"
|
||||
const kvBucketSubnets = "spinifex-vpc-subnets"
|
||||
const kvBucketVPCs = "spinifex-vpc-vpcs"
|
||||
|
||||
// eniFacts is the ENI fields served by the IMDS metadata surface, read live from the ENI record.
|
||||
type eniFacts struct {
|
||||
@@ -68,9 +70,11 @@ type eniRecord struct {
|
||||
// metadataResolver resolves a tap's ENI ID to ENI + instance facts.
|
||||
// Resolution chain: eniID → ENIRecord (account recovered from the bucket key) → instanceFacts.
|
||||
type metadataResolver struct {
|
||||
eniKV nats.KeyValue // spinifex-vpc-enis
|
||||
sgKV nats.KeyValue // spinifex-vpc-security-groups (nil-safe: degrades to IDs)
|
||||
lookup instanceLookup
|
||||
eniKV nats.KeyValue // spinifex-vpc-enis
|
||||
sgKV nats.KeyValue // spinifex-vpc-security-groups (nil-safe: degrades to IDs)
|
||||
subnetKV nats.KeyValue // spinifex-vpc-subnets (nil-safe: CIDR leaf 404s)
|
||||
vpcKV nats.KeyValue // spinifex-vpc-vpcs (nil-safe: CIDR leaf 404s)
|
||||
lookup instanceLookup
|
||||
}
|
||||
|
||||
var _ eniResolver = (*metadataResolver)(nil)
|
||||
@@ -178,3 +182,37 @@ func (r *metadataResolver) resolveSGNames(accountID string, sgIDs []string) []st
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// cidrRecord is the subnet/VPC record subset the network-interfaces subtree reads.
|
||||
type cidrRecord struct {
|
||||
CidrBlock string `json:"cidr_block"`
|
||||
}
|
||||
|
||||
func (r *metadataResolver) resolveSubnetCIDR(accountID, subnetID string) (string, error) {
|
||||
return cidrFromKV(r.subnetKV, accountID, subnetID)
|
||||
}
|
||||
|
||||
func (r *metadataResolver) resolveVPCCIDR(accountID, vpcID string) (string, error) {
|
||||
return cidrFromKV(r.vpcKV, accountID, vpcID)
|
||||
}
|
||||
|
||||
// cidrFromKV reads cidr_block from an account-scoped record. ("", nil) on a nil
|
||||
// bucket or key miss; the error on any other fault so the leaf 500s rather than
|
||||
// serving an empty CIDR a guest would mis-render into its network config.
|
||||
func cidrFromKV(kv nats.KeyValue, accountID, id string) (string, error) {
|
||||
if kv == nil || id == "" {
|
||||
return "", nil
|
||||
}
|
||||
entry, err := kv.Get(accountID + "." + id)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("get cidr record %s.%s: %w", accountID, id, err)
|
||||
}
|
||||
var rec cidrRecord
|
||||
if err := json.Unmarshal(entry.Value(), &rec); err != nil {
|
||||
return "", fmt.Errorf("unmarshal cidr record %s.%s: %w", accountID, id, err)
|
||||
}
|
||||
return rec.CidrBlock, nil
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ type eniResolver interface {
|
||||
resolveENIByID(eniID string) (*eniFacts, error)
|
||||
resolveInstance(eni *eniFacts) (*instanceFacts, error)
|
||||
resolveSGNames(accountID string, sgIDs []string) []string
|
||||
resolveSubnetCIDR(accountID, subnetID string) (string, error)
|
||||
resolveVPCCIDR(accountID, vpcID string) (string, error)
|
||||
}
|
||||
|
||||
// IMDSServiceImpl is the in-process IMDS implementation. It runs one per-tap
|
||||
@@ -101,11 +103,26 @@ func NewIMDSServiceImpl(natsConn *nats.Conn, sts stsAssumer, iamSvc profileLooku
|
||||
sgKV = nil
|
||||
}
|
||||
|
||||
// Open subnet/VPC buckets best-effort; the network-interfaces CIDR leaves 404
|
||||
// (and drop from the listing) when a bucket is unavailable, IMDS still starts.
|
||||
subnetKV, err := js.KeyValue(kvBucketSubnets)
|
||||
if err != nil {
|
||||
slog.Warn("IMDS: subnet bucket unavailable, network-interfaces subnet CIDR will 404", "bucket", kvBucketSubnets, "err", err)
|
||||
subnetKV = nil
|
||||
}
|
||||
vpcKV, err := js.KeyValue(kvBucketVPCs)
|
||||
if err != nil {
|
||||
slog.Warn("IMDS: vpc bucket unavailable, network-interfaces VPC CIDR will 404", "bucket", kvBucketVPCs, "err", err)
|
||||
vpcKV = nil
|
||||
}
|
||||
|
||||
svc := &IMDSServiceImpl{
|
||||
resolver: &metadataResolver{
|
||||
eniKV: eniKV,
|
||||
sgKV: sgKV,
|
||||
lookup: &natsInstanceLookup{nc: natsConn, expectedNodes: expectedNodes},
|
||||
eniKV: eniKV,
|
||||
sgKV: sgKV,
|
||||
subnetKV: subnetKV,
|
||||
vpcKV: vpcKV,
|
||||
lookup: &natsInstanceLookup{nc: natsConn, expectedNodes: expectedNodes},
|
||||
},
|
||||
tokens: newTokenStore(),
|
||||
creds: newCredCache(sts),
|
||||
|
||||
Reference in New Issue
Block a user