diff --git a/cmd/ecs-agent/main.go b/cmd/ecs-agent/main.go index 3dbf6719a..4a53f897c 100644 --- a/cmd/ecs-agent/main.go +++ b/cmd/ecs-agent/main.go @@ -1,13 +1,6 @@ -// ecs-agent runs inside a Spinifex ECS container instance (a guest VM booted from -// the ECS-AMI, with containerd baked in). It registers the host with the ECS -// scheduler through the AWS gateway over TLS+SigV4 (never NATS), heartbeats by -// re-registering, polls the gateway for task assignments, runs them through -// containerd, and reports state back over the gateway. -// -// Static config (gateway URL, CA, region, cluster, seeded IAM creds, containerd -// socket) is read from the cloud-init env file /etc/spinifex-ecs/agent.env -// (KEY=value); real env vars override it. Host identity (account, instance, AZ) -// comes from IMDS at boot. +// Command ecs-agent runs inside a Spinifex ECS container instance; the +// implementation is in agents/ecs/agent. This binary owns process lifecycle: +// signal handling, startup and run failure logging, and the exit code. package main import ( @@ -18,12 +11,19 @@ import ( "syscall" _ "github.com/mulgadc/bluebottle/pkg/fipsboot" + + ecsagent "github.com/mulgadc/spinifex/spinifex/agents/ecs/agent" ) -func main() { - cfg := loadConfig(defaultEnvFile) +// version is the agent build version, reported in the register message. +// Overridable via -ldflags "-X main.version=...". +var version = "dev" - agent, err := New(cfg) +func main() { + cfg := ecsagent.LoadConfig(ecsagent.DefaultEnvFile) + cfg.AgentVersion = version + + agent, err := ecsagent.New(cfg) if err != nil { slog.Error("ecs-agent: startup failed", "err", err) os.Exit(1) diff --git a/cmd/ecs-agent/agent.go b/spinifex/agents/ecs/agent/agent.go similarity index 95% rename from cmd/ecs-agent/agent.go rename to spinifex/agents/ecs/agent/agent.go index 76019e66f..83064f07b 100644 --- a/cmd/ecs-agent/agent.go +++ b/spinifex/agents/ecs/agent/agent.go @@ -1,4 +1,4 @@ -package main +package agent import ( "bufio" @@ -17,17 +17,13 @@ import ( "github.com/mulgadc/spinifex/spinifex/handlers/ecs/bus" ) -// version is the agent build version, reported in the register message. -// Overridable via -ldflags "-X main.version=...". -var version = "dev" - // Agent wires the ecs-agent's runtime seams: a gateway control-plane client, a // container runtime, and an ECR resolver. It registers the host as a container // instance at boot, heartbeats (re-registers) while alive, polls the gateway for // task assignments, and runs them through containerd, reporting state back over // the gateway. It never connects to NATS — the bus stays host-internal. type Agent struct { - cfg config + cfg Config id identity cp controlPlane resolver ctrruntime.Resolver @@ -62,7 +58,7 @@ type Agent struct { // with fakes; New builds the production seams and delegates here. rt may be nil // when containerd is unavailable; the assign path then reports the task STOPPED // rather than crashing, and the watch loop keeps dialling for it. -func newAgent(cfg config, id identity, cp controlPlane, rt ctrruntime.Runtime, resolver ctrruntime.Resolver) *Agent { +func newAgent(cfg Config, id identity, cp controlPlane, rt ctrruntime.Runtime, resolver ctrruntime.Resolver) *Agent { gate := newRuntimeGate(rt) a := &Agent{ cfg: cfg, @@ -87,7 +83,7 @@ func newAgent(cfg config, id identity, cp controlPlane, rt ctrruntime.Runtime, r // registration and heartbeat still run so the instance is visible while the // runtime recovers. The ECR gateway client is built lazily on first image pull // (not here), so a missing or malformed gateway CA does not stop registration. -func New(cfg config) (*Agent, error) { +func New(cfg Config) (*Agent, error) { imdsClient := &http.Client{Timeout: 5 * time.Second} meta, err := fetchInstanceMetadata(imdsClient, cfg.IMDSBase) @@ -103,7 +99,7 @@ func New(cfg config) (*Agent, error) { InstanceType: meta.InstanceType, Hostname: host, Capacity: detectCapacity(discoverNvidiaGPUs(execCommandRunner)), - AgentVersion: version, + AgentVersion: cfg.AgentVersion, } creds := credentials.NewIMDSProvider(imdsClient, cfg.IMDSBase) diff --git a/cmd/ecs-agent/agent_test.go b/spinifex/agents/ecs/agent/agent_test.go similarity index 95% rename from cmd/ecs-agent/agent_test.go rename to spinifex/agents/ecs/agent/agent_test.go index 0a49f84b0..137dcae0c 100644 --- a/cmd/ecs-agent/agent_test.go +++ b/spinifex/agents/ecs/agent/agent_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -10,7 +10,7 @@ import ( func TestAgent_RunRegistersThenStopsOnContext(t *testing.T) { cp := &fakeCP{} - cfg := config{Heartbeat: 5 * time.Millisecond, PollInterval: 5 * time.Millisecond} + cfg := Config{Heartbeat: 5 * time.Millisecond, PollInterval: 5 * time.Millisecond} puller := &ctrruntime.FakePuller{} a := newAgent(cfg, testIdentity(), cp, puller, nil) a.closers = append(a.closers, puller.Close) diff --git a/cmd/ecs-agent/config.go b/spinifex/agents/ecs/agent/config.go similarity index 83% rename from cmd/ecs-agent/config.go rename to spinifex/agents/ecs/agent/config.go index dac91e554..7341dddae 100644 --- a/cmd/ecs-agent/config.go +++ b/spinifex/agents/ecs/agent/config.go @@ -1,4 +1,4 @@ -package main +package agent import ( "strconv" @@ -7,8 +7,11 @@ import ( "github.com/mulgadc/spinifex/internal/guestenv" ) +// DefaultEnvFile is the cloud-init env file the ECS AMI seeds with the +// agent's static configuration. +const DefaultEnvFile = "/etc/spinifex-ecs/agent.env" + const ( - defaultEnvFile = "/etc/spinifex-ecs/agent.env" defaultGatewayCA = "/etc/spinifex-ecs/gateway-ca.pem" defaultIMDSBase = "http://169.254.169.254/latest" defaultContainerdSocket = "/run/containerd/containerd.sock" @@ -16,13 +19,13 @@ const ( defaultPollInterval = 5 * time.Second ) -// config holds the static settings the agent reads at boot. Cluster identity +// Config holds the static settings the agent reads at boot. Cluster identity // (account ID, instance ID) is discovered at runtime from IMDS, not configured; // only the cluster *name* the instance was launched into is static. AccessKey / // SecretKey are legacy seeded creds, optional now that the agent SigV4-signs // gateway calls with instance-role credentials from IMDS; the agent never holds // a NATS token. -type config struct { +type Config struct { GatewayURL string GatewayCA string Region string @@ -36,13 +39,16 @@ type config struct { RuntimeRetry time.Duration CredEndpointIP string CredEndpointPort int + // AgentVersion is the binary's build version, reported in the register + // message; the binary sets it, not the env file. + AgentVersion string } -// loadConfig reads the cloud-init env file then lets real env vars override. -func loadConfig(envFile string) config { +// LoadConfig reads the cloud-init env file then lets real env vars override. +func LoadConfig(envFile string) Config { get := guestenv.Load(envFile).Get - cfg := config{ + cfg := Config{ GatewayURL: get("ECS_GATEWAY_URL"), GatewayCA: get("ECS_GATEWAY_CA"), Region: get("ECS_REGION"), diff --git a/cmd/ecs-agent/config_test.go b/spinifex/agents/ecs/agent/config_test.go similarity index 90% rename from cmd/ecs-agent/config_test.go rename to spinifex/agents/ecs/agent/config_test.go index d2d491632..ad7648fae 100644 --- a/cmd/ecs-agent/config_test.go +++ b/spinifex/agents/ecs/agent/config_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "os" @@ -14,7 +14,7 @@ func TestLoadConfig_Defaults(t *testing.T) { "ECS_CONTAINERD_SOCKET", "ECS_HEARTBEAT_INTERVAL", "ECS_POLL_INTERVAL"} { t.Setenv(k, "") } - cfg := loadConfig(filepath.Join(t.TempDir(), "absent.env")) + cfg := LoadConfig(filepath.Join(t.TempDir(), "absent.env")) if cfg.GatewayCA != defaultGatewayCA { t.Errorf("GatewayCA = %q, want default", cfg.GatewayCA) } @@ -45,7 +45,7 @@ func TestLoadConfig_FileThenEnvOverride(t *testing.T) { t.Setenv("ECS_GATEWAY_URL", "https://gw.env") t.Setenv("ECS_ACCESS_KEY", "AKIAENV") - cfg := loadConfig(envFile) + cfg := LoadConfig(envFile) if cfg.GatewayURL != "https://gw.env" { t.Errorf("GatewayURL = %q, want env override", cfg.GatewayURL) } @@ -62,12 +62,12 @@ func TestLoadConfig_FileThenEnvOverride(t *testing.T) { func TestLoadConfig_HeartbeatOverride(t *testing.T) { t.Setenv("ECS_HEARTBEAT_INTERVAL", "5s") - if cfg := loadConfig(filepath.Join(t.TempDir(), "absent.env")); cfg.Heartbeat != 5*time.Second { + if cfg := LoadConfig(filepath.Join(t.TempDir(), "absent.env")); cfg.Heartbeat != 5*time.Second { t.Errorf("Heartbeat = %v, want 5s", cfg.Heartbeat) } // Garbage value falls back to the default, not zero. t.Setenv("ECS_HEARTBEAT_INTERVAL", "not-a-duration") - if cfg := loadConfig(filepath.Join(t.TempDir(), "absent.env")); cfg.Heartbeat != defaultHeartbeat { + if cfg := LoadConfig(filepath.Join(t.TempDir(), "absent.env")); cfg.Heartbeat != defaultHeartbeat { t.Errorf("Heartbeat = %v, want default on bad value", cfg.Heartbeat) } } diff --git a/cmd/ecs-agent/controlplane.go b/spinifex/agents/ecs/agent/controlplane.go similarity index 98% rename from cmd/ecs-agent/controlplane.go rename to spinifex/agents/ecs/agent/controlplane.go index 721e2c7b0..4f21c074d 100644 --- a/cmd/ecs-agent/controlplane.go +++ b/spinifex/agents/ecs/agent/controlplane.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -47,7 +47,7 @@ const pollTimeout = 30 * time.Second // newGatewayControlPlane builds the SigV4 client signing with the instance-role // credentials from IMDS (fetched per call so rotation is transparent) + pinned CA. -func newGatewayControlPlane(cfg config, creds credentials.CredentialsProvider) (*gatewayControlPlane, error) { +func newGatewayControlPlane(cfg Config, creds credentials.CredentialsProvider) (*gatewayControlPlane, error) { client, err := ecsgw.New(cfg.GatewayURL, cfg.GatewayCA, credentialsFunc(creds), cfg.Region, pollTimeout) if err != nil { return nil, err diff --git a/cmd/ecs-agent/controlplane_test.go b/spinifex/agents/ecs/agent/controlplane_test.go similarity index 98% rename from cmd/ecs-agent/controlplane_test.go rename to spinifex/agents/ecs/agent/controlplane_test.go index 2254701c5..6c3d7903f 100644 --- a/cmd/ecs-agent/controlplane_test.go +++ b/spinifex/agents/ecs/agent/controlplane_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -23,7 +23,7 @@ func (fixedCredsProvider) Retrieve(context.Context) (credentials.Credentials, er func newTestGatewayControlPlane(t *testing.T, srv *httptest.Server) *gatewayControlPlane { t.Helper() - cp, err := newGatewayControlPlane(config{GatewayURL: srv.URL, Region: "us-east-1"}, fixedCredsProvider{}) + cp, err := newGatewayControlPlane(Config{GatewayURL: srv.URL, Region: "us-east-1"}, fixedCredsProvider{}) if err != nil { t.Fatalf("newGatewayControlPlane: %v", err) } diff --git a/cmd/ecs-agent/credendpoint.go b/spinifex/agents/ecs/agent/credendpoint.go similarity index 99% rename from cmd/ecs-agent/credendpoint.go rename to spinifex/agents/ecs/agent/credendpoint.go index 57bebd4a4..c7ffc0033 100644 --- a/cmd/ecs-agent/credendpoint.go +++ b/spinifex/agents/ecs/agent/credendpoint.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/credendpoint_test.go b/spinifex/agents/ecs/agent/credendpoint_test.go similarity index 99% rename from cmd/ecs-agent/credendpoint_test.go rename to spinifex/agents/ecs/agent/credendpoint_test.go index 0266816d6..4be910ddf 100644 --- a/cmd/ecs-agent/credendpoint_test.go +++ b/spinifex/agents/ecs/agent/credendpoint_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/credinject_test.go b/spinifex/agents/ecs/agent/credinject_test.go similarity index 99% rename from cmd/ecs-agent/credinject_test.go rename to spinifex/agents/ecs/agent/credinject_test.go index b69dc1867..e34b20c1a 100644 --- a/cmd/ecs-agent/credinject_test.go +++ b/spinifex/agents/ecs/agent/credinject_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "testing" diff --git a/spinifex/agents/ecs/agent/doc.go b/spinifex/agents/ecs/agent/doc.go new file mode 100644 index 000000000..c32fccc3a --- /dev/null +++ b/spinifex/agents/ecs/agent/doc.go @@ -0,0 +1,12 @@ +// Package agent implements ecs-agent, which runs inside a Spinifex ECS +// container instance (a guest VM booted from the ECS-AMI, with containerd baked +// in). It registers the host with the ECS scheduler through the AWS gateway over +// TLS+SigV4 (never NATS), heartbeats by re-registering, polls the gateway for +// task assignments, runs them through containerd, and reports state back over +// the gateway. +// +// Static config (gateway URL, CA, region, cluster, seeded IAM creds, containerd +// socket) is read from the cloud-init env file /etc/spinifex-ecs/agent.env +// (KEY=value); real env vars override it. Host identity (account, instance, AZ) +// comes from IMDS at boot. +package agent diff --git a/cmd/ecs-agent/ecrresolver.go b/spinifex/agents/ecs/agent/ecrresolver.go similarity index 99% rename from cmd/ecs-agent/ecrresolver.go rename to spinifex/agents/ecs/agent/ecrresolver.go index 6b29d8f83..be76540ab 100644 --- a/cmd/ecs-agent/ecrresolver.go +++ b/spinifex/agents/ecs/agent/ecrresolver.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/ecrresolver_test.go b/spinifex/agents/ecs/agent/ecrresolver_test.go similarity index 99% rename from cmd/ecs-agent/ecrresolver_test.go rename to spinifex/agents/ecs/agent/ecrresolver_test.go index 4ad2ab4ca..1406eaa47 100644 --- a/cmd/ecs-agent/ecrresolver_test.go +++ b/spinifex/agents/ecs/agent/ecrresolver_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/gpu_discovery.go b/spinifex/agents/ecs/agent/gpu_discovery.go similarity index 98% rename from cmd/ecs-agent/gpu_discovery.go rename to spinifex/agents/ecs/agent/gpu_discovery.go index 947e52742..3d36e02f9 100644 --- a/cmd/ecs-agent/gpu_discovery.go +++ b/spinifex/agents/ecs/agent/gpu_discovery.go @@ -1,4 +1,4 @@ -package main +package agent import ( "os/exec" diff --git a/cmd/ecs-agent/gpu_discovery_test.go b/spinifex/agents/ecs/agent/gpu_discovery_test.go similarity index 99% rename from cmd/ecs-agent/gpu_discovery_test.go rename to spinifex/agents/ecs/agent/gpu_discovery_test.go index d5f5651c0..d37db7b9a 100644 --- a/cmd/ecs-agent/gpu_discovery_test.go +++ b/spinifex/agents/ecs/agent/gpu_discovery_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "errors" diff --git a/cmd/ecs-agent/gpu_ledger.go b/spinifex/agents/ecs/agent/gpu_ledger.go similarity index 99% rename from cmd/ecs-agent/gpu_ledger.go rename to spinifex/agents/ecs/agent/gpu_ledger.go index b1e55e87e..40e51a66b 100644 --- a/cmd/ecs-agent/gpu_ledger.go +++ b/spinifex/agents/ecs/agent/gpu_ledger.go @@ -1,4 +1,4 @@ -package main +package agent import ( "fmt" diff --git a/cmd/ecs-agent/gpu_ledger_test.go b/spinifex/agents/ecs/agent/gpu_ledger_test.go similarity index 99% rename from cmd/ecs-agent/gpu_ledger_test.go rename to spinifex/agents/ecs/agent/gpu_ledger_test.go index 92a064b8b..0af88c5e3 100644 --- a/cmd/ecs-agent/gpu_ledger_test.go +++ b/spinifex/agents/ecs/agent/gpu_ledger_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "sync" diff --git a/cmd/ecs-agent/heartbeat.go b/spinifex/agents/ecs/agent/heartbeat.go similarity index 99% rename from cmd/ecs-agent/heartbeat.go rename to spinifex/agents/ecs/agent/heartbeat.go index e2956a9b0..75e4e07a5 100644 --- a/cmd/ecs-agent/heartbeat.go +++ b/spinifex/agents/ecs/agent/heartbeat.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/metadata.go b/spinifex/agents/ecs/agent/metadata.go similarity index 99% rename from cmd/ecs-agent/metadata.go rename to spinifex/agents/ecs/agent/metadata.go index 6d75e0b51..1b2d3c48e 100644 --- a/cmd/ecs-agent/metadata.go +++ b/spinifex/agents/ecs/agent/metadata.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/metadata_test.go b/spinifex/agents/ecs/agent/metadata_test.go similarity index 99% rename from cmd/ecs-agent/metadata_test.go rename to spinifex/agents/ecs/agent/metadata_test.go index 30a2796a4..44bfcfa1a 100644 --- a/cmd/ecs-agent/metadata_test.go +++ b/spinifex/agents/ecs/agent/metadata_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "net/http" diff --git a/cmd/ecs-agent/netns.go b/spinifex/agents/ecs/agent/netns.go similarity index 99% rename from cmd/ecs-agent/netns.go rename to spinifex/agents/ecs/agent/netns.go index 3a431bfcf..9e28cc8b2 100644 --- a/cmd/ecs-agent/netns.go +++ b/spinifex/agents/ecs/agent/netns.go @@ -1,4 +1,4 @@ -package main +package agent import ( "fmt" diff --git a/cmd/ecs-agent/netns_test.go b/spinifex/agents/ecs/agent/netns_test.go similarity index 99% rename from cmd/ecs-agent/netns_test.go rename to spinifex/agents/ecs/agent/netns_test.go index df9a526f6..ac45a70fc 100644 --- a/cmd/ecs-agent/netns_test.go +++ b/spinifex/agents/ecs/agent/netns_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "errors" diff --git a/cmd/ecs-agent/reconcile.go b/spinifex/agents/ecs/agent/reconcile.go similarity index 99% rename from cmd/ecs-agent/reconcile.go rename to spinifex/agents/ecs/agent/reconcile.go index 45bf5f488..87280631d 100644 --- a/cmd/ecs-agent/reconcile.go +++ b/spinifex/agents/ecs/agent/reconcile.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/reconcile_test.go b/spinifex/agents/ecs/agent/reconcile_test.go similarity index 95% rename from cmd/ecs-agent/reconcile_test.go rename to spinifex/agents/ecs/agent/reconcile_test.go index ab3e20349..e2bb43555 100644 --- a/cmd/ecs-agent/reconcile_test.go +++ b/spinifex/agents/ecs/agent/reconcile_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -55,7 +55,7 @@ func TestReconcile_AdoptsRunningLabeledContainer(t *testing.T) { adoptedContainer("t-001", "web", "default", "t-001", testTaskRole, "52:54:00:de:ad:01", true), }, } - a := newAgent(config{}, testIdentity(), cp, rt, nil) + a := newAgent(Config{}, testIdentity(), cp, rt, nil) a.cred = newCredEndpoint(nil, "us-east-1", "https://gw", "", "127.0.0.1", 0, nil) adopted := a.reconcile(context.Background()) @@ -84,7 +84,7 @@ func TestReconcile_ReportsTaskWhoseContainerDied(t *testing.T) { adoptedContainer("t-dead", "web", "default", "t-dead", testTaskRole, "52:54:00:de:ad:02", false), }, } - a := newAgent(config{}, testIdentity(), cp, rt, nil) + a := newAgent(Config{}, testIdentity(), cp, rt, nil) a.cred = newCredEndpoint(nil, "us-east-1", "https://gw", "", "127.0.0.1", 0, nil) adopted := a.reconcile(context.Background()) @@ -116,7 +116,7 @@ func TestReconcile_PartiallyDeadTaskStaysRunning(t *testing.T) { adoptedContainer("t-mixed", "sidecar", "default", "t-mixed", testTaskRole, "52:54:00:de:ad:03", false), }, } - a := newAgent(config{}, testIdentity(), cp, rt, nil) + a := newAgent(Config{}, testIdentity(), cp, rt, nil) a.cred = newCredEndpoint(nil, "us-east-1", "https://gw", "", "127.0.0.1", 0, nil) a.reconcile(context.Background()) @@ -145,7 +145,7 @@ func TestPollAssignments_SeededTaskNotRerun(t *testing.T) { nil, }} rt := &ctrruntime.FakePuller{WaitErr: errors.New("blocked")} - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) go a.pollAssignments(ctx, map[string]bool{"t-001": true}) @@ -197,7 +197,7 @@ func TestReconcile_IgnoresOtherClusterContainers(t *testing.T) { adoptedContainer("t-ok", "web", "default", "", "", "", true), }, } - a := newAgent(config{}, testIdentity(), cp, rt, nil) + a := newAgent(Config{}, testIdentity(), cp, rt, nil) adopted := a.reconcile(context.Background()) @@ -213,13 +213,13 @@ func TestReconcile_IgnoresOtherClusterContainers(t *testing.T) { // A nil runner or a List error degrades to an empty set without panicking. func TestReconcile_DegradesGracefully(t *testing.T) { - noRunner := newAgent(config{}, testIdentity(), &fakeCP{}, nil, nil) + noRunner := newAgent(Config{}, testIdentity(), &fakeCP{}, nil, nil) if got := noRunner.reconcile(context.Background()); len(got) != 0 { t.Errorf("nil runner: adopted = %v, want empty", got) } rt := &ctrruntime.FakePuller{ListErr: errors.New("boom")} - listErr := newAgent(config{}, testIdentity(), &fakeCP{}, rt, nil) + listErr := newAgent(Config{}, testIdentity(), &fakeCP{}, rt, nil) if got := listErr.reconcile(context.Background()); len(got) != 0 { t.Errorf("list error: adopted = %v, want empty", got) } diff --git a/cmd/ecs-agent/registration.go b/spinifex/agents/ecs/agent/registration.go similarity index 99% rename from cmd/ecs-agent/registration.go rename to spinifex/agents/ecs/agent/registration.go index 8c8cab11c..fb6d41760 100644 --- a/cmd/ecs-agent/registration.go +++ b/spinifex/agents/ecs/agent/registration.go @@ -1,4 +1,4 @@ -package main +package agent import ( "fmt" diff --git a/cmd/ecs-agent/registration_test.go b/spinifex/agents/ecs/agent/registration_test.go similarity index 99% rename from cmd/ecs-agent/registration_test.go rename to spinifex/agents/ecs/agent/registration_test.go index 2135061b0..96c63ca1a 100644 --- a/cmd/ecs-agent/registration_test.go +++ b/spinifex/agents/ecs/agent/registration_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/runtimegate.go b/spinifex/agents/ecs/agent/runtimegate.go similarity index 99% rename from cmd/ecs-agent/runtimegate.go rename to spinifex/agents/ecs/agent/runtimegate.go index 33fcc1570..dcb5a5524 100644 --- a/cmd/ecs-agent/runtimegate.go +++ b/spinifex/agents/ecs/agent/runtimegate.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/runtimegate_test.go b/spinifex/agents/ecs/agent/runtimegate_test.go similarity index 99% rename from cmd/ecs-agent/runtimegate_test.go rename to spinifex/agents/ecs/agent/runtimegate_test.go index d6c9b4764..19e5b9d0e 100644 --- a/cmd/ecs-agent/runtimegate_test.go +++ b/spinifex/agents/ecs/agent/runtimegate_test.go @@ -1,6 +1,6 @@ //test:in-package — package main cannot be imported by an external test package. -package main +package agent import ( "context" diff --git a/cmd/ecs-agent/taskrun.go b/spinifex/agents/ecs/agent/taskrun.go similarity index 99% rename from cmd/ecs-agent/taskrun.go rename to spinifex/agents/ecs/agent/taskrun.go index bf435ca5c..205bfd7a3 100644 --- a/cmd/ecs-agent/taskrun.go +++ b/spinifex/agents/ecs/agent/taskrun.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -440,7 +440,7 @@ const ( labelTaskID = "mulga.ecs.taskID" labelContainerName = "mulga.ecs.containerName" labelClusterName = "mulga.ecs.clusterName" - labelCredID = "mulga.ecs.credID" + labelCredID = "mulga.ecs.credID" //nolint:gosec // G101: label key name, not a credential value. labelTaskRoleARN = "mulga.ecs.taskRoleArn" labelENIMac = "mulga.ecs.eniMac" // labelStopTimeout carries a container's stopTimeout (seconds) so stopTask diff --git a/cmd/ecs-agent/taskrun_test.go b/spinifex/agents/ecs/agent/taskrun_test.go similarity index 97% rename from cmd/ecs-agent/taskrun_test.go rename to spinifex/agents/ecs/agent/taskrun_test.go index aac92fc05..12ce3ddd2 100644 --- a/cmd/ecs-agent/taskrun_test.go +++ b/spinifex/agents/ecs/agent/taskrun_test.go @@ -1,4 +1,4 @@ -package main +package agent import ( "context" @@ -25,13 +25,13 @@ func testAssign() *bus.Assign { } func newRunAgent(cp controlPlane, rt *ctrruntime.FakePuller) *Agent { - return newAgent(config{}, testIdentity(), cp, rt, nil) + return newAgent(Config{}, testIdentity(), cp, rt, nil) } // runTask with no runtime reports the task STOPPED instead of crashing. func TestRunTask_NoRuntimeReportsStopped(t *testing.T) { cp := &fakeCP{} - a := newAgent(config{}, testIdentity(), cp, nil, nil) + a := newAgent(Config{}, testIdentity(), cp, nil, nil) a.runTask(context.Background(), testAssign()) st := cp.taskStates() @@ -252,7 +252,7 @@ func TestRunTask_GPUCarriedToRunSpec(t *testing.T) { func TestRunTask_GPUPinsAndReportsUUIDs(t *testing.T) { cp := &fakeCP{} rt := &ctrruntime.FakePuller{WaitErr: errors.New("blocked")} - a := newAgent(config{}, testIdentityWithGPUs("GPU-aaa", "GPU-bbb"), cp, rt, nil) + a := newAgent(Config{}, testIdentityWithGPUs("GPU-aaa", "GPU-bbb"), cp, rt, nil) as := testAssign() as.Containers = append(as.Containers, bus.AssignContainer{ Name: "trainer", Image: "registry/trainer:1", GPU: 1, @@ -296,7 +296,7 @@ func TestRunTask_GPUPinsAndReportsUUIDs(t *testing.T) { func TestRunTask_GPUIDsReachRunSpec(t *testing.T) { cp := &fakeCP{} rt := &ctrruntime.FakePuller{WaitErr: errors.New("blocked")} - a := newAgent(config{}, testIdentityWithGPUs("GPU-aaa", "GPU-bbb"), cp, rt, nil) + a := newAgent(Config{}, testIdentityWithGPUs("GPU-aaa", "GPU-bbb"), cp, rt, nil) as := testAssign() as.Containers = append(as.Containers, bus.AssignContainer{ Name: "trainer", Image: "registry/trainer:1", GPU: 1, @@ -361,7 +361,7 @@ func TestStopTask_ReleasesGPUsBackToLedger(t *testing.T) { "mulga.ecs.taskID": "t-001", "mulga.ecs.containerName": "trainer", }}, }} - a := newAgent(config{}, testIdentityWithGPUs("GPU-aaa"), cp, rt, nil) + a := newAgent(Config{}, testIdentityWithGPUs("GPU-aaa"), cp, rt, nil) if _, err := a.gpu.Pin(gpuKey("t-001", "trainer"), 1); err != nil { t.Fatalf("seed pin: %v", err) } @@ -439,7 +439,7 @@ func TestPollAssignments_DispatchesOnceAndAcks(t *testing.T) { nil, // poll 3: gateway dropped it after the ack }} rt := &ctrruntime.FakePuller{WaitErr: errors.New("blocked")} - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) go a.pollAssignments(ctx, map[string]bool{}) @@ -538,7 +538,7 @@ func TestPollAssignments_StopReapsAndSuppressesAssign(t *testing.T) { rt := &ctrruntime.FakePuller{Containers: []ctrruntime.Container{ {ID: "t-001-web", Running: true, Labels: map[string]string{"mulga.ecs.taskID": "t-001"}}, }} - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) go a.pollAssignments(ctx, map[string]bool{}) @@ -593,7 +593,7 @@ func countStopAcks(acks [][]string, taskID string) int { func TestPollAssignments_UnperformableStopIsNotAckedAndRetried(t *testing.T) { cp := &fakeCP{stopPending: []bus.StopDirective{{TaskID: "t-001", Reason: "bye"}}} rt := &ctrruntime.FakePuller{ListErr: errors.New("containerd is down")} - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -628,7 +628,7 @@ func TestPollAssignments_RecoveredStopIsAckedExactlyOnce(t *testing.T) { {ID: "t-001-web", Running: true, Labels: map[string]string{"mulga.ecs.taskID": "t-001"}}, }, } - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -667,7 +667,7 @@ func TestPollAssignments_StoppedTaskIsNotRerunByRedeliveredAssign(t *testing.T) rt := &ctrruntime.FakePuller{Containers: []ctrruntime.Container{ {ID: "t-001-web", Running: true, Labels: map[string]string{"mulga.ecs.taskID": "t-001"}}, }} - a := newAgent(config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) + a := newAgent(Config{PollInterval: 5 * time.Millisecond}, testIdentity(), cp, rt, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -696,7 +696,7 @@ func TestPollAssignments_StoppedTaskIsNotRerunByRedeliveredAssign(t *testing.T) // stopTask with no runtime is a no-op: nothing to reap, no state reported. func TestStopTask_NilRunnerNoOp(t *testing.T) { cp := &fakeCP{} - a := newAgent(config{}, testIdentity(), cp, nil, nil) + a := newAgent(Config{}, testIdentity(), cp, nil, nil) a.stopTask(context.Background(), bus.StopDirective{TaskID: "t-001", Reason: "x"}) if len(cp.taskStates()) != 0 { t.Fatalf("nil runner should report no state, got %+v", cp.taskStates()) @@ -807,7 +807,7 @@ func (sentinelResolver) Authorize(context.Context, string) (string, string, stri // exec-role resolver is built. func TestPullResolver(t *testing.T) { fake := sentinelResolver{} - a := newAgent(config{Region: "us-east-1"}, testIdentity(), &fakeCP{}, nil, fake) + a := newAgent(Config{Region: "us-east-1"}, testIdentity(), &fakeCP{}, nil, fake) if got := a.pullResolver(&bus.Assign{TaskID: "t1"}); got != fake { t.Fatalf("no execution role: want instance resolver fallback") diff --git a/spinifex/agents/ecs/agent/version_test.go b/spinifex/agents/ecs/agent/version_test.go new file mode 100644 index 000000000..a8aad3c30 --- /dev/null +++ b/spinifex/agents/ecs/agent/version_test.go @@ -0,0 +1,26 @@ +package agent + +import ( + "path/filepath" + "testing" +) + +// The build version is injected into the binary's main package and handed in +// through Config; New must carry it into the identity the register message sends. +func TestNewReportsConfiguredAgentVersion(t *testing.T) { + srv := metadataStub(t, true) + a, err := New(Config{ + IMDSBase: srv.URL + "/latest", + GatewayURL: "https://127.0.0.1:1", + Region: "us-east-1", + ClusterName: "default", + ContainerdSocket: filepath.Join(t.TempDir(), "absent.sock"), + AgentVersion: "1.2.3-test", + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if a.id.AgentVersion != "1.2.3-test" { + t.Errorf("identity AgentVersion = %q, want the configured build version", a.id.AgentVersion) + } +}