A user, bytes, and an archive — because most of what people install is
not a service A shell, a terminal, a chat client, a desktop are a package plus configuration in somebody's home. A mesh with no notion of a user can own /etc and nothing anybody looks at, which is most of the reason to manage a machine at all. Three shapes, and the vocabulary test asserts the count precisely because widening it widens what a compromised control plane can express: user a login, its shell and its groups archive a set of files, fetched by digest and unpacked (file) gains `bytes` for what is not text, and `owner` `user` also makes "zsh is my login shell" declared state. chsh is a command, the link may not carry one, and a shell settable only by hand is a shell the mesh cannot manage. Groups are additive and never pruned — usermod without --append REPLACES them, which would silently remove every group that makes a login able to use the machine. A machine's own groups are not the mesh's to know about. The archive is the one place this host reaches out on its own; everywhere else it holds one outbound connection and fetches nothing. So it carries the discipline the bootstrap already uses for images: pinned by digest, and the digest checked before a single file is written. Two decisions in the unpacker worth naming: - an entry naming a path outside the archive is REFUSED, not sanitised. Rewriting it to land inside would put a file somewhere nobody asked for and report success. Found by the test: the first version quietly relocated it. - symlinks and device nodes are refused rather than skipped, or an archive that needed one arrives silently incomplete. A partial host does archives and refuses users: an archive needs a filesystem and a way to fetch; a user needs a user database it is allowed to write.
This commit is contained in:
@@ -13,6 +13,7 @@ package apply
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -168,6 +169,10 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
|
||||
return applyPackage(ctx, sys, res, run)
|
||||
case *declaration.Container:
|
||||
return applyContainer(ctx, res, run)
|
||||
case *declaration.User:
|
||||
return applyUser(ctx, sys, res, run)
|
||||
case *declaration.Archive:
|
||||
return applyArchive(ctx, res, previous)
|
||||
case *declaration.Action:
|
||||
return applyAction(ctx, res, run)
|
||||
default:
|
||||
@@ -234,9 +239,21 @@ func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
||||
return out, fmt.Errorf("%s is mode %o after setting %o", r.Path, after.Mode().Perm(), mode.Perm())
|
||||
}
|
||||
|
||||
ownedAlready, err := ownedBy(r.Path, r.Owner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if !ownedAlready {
|
||||
if err := own(r.Path, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
|
||||
out.Action = "unchanged"
|
||||
if !existed {
|
||||
out.Action = "created"
|
||||
} else if !ownedAlready {
|
||||
out.Action = "updated"
|
||||
} else if before.Mode().Perm() != mode.Perm() {
|
||||
out.Action = "updated"
|
||||
out.Detail = fmt.Sprintf("mode %o to %o", before.Mode().Perm(), mode.Perm())
|
||||
@@ -250,6 +267,16 @@ func applyFile(r *declaration.File, previous store.Applied, unseal Unseal) (Outc
|
||||
// What actually goes on disk. For a sealed file the mesh never had this, and neither did
|
||||
// whatever carried the declaration here.
|
||||
content := r.Content
|
||||
if r.Bytes != "" {
|
||||
// Not text. Decoded here rather than written as base64, because what a declaration says
|
||||
// is in a file has to be what ends up in it — a wallpaper stored as its own encoding is
|
||||
// a wallpaper nothing can open.
|
||||
decoded, err := base64.StdEncoding.DecodeString(r.Bytes)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("%s carries bytes that are not base64: %w", r.Path, err)
|
||||
}
|
||||
content = string(decoded)
|
||||
}
|
||||
// A secret written world-readable is a secret. The default differs from an ordinary file's
|
||||
// for that reason alone; an explicit mode still wins, because a module may need its own user
|
||||
// to read it and only the module knows which.
|
||||
@@ -324,6 +351,19 @@ func applyFile(r *declaration.File, previous store.Applied, unseal Unseal) (Outc
|
||||
return out, fmt.Errorf("%s is mode %o after setting %o", r.Path, info.Mode().Perm(), mode.Perm())
|
||||
}
|
||||
|
||||
// And who it belongs to. Checked before setting, so a file already owned correctly is not
|
||||
// reported as changed on every apply — which would make every reconcile look like work.
|
||||
ownedAlready, err := ownedBy(r.Path, r.Owner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if !ownedAlready {
|
||||
if err := own(r.Path, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
contentSame = false
|
||||
}
|
||||
|
||||
switch {
|
||||
case !existed:
|
||||
out.Action = "created"
|
||||
|
||||
Reference in New Issue
Block a user