mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
93db7e4d1b
* feat(ecs): ecs-agent skeleton + containerd image pull (Sprint 4c) Add the ecs-agent binary that runs inside an ECS container instance: it resolves host identity from IMDS, registers on the Layer-2 NATS bus, and heartbeats while alive. Image pulls go through a containerd-backed ImagePuller behind an interface, with an ECR resolver that mints GetAuthorizationToken creds from the instance role. Extract the IMDS-credential and ECR-auth flow out of ecr-credential-provider into internal/imdscreds and internal/ecrauth so the agent and the kubelet provider share one implementation. Add spinifex/handlers/ecs/bus as the single source of truth for Layer-2 subject builders and wire payloads, shared by the agent now and the scheduler in Sprint 4e. * build: pin genproto via replace to fix containerd/v2 ambiguous import go.opencensus.io -> grpc@v1.33.2 drags the pre-split monolithic genproto into the module graph; it still ships googleapis/rpc/status, which collides with the split genproto/googleapis/rpc module that containerd/ttrpc needs. The main module never imports the monolith, so a plain require bump is dropped by 'go mod tidy'. Pin it with a replace to a post-split version so the fix survives tidy and contributors can run 'go mod tidy' freely. * fix(ecs): agent boot failsafes from runtime verify - Build the ECR gateway HTTP client lazily on first image pull instead of at boot. A missing/malformed gateway CA no longer kills the agent before it can register + heartbeat, matching the best-effort containerd path. - Actively probe containerd with IsServing in runtime.New. The client dials lazily, so a dead socket previously slipped past the boot-time 'containerd unavailable' warn and only surfaced on the first pull. - Make the heartbeat interval configurable via ECS_HEARTBEAT_INTERVAL (default 30s) for test/verify and slow-link tuning.
98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// instanceMetadata is the IMDS-derived identity of the host the agent runs on.
|
|
type instanceMetadata struct {
|
|
AccountID string
|
|
InstanceID string
|
|
AZ string
|
|
}
|
|
|
|
// fetchInstanceMetadata reads the instance-id, AZ and account-id from IMDSv2.
|
|
// base is the IMDS root, e.g. "http://169.254.169.254/latest".
|
|
func fetchInstanceMetadata(client *http.Client, base string) (instanceMetadata, error) {
|
|
token, err := imdsToken(client, base)
|
|
if err != nil {
|
|
return instanceMetadata{}, err
|
|
}
|
|
|
|
instanceID, err := imdsGet(client, base+"/meta-data/instance-id", token)
|
|
if err != nil {
|
|
return instanceMetadata{}, fmt.Errorf("instance-id: %w", err)
|
|
}
|
|
az, err := imdsGet(client, base+"/meta-data/placement/availability-zone", token)
|
|
if err != nil {
|
|
return instanceMetadata{}, fmt.Errorf("availability-zone: %w", err)
|
|
}
|
|
|
|
doc, err := imdsGet(client, base+"/dynamic/instance-identity/document", token)
|
|
if err != nil {
|
|
return instanceMetadata{}, fmt.Errorf("identity document: %w", err)
|
|
}
|
|
var ident struct {
|
|
AccountID string `json:"accountId"`
|
|
}
|
|
if err := json.Unmarshal([]byte(doc), &ident); err != nil {
|
|
return instanceMetadata{}, fmt.Errorf("parse identity document: %w", err)
|
|
}
|
|
|
|
return instanceMetadata{
|
|
AccountID: ident.AccountID,
|
|
InstanceID: strings.TrimSpace(instanceID),
|
|
AZ: strings.TrimSpace(az),
|
|
}, nil
|
|
}
|
|
|
|
// imdsToken fetches an IMDSv2 session token.
|
|
func imdsToken(client *http.Client, base string) (string, error) {
|
|
req, err := http.NewRequest(http.MethodPut, base+"/api/token", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "21600")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("imds token: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("imds token: status %d", resp.StatusCode)
|
|
}
|
|
return string(body), nil
|
|
}
|
|
|
|
// imdsGet performs a token-authenticated IMDS GET.
|
|
func imdsGet(client *http.Client, url, token string) (string, error) {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("X-Aws-Ec2-Metadata-Token", token)
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("status %d", resp.StatusCode)
|
|
}
|
|
return string(body), nil
|
|
}
|