feat(agent): adopt what the command seeded, and leave it until then (MK8S-434) - #26
ezekiel-alexrod wants to merge 18 commits into
Conversation
The agent and the command ran the same steps in the same order: check the cache path, pull the image, close the stream on the way out, extract it. They differed only in how they reported. The next changes touch what an extraction records, and two copies would have had to move together. Both now call internal/fill. Two small differences fall out of it. The agent refuses a cache path that is a regular file, as the command did, instead of failing later on the extraction. And it logs a failure to close the stream only once the extraction went through, as the command did: before that, the error it returns already says what happened. Relates-to: MK8S-434
…lling The digest a puller returned was the manifest's, and the manifest depends on how the image is stored: the same image read from a registry and from a docker archive has two. The digest of the image's configuration does not change with the transport or the registry endpoint, so it is the one two sources can be compared by. Both pullers now report both. Resolve returns them without reading a layer. For a registry that is the manifest alone, a few kilobytes, which is what the agent needs to tell whether a directory seeded from somewhere else holds the image a resource names. Relates-to: MK8S-434
The sentinel listed the files and a digest nothing read back. It now also records the owner, the source the content was read from, and the digest of the image's configuration. The agent writes image-cache-agent as owner and the command imagecachectl, so that the agent can tell a directory it wrote from one seeded before it arrived. The configuration digest is what the agent will compare against the image a resource names. The source is kept for whoever looks at the directory, not for that comparison: the same image is named by an archive path at install and by a registry reference in the resource. A sentinel written before this has none of the three fields and reads as before; an empty owner is the agent's. Relates-to: MK8S-434
Garbage collection removed every sentinel-bearing directory no resource claimed on the node, and it runs on every pass, the first one included. At install the agent can land before the resources that name what the command seeded, and it collected that cache and pulled it back from the registry: about a gigabyte, with the node holding none of the images in between. A directory whose sentinel names another owner is now left alone, whatever claims it or not. An empty owner still means the agent, so the directories every existing cluster holds are collected as before, and a sentinel that does not parse is still the agent's. Relates-to: MK8S-434
A resource that claims a directory the command seeded now checks what the directory holds before counting it as synced. The agent resolves the resource's source, which reads the manifest and the configuration and never a layer, and compares the configuration digest with the one the command recorded: - the same image: the agent takes the directory over, rewriting only the owner in the sentinel, and pulls nothing; - another image: the directory is replaced like any other that does not hold what its resource asks for; - the source cannot be resolved: the directory is left as it is, the resource stays pending, and the next pass tries again. The configuration digest is what identifies the content because it is the one identity a docker archive and a registry agree on. Comparing the source string would fail on the very first node, seeded from an archive path and claimed through a registry reference, and again whenever the registry endpoint changes. The manifest digest differs between an archive and a pushed image too. The sentinel is rewritten through a temporary file renamed over it, so a crash leaves the old owner or the new one, never a half written sentinel that would read as an incomplete directory to pull again. Relates-to: MK8S-434
The README warned that nothing checked a seeded directory against the resource that claims it, and the design said garbage collection removed a directory the command wrote under any other name. Neither holds now: the sentinel names its writer, garbage collection leaves the command's directories alone, and the agent compares configuration digests before taking one over. agent/DESIGN.md gains a section on adoption, with the reason the configuration digest identifies the content rather than the source string or the manifest digest. Relates-to: MK8S-434
The two log messages start with a capital letter, as agent/AGENTS.md asks, and the Adopted event names the resource and the writer apart: "adopted the directory imagecachectl seeded" read as if imagecachectl were the directory. Relates-to: MK8S-434
Three comments and two sentences of agent/DESIGN.md still said a sentinel marks a directory as the agent's, that replaceable() applies the same rule as garbage collection, and that a complete directory is never pulled again. The sentinel now names its writer: collection leaves another writer's directory alone, the swap still replaces any directory the store wrote under the resource's own name, and a seeded directory is checked once before the agent takes it over. Relates-to: MK8S-434
…nt's The design's introduction to the sentinel still called it the mark of an agent-owned directory, and the README called it the agent's, while the list right under the first one and the rest of the README explain that the command writes it too and that it names who did. Relates-to: MK8S-434
| if _, err := os.Stat(filepath.Join(cachePath, e.Name(), sentinelName)); err != nil { | ||
| continue | ||
| } | ||
| if !stale && !s.agentOwned(filepath.Join(cachePath, e.Name())) { |
There was a problem hiding this comment.
Something strikes me: Does it mean that files put by anyone are left as is ?
There was a problem hiding this comment.
Yes, on purpose. The cache path is shared, so the GC only removes what the agent wrote: a directory whose sentinel names the agent, or one of its own temporaries. A flat file or a directory from someone else is never touched.
Note that the preload service still imports any *.tar it finds there, that part doesn't change.
There was a problem hiding this comment.
To be discussed with @TeddyAndrieux I believe
…-434-sentinel-owner Brings in the review fixes of #25. The cancellation test they add passes a record to Extract, which takes one on this branch. Relates-to: MK8S-434
…digest The agent adopted a seeded directory when the configuration digest the command recorded matched the one the resource's source resolves to. That digest hashes the configuration's bytes. A conversion from the Docker manifest format to OCI writes the configuration out again in another key order: same fields, same values, another digest. On a kind cluster serving the carrier through the registry stack, an ISO archive saved from the Docker daemon never matched the image the registry served, and every seeded directory was pulled again. The sentinel now records the layers' diff IDs, the digests of the uncompressed layers, and adoption compares them in order. They are what the directory holds and they do not depend on how the image is stored. Resolving still reads the manifest and the configuration, never a layer. TestBothPullersAgreeOnTheLayers serves an image from a registry and the same image, its configuration in another key order, from an archive. Compared by configuration digest the two read b7b1... and a6d6... Relates-to: MK8S-434
imagecachectl runs as root and leaves each resource directory owned by root, in mode 0700. The init container only chowned the cache root, so the agent, running as UID 65532, could not read the sentinel of a seeded directory, let alone rewrite it to adopt the directory or remove it to replace the content. The init container now also chowns the directories that hold the store's sentinel, and the hidden temporaries an interrupted extraction leaves, one level down and not recursively: the agent only has to write in the directory itself. It skips symbolic links, and nothing else under the shared path changes. The e2e suite seeds, as root on the kind node before the agent deploys, a directory with a sentinel, a temporary holding a file, and a directory without a sentinel. It checks that only the first changed hands and that the agent collects the temporary, which it cannot do without write access to it: with the temporaries left out of the chown, the spec times out. Relates-to: MK8S-434
…mage The store checked whether it could replace a resource's directory at the swap only, after the whole image had been pulled and extracted. A directory the agent could not take over was therefore pulled again on every pass: on a kind cluster, with a seeded directory the agent could not read, about 950 layer pulls in a minute and a half. Fill now asks the store first, through Store.Replaceable, and pulls nothing when the answer is no. Extract still checks at the swap. A sentinel that cannot be read is also reported as such, with its cause, instead of as a directory the store did not write: the refusal then points at who owns the directory, not at what it holds. Relates-to: MK8S-434
The store never uses it: to garbage collection, any owner other than the agent is foreign. Only the command writes it, so it lives in internal/cli, and the store tests use a writer of their own. Relates-to: MK8S-434
The sentinel held the same four fields as Record, plus the files, and three places copied them one by one. It now embeds Record, which carries the JSON names. The file keeps the same flat keys. Relates-to: MK8S-434
Store.Record and the Record type read the same at a call site. The method reads the sentinel from disk, which its name now says. Relates-to: MK8S-434
An empty owner meant the agent, so that sentinels written before owners were recorded stayed collectable. None exists: the agent was never released, and the command only now writes sentinels. The rule and the test that kept it go, and only a sentinel naming the agent is the agent's. One that does not parse is still collected, since it is damaged. Relates-to: MK8S-434
…-434-sentinel-owner Brings in cobra for the command line, and the design introduction fix of #25. cli.go keeps this branch's Owner next to the new help text. Relates-to: MK8S-434
| command: | ||
| - sh | ||
| - -c | ||
| - | | ||
| cache=/var/lib/image-cache | ||
| chown 65532:65532 "$cache" || exit 1 | ||
| for d in "$cache"/*/; do | ||
| [ -L "${d%/}" ] && continue | ||
| [ -f "$d.image-cache-agent.json" ] || continue | ||
| chown 65532:65532 "$d" || exit 1 | ||
| done | ||
| for d in "$cache"/.*.tmp-*/; do | ||
| [ -L "${d%/}" ] && continue | ||
| [ -d "$d" ] || continue | ||
| chown 65532:65532 "$d" || exit 1 | ||
| done |
There was a problem hiding this comment.
Why not using:
"chown -R 65532:65532 /var/lib/image-cache"
There was a problem hiding this comment.
To discuss with @TeddyAndrieux: is /var/lib/image-cache a directory only for imagecachectl and the agent? If not, to whom else?
If yes, chown -R 65532:65532 /var/lib/image-cache is easier
Component
agent, config, docs
Problem
Stacked on #25. The agent's GC removes every directory with a sentinel that no
ImageCacheon the node claims, on every pass, the first one included. At install the DaemonSet can land before the resources, so it deletes whatimagecachectljust seeded and pulls the same gigabyte again.Also, once a resource claims the directory, nothing checks that it holds the image the resource asks for.
Fix
The sentinel now records who wrote the directory (
owner), where it was read from (source) and the diff IDs of the image layers (layers), next to the manifest digest.pendinguntil the agent checks the content. The agent resolvesspec.source, which reads the manifest and the config, never a layer (TestRemoteResolveReadsNoLayer). Same layers: it rewrites the owner and labels the nodesyncedwith no pull. Other layers: it replaces the directory. Source unreachable: it leaves the directory alone and retries on the next pass.TestBothPullersAgreeOnTheLayersreproduces it.imagecachectlruns as root and leaves root-owned 0700 directories, so the agent (UID 65532) couldn't read them. Thechown-cacheinit container now also hands over the directories with a sentinel and the hidden temporaries, one level down, not recursively. Nothing else under the cache path changes.internal/fill, called by both the command and the agent. Its own package, socachekeeps no registry code.sentinelembedsRecord, so the four recorded fields are written once. The file keeps the same flat keys.Test
make -C agent testandlintare green, and every commit builds and passes on its own.make -C agent test-e2eon kind seeds as root, before the agent deploys, a directory with a sentinel, a temporary with a file in it, and a directory without a sentinel. Only the first changes hands and the agent collects the temporary --> OK. Without the chown of the temporaries, the spec times out.Out of scope
README.md,agent/README.md,DESIGN.md,agent/DESIGN.md,CONTRIBUTING.md, whichever owns it.agent/DESIGN.md. The preload service only globs*.tarand never reads the sentinel, so onlyagent/DESIGN.mdchanges.Relates-to: MK8S-434