mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
1ab61b0dd5
* fix(ecs): enforce or refuse container runtime fields (TFC-0040) user, mountPoints, volumesFrom, readonlyRootFilesystem, privileged, pseudoTerminal, interactive, linuxParameters, systemControls, startTimeout and stopTimeout registered successfully but were never stored, sent to the agent, enforced, or echoed back on describe. Store and echo the fields the agent can enforce (user, readonly root filesystem, privileged, pseudo terminal, interactive, systemControls, capability add/drop, start/stop timeout), apply them through containerd via OCI spec options and a graceful Stop, and refuse at RegisterTaskDefinition the fields the agent cannot honor (mountPoints, volumesFrom, linuxParameters.devices/initProcessEnabled/ sharedMemorySize/tmpfs). Refusal only triggers on a value that asks for the restriction; empty lists and false/unset flags are accepted and echoed as before. * test: move the bus wire-contract test to an external package * fix(ecs-agent): release an interactive container's stdin so its copier exits * fix(ecs): return a caller-supplied empty collection instead of omitting it
125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
containerd "github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/core/remotes"
|
|
"github.com/containerd/containerd/v2/core/remotes/docker"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
"github.com/distribution/reference"
|
|
)
|
|
|
|
// ecsNamespace isolates ecs-agent images/containers in containerd, mirroring the
|
|
// "moby"/"k8s.io" convention other runtimes use.
|
|
const ecsNamespace = "ecs"
|
|
|
|
// servingProbeTimeout bounds the boot-time daemon liveness check.
|
|
const servingProbeTimeout = 3 * time.Second
|
|
|
|
// containerdPuller drives a real containerd daemon over its unix socket.
|
|
type containerdPuller struct {
|
|
client *containerd.Client
|
|
|
|
// stdinDone holds one channel per interactive container, closed when that
|
|
// container is stopped or removed so its stdin reader returns EOF.
|
|
mu sync.Mutex
|
|
stdinDone map[string]chan struct{}
|
|
}
|
|
|
|
var _ ImagePuller = (*containerdPuller)(nil)
|
|
|
|
// New connects to the containerd daemon at socket (e.g.
|
|
// "/run/containerd/containerd.sock") and returns an ImagePuller. The containerd
|
|
// client dials lazily, so New actively probes the daemon with IsServing and
|
|
// returns an error if it is unreachable — otherwise a dead socket would only
|
|
// surface on the first pull, and the agent's boot-time "containerd unavailable"
|
|
// log would never fire.
|
|
func New(socket string) (Runtime, error) {
|
|
client, err := containerd.New(socket, containerd.WithDefaultNamespace(ecsNamespace))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect containerd %s: %w", socket, err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), servingProbeTimeout)
|
|
defer cancel()
|
|
serving, err := client.IsServing(ctx)
|
|
if err != nil || !serving {
|
|
_ = client.Close()
|
|
return nil, fmt.Errorf("containerd %s not serving: %w", socket, err)
|
|
}
|
|
return &containerdPuller{client: client}, nil
|
|
}
|
|
|
|
// Pull resolves credentials via r, then pulls and unpacks the image.
|
|
func (p *containerdPuller) Pull(ctx context.Context, spec PullSpec, r Resolver) (Image, error) {
|
|
ctx = namespaces.WithNamespace(ctx, ecsNamespace)
|
|
|
|
ref, err := normalizeRef(spec.Ref)
|
|
if err != nil {
|
|
return Image{}, err
|
|
}
|
|
|
|
resolver, err := newDockerResolver(ctx, ref, r)
|
|
if err != nil {
|
|
return Image{}, err
|
|
}
|
|
|
|
img, err := p.client.Pull(ctx, ref,
|
|
containerd.WithPullUnpack,
|
|
containerd.WithResolver(resolver),
|
|
)
|
|
if err != nil {
|
|
return Image{}, fmt.Errorf("pull %s: %w", ref, err)
|
|
}
|
|
|
|
target := img.Target()
|
|
return Image{
|
|
Ref: ref,
|
|
Digest: target.Digest.String(),
|
|
Size: target.Size,
|
|
}, nil
|
|
}
|
|
|
|
// normalizeRef expands a task-definition image string into the fully-qualified
|
|
// reference containerd's resolver requires: a bare "nginx:alpine" becomes
|
|
// "docker.io/library/nginx:alpine" and an untagged ref gets ":latest". Without
|
|
// this, containerd parses the bare ref as "dummy://nginx:alpine" and rejects it.
|
|
func normalizeRef(ref string) (string, error) {
|
|
named, err := reference.ParseNormalizedNamed(ref)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse image ref %q: %w", ref, err)
|
|
}
|
|
return reference.TagNameOnly(named).String(), nil
|
|
}
|
|
|
|
// Close releases the containerd client connection.
|
|
func (p *containerdPuller) Close() error {
|
|
if p.client == nil {
|
|
return nil
|
|
}
|
|
return p.client.Close()
|
|
}
|
|
|
|
// newDockerResolver builds a containerd remote resolver whose per-host
|
|
// credentials come from r (the ECR token resolver). Credentials are fetched once
|
|
// for ref and reused for every host containerd asks about during the pull.
|
|
func newDockerResolver(ctx context.Context, ref string, r Resolver) (remotes.Resolver, error) {
|
|
creds := func(string) (string, string, error) { return "", "", nil }
|
|
if r != nil {
|
|
user, pass, _, err := r.Authorize(ctx, ref)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authorize %s: %w", ref, err)
|
|
}
|
|
creds = func(string) (string, string, error) { return user, pass, nil }
|
|
}
|
|
|
|
authorizer := docker.NewDockerAuthorizer(docker.WithAuthCreds(creds))
|
|
return docker.NewResolver(docker.ResolverOptions{
|
|
Hosts: docker.ConfigureDefaultRegistries(docker.WithAuthorizer(authorizer)),
|
|
}), nil
|
|
}
|