mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +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.
120 lines
3.8 KiB
Go
120 lines
3.8 KiB
Go
// Package ecrauth mints ECR registry credentials by SigV4-signing
|
|
// GetAuthorizationToken against the Spinifex AWS gateway's ECR endpoint and
|
|
// decoding the returned AWS:<jwt> token into Basic-auth parts. It is shared by
|
|
// the in-guest binaries that pull images from the internal registry: the EKS
|
|
// ecr-credential-provider and the ECS ecs-agent.
|
|
package ecrauth
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/ecr"
|
|
"github.com/mulgadc/spinifex/internal/tlsconfig"
|
|
)
|
|
|
|
// defaultRegion is used when the caller supplies no region; SigV4 still needs a
|
|
// scope and the gateway does not enforce a specific value.
|
|
const defaultRegion = "us-east-1"
|
|
|
|
// Token is the decoded result of GetAuthorizationToken: Basic-auth parts plus
|
|
// the registry proxy host and token expiry.
|
|
type Token struct {
|
|
Username string
|
|
Password string
|
|
ProxyHost string
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// GetAuthorizationToken builds an aws-sdk ECR client pointed at the gateway and
|
|
// calls GetAuthorizationToken with the supplied static credentials, decoding the
|
|
// AWS:<jwt> token into Basic-auth parts. httpClient must trust the gateway CA.
|
|
func GetAuthorizationToken(region, gatewayURL string, httpClient *http.Client, akid, secret, sessionToken string) (Token, error) {
|
|
if region == "" {
|
|
region = defaultRegion
|
|
}
|
|
sess, err := session.NewSession(&aws.Config{
|
|
Region: aws.String(region),
|
|
Endpoint: aws.String(gatewayURL),
|
|
Credentials: credentials.NewStaticCredentials(akid, secret, sessionToken),
|
|
HTTPClient: httpClient,
|
|
DisableSSL: aws.Bool(false),
|
|
})
|
|
if err != nil {
|
|
return Token{}, fmt.Errorf("new session: %w", err)
|
|
}
|
|
|
|
out, err := ecr.New(sess).GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
|
|
if err != nil {
|
|
return Token{}, fmt.Errorf("GetAuthorizationToken: %w", err)
|
|
}
|
|
if len(out.AuthorizationData) == 0 || out.AuthorizationData[0].AuthorizationToken == nil {
|
|
return Token{}, fmt.Errorf("GetAuthorizationToken returned no authorization data")
|
|
}
|
|
data := out.AuthorizationData[0]
|
|
|
|
raw, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken))
|
|
if err != nil {
|
|
return Token{}, fmt.Errorf("decode authorization token: %w", err)
|
|
}
|
|
user, pass, ok := strings.Cut(string(raw), ":")
|
|
if !ok {
|
|
return Token{}, fmt.Errorf("authorization token not in user:password form")
|
|
}
|
|
|
|
tok := Token{
|
|
Username: user,
|
|
Password: pass,
|
|
ProxyHost: HostFromImage(aws.StringValue(data.ProxyEndpoint)),
|
|
}
|
|
if data.ExpiresAt != nil {
|
|
tok.ExpiresAt = aws.TimeValue(data.ExpiresAt)
|
|
}
|
|
return tok, nil
|
|
}
|
|
|
|
// GatewayHTTPClient builds an HTTP client trusting the gateway CA at caPath.
|
|
// When caPath is empty it relies on the system trust store.
|
|
func GatewayHTTPClient(caPath string) (*http.Client, error) {
|
|
tlsCfg := &tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
CurvePreferences: tlsconfig.Curves,
|
|
}
|
|
if caPath != "" {
|
|
pem, err := os.ReadFile(caPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read gateway CA %q: %w", caPath, err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("gateway CA %q has no usable certificates", caPath)
|
|
}
|
|
tlsCfg.RootCAs = pool
|
|
}
|
|
return &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
Transport: &http.Transport{TLSClientConfig: tlsCfg, MaxIdleConns: 2, IdleConnTimeout: 30 * time.Second},
|
|
}, nil
|
|
}
|
|
|
|
// HostFromImage returns the host[:port] portion of an image ref or URL. It strips
|
|
// any scheme and trims at the first path separator.
|
|
func HostFromImage(image string) string {
|
|
if i := strings.Index(image, "://"); i >= 0 {
|
|
image = image[i+3:]
|
|
}
|
|
if i := strings.IndexByte(image, '/'); i >= 0 {
|
|
image = image[:i]
|
|
}
|
|
return image
|
|
}
|