Repository navigation
kind: run node and runtime configuration like a deployed node - #472
Conversation
80a2144 to
0534b5a
Compare
0534b5a to
05b466f
Compare
05b466f to
cd605e3
Compare
nickdujay-pplx
left a comment
There was a problem hiding this comment.
Reviewed cd605e3 against #480. No additional blocker in this layer.
Checked the zero-sized warm pool against the allocation/readiness code, node-ctl's ownership of advertised capacity, the containerd v2 import and systemd-cgroup preservation checks, atomic checksum-verified shim installation, and the cache-format/loop-device safeguards. Exact-head chart renders, shell syntax and the existing node-image input/hash/compatibility check passed. All four Kind CI shards passed; Rust CodeQL is still running.
The image compatibility guard correctly requires recreating an older Kind cluster. A node-container restart now deliberately loses its local cache; reverting the script cannot recover that state, and manually staged seed artifacts must be staged again as documented. I did not independently repeat the Linux/KVM restart drill. Approval covers this PR's delta; #480's separate installer finding still needs resolving before the stack lands.
Kind already runs the production node layout; this makes its node and runtime settings match what deployed nodes run, so the paths those settings select are the ones Kind exercises. - The public-operation dispatcher's tick, claim limit and claim staleness and the snapshot-retention sweeper's minimum age are test hooks Kind set without needing them. The tick override made Kind pick up queued operations every second instead of on the release 25 ms tick. All four are dropped from the cluster-controller chart; the retention scenarios still shorten the minimum age for their own run. - node-manager runs with no warm network-slot pool (target, retained and minimum ready all 0), as deployed nodes do. With the binary defaults the pool kept every slot the node has (300) ready and bridged to the node's own namespace. Sandbox pods bring their own namespace, so each sandbox claim took one of those slots and rebuilt it for the pod, and the pool rebuilt it after release. Builders, whose build VMs allocate without a pod namespace, leased pooled slots. Now every slot is set up on allocation and torn down on release, and the pool reports healthy while empty. - The static network-slots capacity that bootstrap.sh and the Tiltfile seeded is removed: node-ctl already advertised node-manager's capacity over it, as it does on a deployed node. - The SPACE Kind node image carries a kubelet --config-dir drop-in with eventRecordQPS: 0 and failSwapOn: false, and registers the `space` runtime with the deployed CniConfDir option through a drop-in that kind's containerd config imports. The drop-in uses kind's version 2 format, so containerd migrates it with kind's own settings into the CRI v1 runtime plugin where a deployed node has it. A version 3 drop-in would raise the merged version and skip that migration, losing runc's systemd cgroup driver (kubelet would then run cgroupfs), base_runtime_spec and discard_unpacked_layers. The image build fails unless containerd's loaded config has the runtime, its options, the systemd driver and discard_unpacked_layers. containerd loads it on first start, so install-shim.sh no longer edits its config or restarts it; it installs the shim binary and checks its SHA-256 on the node. Both settings change the image tag, so clusters created without them are refused by check-kind-node-image.sh. - space-cache.service reformats the cache image on every boot of the node container, as deployed nodes reformat their instance-store cache, stays idempotent within a boot, and mounts it nosuid,noatime. The optional bootstrap-prod-seed.sh, whose templates live only on that cache, now re-stages a worker whose cache lost them, and says to re-run it after a node restart. The README's comparison with the EKS modules gains rows for what stays different: shim installation after cluster creation, the kubelet settings on the non-SPACE nodes, the test-hooks build (naming the reaper, template GC and prefetch hooks the charts set on every run), and the disk-admission reservation, whose chart comment no longer calls it a production setting.
cd605e3 to
ef9103f
Compare
Kind already runs the production node layout. This change makes its node and runtime configuration match what deployed nodes run, so a green Kind run covers the code paths those settings select.
What Kind now does like deployments
public_operation_dispatcher.rs). Create pickup in the suite fell from a 566 ms median to 14 ms (below).SPACE_NODEMGR_NETWORK_POOL_TARGET,_RETAIN_READYand_MIN_READYat0, as deployed nodes do.claim_allocationserves only node-bridged allocations from the pool, so sandbox slots were already set up on allocation and torn down on release. But the binary defaults filled the pool tomax(target, retained), which is every slot the node has (300), all bridged to the node (the fill loop inslot_pool/worker.rs). So each sandbox claim took one of those slots and rebuilt it for the pod (handoff_network_namespace), and the pool rebuilt it after release. Builders, whose build VMs allocate without a pod namespace (build-manager'snetns_path: None), leased pooled slots. Now builders set up slots on demand too, nothing is pre-built or rebuilt, and the pool reports healthy while empty (pool_status). Measured: 300 pool namespaces per worker before; after a full suite, none on any SPACE node.space.pplx.net/network-slots: 100seeding inbootstrap.shand the Tiltfile is removed.network_pool_max_slots(300), and its server-side-apply field manager owned the field. Node-ctl is now the only source, as on a deployed node, including after the drain scenarios delete and re-register a Node.--config-dirdrop-in witheventRecordQPS: 0andfailSwapOn: false.configz). The image tag changes, socheck-kind-node-image.shrefuses clusters created without it.spaceruntime with the deployedCniConfDiroption through a drop-in that the image's containerd config imports. The drop-in is written in the version 2 format of kind's ownconfig.toml, so containerd migrates both intoio.containerd.cri.v1.runtime, where a deployed node has the handler.install-shim.shno longer edits containerd's config or restarts it; it installs the binary and checks its SHA-256 on the node.joinPodCgroupjoins the pod's systemd slice, as on a deployed node. The image build checks the loaded config (containerd config dump) and fails unless it has thespaceruntime, its annotations andCniConfDir, runc'sSystemdCgroup = trueanddiscard_unpacked_layers = true. containerd is never restarted (NRestarts=0).space-cache.servicereformats the cache image on every boot of the node container, as deployed nodes reformat their instance-store cache. It stays idempotent within a boot (a/runmarker, as in11-nvme-cache.sh) and mountsnosuid,noatime.staged=1 unchanged=0), and node-ctl republishes. The opt-inbootstrap-prod-seed.shstages templates by hand onto that cache, so it now says to re-run it after a node restart. On a re-run it skips a worker only if the build is in that worker's catalog and also on its cache.The disk-admission comment no longer calls the allocation reservation a production setting; it is a test hook.
What stays different (README rows)
kind createin every bring-up path, which other open work is changing./space/cacherow (it outlives pod restarts but not node restarts), the node-layout section (capacity comes from node-ctl, no warm pool), and the node-layer section (format on every boot, re-runningbootstrap-prod-seed.sh, the kubelet drop-in, and the containerd drop-in with its version-2 format, plusdocker restartto start a cache clean).Validation
One fresh cluster from this PR's merge commit (
cc6c4ba: head023ea6donmainat0f1d3be; same tree as the head), on a Linux/KVM host with CI's settings (SPACE_KIND_TELEMETRY_MODE=otel,DATA_PLANE_URL, admin token file and cold-receipts directory as inkind-ci.yml). It ransmoke-kind-e2e.sh, thensmoke-kind-typed.sh allunsharded withSKIP_BOOTSTRAP=1, behind a health gate: Tilt quiet, no ReplicaSet younger than 180 s, every node published, then a 120 s settle. A new baseline run was not needed: the comparison columns are the previous round's baseline on the starting commit.SPACE_KIND_RUST_ARTIFACTS=prebuilt, imagesha256:ff4898a8…), as the previous round used.space-kind-node:abcf737f2e7c8172. The pod-spec environment of cluster-controller and both node-manager DaemonSets showed the chart change.containerd config dumphas thespaceruntime withCniConfDir, runc'sSystemdCgroup = trueandbase_runtime_spec, anddiscard_unpacked_layers = true. kubelet loggedUsing cgroup driver setting received from the CRI runtime cgroupDriver="systemd",crictl runtime-configreportsSYSTEMD, andconfigzshowscgroupDriver: systemd,eventRecordQPS: 0,failSwapOn: false. The shim loggedjoinPodCgroup: … → /sys/fs/cgroup/kubelet.slice/kubelet-kubepods.slice/…/cri-containerd-<id>.scope/space-runtime, which is the systemd-slice path. containerdNRestarts=0.main's, the previous head's, and this head's. kubelet tookcgroupDriver="systemd"onmain's and this head's image, andcgroupDriver="cgroupfs"on the previous head's. A pod's OCIcgroupsPathwas the slice form (kubelet-kubepods-besteffort-pod<uid>.slice:cri-containerd:<id>) on the first two and absolute (/kubelet/kubepods/besteffort/pod<uid>/<id>) on the previous head's. That absolute form takesjoinPodCgroup's absolute-path branch.023ea6d.80a2144is023ea6drebased onto newermain(9254f09). Two files this PR edits (the Kind README andbootstrap.sh) also changed onmain, in neighbouring lines; the rebase merged them without conflict, and the node image tag is unchanged. The force-push starts PR CI on it.bootstrap-prod-seed.shneeds an externally configured S3 source. Its new skip check was run on its own against the restarted worker: the seed's build directory there (templates/seed/sha256:<hash>) matches the glob.Every scenario passed.
local.ecw_template_build_from_oci_bootspassed for the first time on this host: its INFRA_ERROR in every earlier run was the builder's disk admission, and the harness now prunes superseded images from the node stores (#476).native.encrypted_double_restore_retires_prior_podsis new onmain.Every scenario (102)
ecw.access_idle_reconnect_preserves_workspaceecw.authenticated_access_with_public_traffic_disabledecw.background_process_controlecw.binary_file_round_tripecw.cold_create_environmentecw.command_round_tripecw.domain_access_survives_default_denyecw.filesystem_mutationsecw.filesystem_ownershipecw.filesystem_private_boundaryecw.filesystem_read_identityecw.filesystem_round_tripecw.filesystem_watcherecw.filesystem_write_identityecw.guest_vlan_frames_droppedecw.immediate_egressecw.internet_access_allowedecw.internet_access_deniedecw.large_stdout_integrityecw.loopback_route_stays_openecw.pause_connectecw.pause_connect_raceecw.pause_timeout_requires_connectecw.process_identityecw.process_reattachecw.public_port_to_all_interfacesecw.public_port_to_loopbackecw.refresh_keepaliveecw.sandbox_deleteecw.sandbox_infoecw.sandbox_list_paginationecw.timeout_rearmecw.timeout_updateecw.warm_pool_environmentecw.watcher_identitylocal.btrfs_send_restore_fails_closed_without_parent_sourcelocal.btrfs_send_restore_pulls_parent_templatelocal.btrfs_send_restore_rejects_corrupted_streamlocal.cold_sandbox_eviction_wake_round_triplocal.concurrent_restore_converges_to_one_executionlocal.deferred_btrfs_send_capture_restores_cross_nodelocal.ecw_template_build_from_oci_bootslocal.encrypted_resuspend_preserves_cowlocal.forced_drain_evacuates_sandboxlocal.gentle_drain_leaves_sandbox_residentlocal.node_ctl_fence_refuses_and_reconcileslocal.node_group_boundary_scheduleslocal.node_selector_extras_render_and_enforcelocal.orphaned_restore_pod_gc_reaps_only_supersededlocal.pending_backup_census_reports_blocked_uploadlocal.private_template_build_isolationlocal.quarantine_recovery_backs_offlocal.rate_limiters_enforce_and_fail_openlocal.retention_claim_finish_forwardlocal.retention_restore_source_guardlocal.sandbox_network_isolation_holdslocal.snapshot_reaper_reclaims_backed_fork_snapshotlocal.suspend_restore_cross_node_from_object_storagelocal.suspend_snapshot_rows_are_per_attemptlocal.telemetry_reaches_local_backendslocal.template_cache_lru_eviction_reclaimslocal.template_startup_recovery_decision_tablenetwork_policy.v1_cidr_allownetwork_policy.v1_cidr_denynetwork_policy.v1_deny_precedes_allownetwork_policy.v1_domain_bindingnetwork_policy.v1_exact_ip_allownetwork_policy.v1_exact_ip_denynetwork_policy.v1_fastly_shared_edge_bypassnetwork_policy.v1_platform_guardrail_precedes_allownetwork_policy.v1_rules_survive_restorenetwork_policy.v1_tcp_dns_transport_respects_ip_allownetwork_policy.v1_udp_deny_precedes_allownetwork_policy.v1_udp_respects_cidr_allownetwork_policy.v1_udp_respects_cidr_denynetwork_policy.v1_udp_respects_ip_allownetwork_policy.v1_udp_respects_ip_denynetwork_policy.v1_wildcard_domain_allowecw.background_process_controlecw.command_round_tripecw.pause_connectecw.sandbox_deleteecw.sandbox_infoecw.sandbox_list_paginationnative.encrypted_double_restore_retires_prior_podsnative.resident_pausenative.stopped_restart_preserves_rootfsnative_concurrency.command_idempotencynative_concurrency.create_idempotencynative_concurrency.independent_commandsnative_concurrency.lifecycle_contentionnative_concurrency.slow_readernative_contract.command_validationnative_contract.completed_delete_replaynative_contract.idempotency_conflictnative_contract.permanent_command_errornative_contract.tenant_isolationnative_contract.token_scopenative_contract.zero_vcpu_admissionnative_recovery.command_survives_data_plane_restartnative_recovery.queued_create_survives_api_restartnative_recovery.slow_operation_does_not_block_later_workNode restart recovery. After the suite, a native sandbox was created on
space-local-worker, written to and suspended. Thendocker restartwas run onspace-local-worker2:space-cachelogged a fresh format at boot, and template-manager pulledseedagain 23 s after the restart;staged=1 unchanged=0), and node-ctl republished the node as ready (0 ready slots,min_ready 0) with capacity 300;Event throttling. No throttling or dropped-event lines in either sandbox worker's kubelet journal after the suite and three bursts of 20 concurrent native creates.
Create latency (local Kind, medians). The previous round's branch column was measured while kind's runc settings were lost, with nodes on the cgroupfs driver. Those numbers are superseded by this round's:
public_operations, suite)space_sandbox_api_latency_milliseconds, suite)The gain over the baseline is consistent with the dispatcher tick, and it holds with the systemd cgroup driver. Concurrent bursts finished about 0.3–0.5 s slower than the superseded measurement.
No scenario failed because of this change, so there is no product fix to depend on.
Review follow-up
Restack onto #480: conflicts in the README bring-up paragraph and
bootstrap.sh, where #241's profile wording and unconditionalinstall-observability.shcall are kept beside this PR's shim text.install-shim.shmerged without conflict: it keeps #241's shim wrapper and telemetry environment, and nothing in Kind edits containerd's config or restarts containerd. The commit's content is otherwise unchanged.Earlier rounds:
discard_unpacked_layersas well as the runtime. Verified live and against the previous image.runtimeclass.yamland the README now say the node image registers the handler andinstall-shim.shonly installs the binary, never restarting containerd.bootstrap-prod-seed.shafter a node restart. Its skip check now also requires the build directory on the cache, so a re-run soon after a restart stages again.min(max, 100)slots. The fill loop fills tomax(target, retained), and the retained default ismax_slots, so it is 300, which matches the 300 namespaces measured on the baseline.Rollback
The node image tag changes, so an existing Kind cluster must be recreated to pick this up, and also to revert it: the image check refuses nodes from another image. A node-container restart now deliberately starts with an empty cache. Reverting cannot recover a cache that was already reset, and templates staged by hand with
bootstrap-prod-seed.shmust be staged again after a restart, as documented.🤖 Generated with Claude Code. The change and its validation were produced by an AI agent; a person reviews it before merge.