apply: container and network resources, over the container runtime
Extends the applier past the filesystem to the two types the workloads need: a container and the private network it joins. The workloads are the bulk of what a cutover re-declares (research 009), so this is what makes a workload manifest actually appliable. - container: run/reconcile/remove over the runtime. Up to date means a container that is ours (a spec-hash label matches this exact declaration) AND running; anything else — a changed spec, a stopped container, or a foreign one the old control plane left by that name — is recreated into ours. Safe because a container carries no state: its data is in bind-mounted directories declared separately, and recreating it never touches them. Read-back asks the runtime whether it is actually running on the declared spec, because 'started' only means the runtime returned. - network: create if absent, adopt if present, remove only what it created. - The runtime is driven through a Runner, faked in unit tests and exercised for real in a smoke test that stands a container up, proves idempotency, and tears it down — skipped, never failed, where the runtime is absent. The store's per-resource reference generalises from a path to a ref: a path for files and directories, a name for containers and networks. Verified end to end through the binary: a container on a bind mount, then dropped from the declaration — the container is removed and the data directory survives, which is the migration property itself. Still deferred: sealed secrets, and package/service/archive/user/action — refused whole until built, never half-applied. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -53,12 +53,21 @@ type Resource struct {
|
||||
Fields map[string]json.RawMessage
|
||||
}
|
||||
|
||||
// Path is the host-owned filesystem path this resource lives at. Every type this host applies
|
||||
// so far is addressed by a path, and the store needs it to remove the resource later.
|
||||
// Path is the host-owned filesystem path a file or directory resource lives at.
|
||||
func (r Resource) Path() string {
|
||||
return r.stringField("path")
|
||||
}
|
||||
|
||||
// ref is what the store records to find this resource again for removal: a filesystem path for
|
||||
// files and directories, a name for containers and networks. Every resource is addressed by one
|
||||
// or the other, and the parser refuses a resource that has neither.
|
||||
func (r Resource) ref() string {
|
||||
if p := r.stringField("path"); p != "" {
|
||||
return p
|
||||
}
|
||||
return r.stringField("name")
|
||||
}
|
||||
|
||||
func (r Resource) stringField(key string) string {
|
||||
raw, ok := r.Fields[key]
|
||||
if !ok {
|
||||
@@ -71,6 +80,29 @@ func (r Resource) stringField(key string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// stringSlice reads a field that is a JSON array of strings — ports, volumes, args, hosts.
|
||||
func (r Resource) stringSlice(key string) []string {
|
||||
raw, ok := r.Fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// stringMap reads a field that is a JSON object of string→string — a container's env. Keys are
|
||||
// returned sorted by the caller when order matters, so a container's spec hash is stable.
|
||||
func (r Resource) stringMap(key string) map[string]string {
|
||||
raw, ok := r.Fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var out map[string]string
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// shape is the set of field keys a resource type may carry, beyond the common id and type.
|
||||
// This is the host's half of a wire contract whose other half is the control plane's catalogue
|
||||
// (novox/mesh-control examples/modules/modules_test.go). Duplicated deliberately, because the
|
||||
@@ -84,6 +116,8 @@ func (r Resource) stringField(key string) string {
|
||||
var shapes = map[string][]string{
|
||||
"directory": {"path", "mode", "owner"},
|
||||
"file": {"path", "content", "mode", "owner"},
|
||||
"network": {"name"},
|
||||
"container": {"name", "image", "env", "env-file", "ports", "volumes", "args", "hosts", "network"},
|
||||
}
|
||||
|
||||
// Parse reads a declaration and refuses anything it does not fully understand.
|
||||
@@ -167,11 +201,13 @@ func parseResource(raw json.RawMessage) (Resource, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, hasPath := extra["path"]; !hasPath {
|
||||
return Resource{}, fmt.Errorf("%q is a %s and names no path", id, typ)
|
||||
res := Resource{ID: id, Type: typ, Fields: extra}
|
||||
if res.ref() == "" {
|
||||
return Resource{}, fmt.Errorf(
|
||||
"%q is a %s and names neither a path nor a name — the store would have no way to find "+
|
||||
"it again", id, typ)
|
||||
}
|
||||
|
||||
return Resource{ID: id, Type: typ, Fields: extra}, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func decodeString(raw json.RawMessage) string {
|
||||
|
||||
Reference in New Issue
Block a user