mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 19:25:52 +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.
77 lines
2.6 KiB
Go
77 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func metadataStub(t *testing.T, enforceV2 bool) *httptest.Server {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/token", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte("v2-token"))
|
|
})
|
|
guard := func(w http.ResponseWriter, r *http.Request) bool {
|
|
if enforceV2 && r.Header.Get("X-Aws-Ec2-Metadata-Token") != "v2-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
mux.HandleFunc("/meta-data/instance-id", func(w http.ResponseWriter, r *http.Request) {
|
|
if guard(w, r) {
|
|
_, _ = w.Write([]byte("i-0abc123"))
|
|
}
|
|
})
|
|
mux.HandleFunc("/meta-data/placement/availability-zone", func(w http.ResponseWriter, r *http.Request) {
|
|
if guard(w, r) {
|
|
_, _ = w.Write([]byte("us-east-1a"))
|
|
}
|
|
})
|
|
mux.HandleFunc("/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) {
|
|
if guard(w, r) {
|
|
_, _ = w.Write([]byte(`{"accountId":"123456789012","region":"us-east-1"}`))
|
|
}
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestFetchInstanceMetadata_OK(t *testing.T) {
|
|
srv := metadataStub(t, true)
|
|
got, err := fetchInstanceMetadata(srv.Client(), srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("fetch: %v", err)
|
|
}
|
|
if got.InstanceID != "i-0abc123" || got.AZ != "us-east-1a" || got.AccountID != "123456789012" {
|
|
t.Errorf("metadata mismatch: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchInstanceMetadata_TokenRequired(t *testing.T) {
|
|
// Server returns 405 for non-PUT token; a GET-only client would 401 downstream.
|
|
srv := metadataStub(t, true)
|
|
if _, err := fetchInstanceMetadata(srv.Client(), srv.URL); err != nil {
|
|
t.Fatalf("v2 path should succeed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFetchInstanceMetadata_BadDocument(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/token", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("t")) })
|
|
mux.HandleFunc("/meta-data/instance-id", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("i-1")) })
|
|
mux.HandleFunc("/meta-data/placement/availability-zone", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("z")) })
|
|
mux.HandleFunc("/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("not json")) })
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
if _, err := fetchInstanceMetadata(srv.Client(), srv.URL); err == nil {
|
|
t.Fatal("expected parse error on bad identity document")
|
|
}
|
|
}
|