Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -181,7 +181,7 @@ func TestAnInhibitionTurnsIdleOffAndRestoresWhatWasThereWhenItEndsOrIsStopped(t
|
||||
|
||||
func TestTheDeclaredTimeoutsAreTheSessionStartsOwn(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
code := m.Shell[0].Code
|
||||
code := m.shell(t)[0].Code
|
||||
if !strings.Contains(code, "xset s "+strconv.Itoa(declaredLock)+" "+strconv.Itoa(declaredLock)+"\n") ||
|
||||
!strings.Contains(code, "xset dpms "+strconv.Itoa(declaredStandby)+" "+strconv.Itoa(declaredSuspend)+" "+strconv.Itoa(declaredOff)+"\n") {
|
||||
t.Fatalf("the tools' declared values and the session start's disagree:\n%s", code)
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
type manifest struct {
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Moved bool `json:"moved"`
|
||||
Items []item `json:"items"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Requires []string `json:"requires"`
|
||||
Claims []claim `json:"claims"`
|
||||
@@ -44,6 +46,53 @@ type shellCode struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// An item of a collection (novox/hq ADR 0349 clause 13): its text is the template file beside the manifest.
|
||||
type item struct {
|
||||
Collection string `json:"collection"`
|
||||
Name string `json:"name"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
// shell is the code the module adds to a shell's or the session's start, read from the templates of its items of
|
||||
// the collections shell:<file>:<slot>, in the order they are declared.
|
||||
func (m manifest) shell(t *testing.T) []shellCode {
|
||||
t.Helper()
|
||||
var out []shellCode
|
||||
for _, it := range m.Items {
|
||||
parts := strings.Split(it.Collection, ":")
|
||||
if len(parts) != 3 || parts[0] != "shell" {
|
||||
continue
|
||||
}
|
||||
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out = append(out, shellCode{For: parts[1], Slot: parts[2], Code: string(text)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// environmentOf is the variables the module sets, read from the templates of its items of the environment.
|
||||
func (m manifest) environmentOf(t *testing.T) *environment {
|
||||
t.Helper()
|
||||
var env *environment
|
||||
for _, it := range m.Items {
|
||||
if it.Collection != "environment" || it.Attributes["kind"] != "variable" {
|
||||
continue
|
||||
}
|
||||
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if env == nil {
|
||||
env = &environment{Variables: map[string]string{}}
|
||||
}
|
||||
env.Variables[it.Attributes["name"]] = strings.TrimSuffix(string(text), "\n")
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func readManifest(t *testing.T) manifest {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
||||
@@ -56,6 +105,7 @@ func readManifest(t *testing.T) manifest {
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
t.Fatalf("module.json: %v", err)
|
||||
}
|
||||
m.Environment = m.environmentOf(t)
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -83,26 +133,66 @@ func (m manifest) packages() (present, absent []string) {
|
||||
return present, absent
|
||||
}
|
||||
|
||||
// sameAsSource checks that a file resource's content is byte for byte the module's source file, so
|
||||
// the readable file in the repository is what the machine gets.
|
||||
func (m manifest) sameAsSource(t *testing.T, id, source string) {
|
||||
// fromTemplate checks that a file resource takes its text from its template file in the module's
|
||||
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
|
||||
// template is there. The readable file in the repository is the one place the machine's text is written.
|
||||
func (m manifest) fromTemplate(t *testing.T, id, template string) {
|
||||
t.Helper()
|
||||
want, err := os.ReadFile(filepath.Join("..", "..", source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := m.resource(t, id)
|
||||
if r["type"] != "file" {
|
||||
t.Fatalf("%s is a %v, not a file", id, r["type"])
|
||||
}
|
||||
if got, _ := r["content"].(string); got != string(want) {
|
||||
t.Fatalf("resource %s's content is not %s: edit the source and copy it into module.json", id, source)
|
||||
if r["template"] != template {
|
||||
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
|
||||
}
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
|
||||
t.Fatalf("resource %s names %s: %v", id, template, err)
|
||||
}
|
||||
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
|
||||
t.Fatalf("%s under the home is the account's", id)
|
||||
}
|
||||
}
|
||||
|
||||
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
|
||||
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
||||
func (m manifest) everyFileIsATemplate(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, r := range m.Resources {
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
||||
}
|
||||
}
|
||||
packs := 0
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a["kind"] == "archive" && a["from"] == "files" {
|
||||
packs++
|
||||
if a["name"] != "files" {
|
||||
t.Errorf("the templates archive is named %v, not files", a["name"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if packs != 1 {
|
||||
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
||||
}
|
||||
}
|
||||
|
||||
// text is the text a file resource places on the machine: its template file's.
|
||||
func (m manifest) text(t *testing.T, id string) string {
|
||||
t.Helper()
|
||||
name, _ := m.resource(t, id)["template"].(string)
|
||||
if name == "" {
|
||||
t.Fatalf("resource %s names no template", id)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
|
||||
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
|
||||
func checkTheToolsAgree(t *testing.T, m manifest) {
|
||||
|
||||
@@ -30,12 +30,12 @@ func TestTheColourBuildIsKeptWithAFallbackAndXscreensaverGoes(t *testing.T) {
|
||||
if !reflect.DeepEqual(present, []string{"xss-lock", "i3lock"}) || !reflect.DeepEqual(absent, []string{"xscreensaver"}) {
|
||||
t.Fatalf("packages: %v, absent %v", present, absent)
|
||||
}
|
||||
m.sameAsSource(t, "wrapper", "files/bin/screen-lock")
|
||||
m.fromTemplate(t, "wrapper", "files/bin/screen-lock")
|
||||
wrapper := m.resource(t, "wrapper")
|
||||
if wrapper["mode"] != "0755" || wrapper["path"] != "${machine:account-home}/.local/bin/screen-lock" {
|
||||
t.Fatalf("the locker: %v", wrapper)
|
||||
}
|
||||
c := wrapper["content"].(string)
|
||||
c := m.text(t, "wrapper")
|
||||
// The colour build's options only once it has said it is the colour build: the distribution's
|
||||
// i3lock refuses an option it does not know, and the screen would not lock at all.
|
||||
check := strings.Index(c, `i3lock --version 2>&1 | grep -qE '[0-9]\.c\.[0-9]'`)
|
||||
@@ -53,10 +53,10 @@ func TestTheColourBuildIsKeptWithAFallbackAndXscreensaverGoes(t *testing.T) {
|
||||
|
||||
func TestTheSessionStartSetsTheTimeoutsAndKeepsOneWatcherForTheSession(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
if len(m.Shell) != 1 || m.Shell[0].For != "xinitrc" || m.Shell[0].Slot != "normal" {
|
||||
t.Fatalf("one xinitrc contribution in normal: %+v", m.Shell)
|
||||
if len(m.shell(t)) != 1 || m.shell(t)[0].For != "xinitrc" || m.shell(t)[0].Slot != "normal" {
|
||||
t.Fatalf("one xinitrc contribution in normal: %+v", m.shell(t))
|
||||
}
|
||||
code := m.Shell[0].Code
|
||||
code := m.shell(t)[0].Code
|
||||
if strings.Count(code, "xss-lock --") != 1 || !strings.Contains(code, "--transfer-sleep-lock") ||
|
||||
!strings.Contains(code, "while kill -0 $$") || !strings.HasSuffix(strings.TrimSpace(code), "&") {
|
||||
t.Fatalf("the watcher: %q", code)
|
||||
@@ -87,3 +87,8 @@ func TestTheToolsAgreeWithTheManifest(t *testing.T) {
|
||||
checkTheToolsAgree(t, m)
|
||||
checkNoSecretsOrInstallationNames(t)
|
||||
}
|
||||
|
||||
// No file resource carries its text inline (novox/hq ADR 0349 §2).
|
||||
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
||||
readManifest(t).everyFileIsATemplate(t)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# The lock screen (module screen-lock, novox/hq ADR 0208): the session locks after 30 minutes idle,
|
||||
# the displays go to standby and suspend then and off after an hour, and xss-lock runs the locker on
|
||||
# idle, before suspend and on logind's Lock. xss-lock needs this login session, so it starts here and
|
||||
# not as a unit. It is started again if it exits, for as long as this session lasts ($$ is the
|
||||
# session's own process, which becomes the window manager).
|
||||
xset s 1800 1800
|
||||
xset dpms 1800 1800 3600
|
||||
(while kill -0 $$ 2>/dev/null; do xss-lock --transfer-sleep-lock -- "$HOME/.local/bin/screen-lock"; sleep 2; done) &
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"module": "screen-lock",
|
||||
"version": "1",
|
||||
"moved": true,
|
||||
"capabilities": [
|
||||
"package-manager"
|
||||
],
|
||||
@@ -21,11 +22,11 @@
|
||||
"screen_lock_inhibit",
|
||||
"screen_lock_locked"
|
||||
],
|
||||
"shell": [
|
||||
"items": [
|
||||
{
|
||||
"for": "xinitrc",
|
||||
"slot": "normal",
|
||||
"code": "# The lock screen (module screen-lock, novox/hq ADR 0208): the session locks after 30 minutes idle,\n# the displays go to standby and suspend then and off after an hour, and xss-lock runs the locker on\n# idle, before suspend and on logind's Lock. xss-lock needs this login session, so it starts here and\n# not as a unit. It is started again if it exits, for as long as this session lasts ($$ is the\n# session's own process, which becomes the window manager).\nxset s 1800 1800\nxset dpms 1800 1800 3600\n(while kill -0 $$ 2>/dev/null; do xss-lock --transfer-sleep-lock -- \"$HOME/.local/bin/screen-lock\"; sleep 2; done) &\n"
|
||||
"collection": "shell:xinitrc:normal",
|
||||
"name": "xinitrc-normal",
|
||||
"template": "files/xinitrc-normal"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
@@ -51,7 +52,7 @@
|
||||
"path": "${machine:account-home}/.local/bin/screen-lock",
|
||||
"owner": "${machine:account}",
|
||||
"mode": "0755",
|
||||
"content": "#!/usr/bin/env bash\n# screen-lock (module screen-lock, novox/hq ADR 0208): the locker. xss-lock runs it on idle, before\n# suspend and on logind's Lock, and node-lock-screen's `lock` ends in it.\n#\n# The operator's look, adopted from the predecessor's my-i3lock: the screen as it was, blurred, with\n# an orange ring, the time and the date. That needs i3lock-color, from the distribution's user\n# repository, kept as found until the mesh carries such software (novox/hq research 027, question 1).\n# On a machine without it the distribution's i3lock shows the same blurred screen, taken here, with\n# its own plain ring; failing that, black.\n#\n# Before a suspend, xss-lock hands this script a lock on the sleep (XSS_SLEEP_LOCK_FD) and suspends\n# once it is released. The locker must not inherit it, or the machine would wait for the unlock\n# before sleeping; it is released once i3lock is up, so the machine never sleeps unlocked. This is\n# xss-lock's own documented pattern for i3lock.\nset -u\n\n# One locker: a second press of the key, or a lock while locked, changes nothing.\nif pgrep -xu \"$EUID\" i3lock >/dev/null; then\n\texit 0\nfi\n\n# The colour build numbers its versions <n>.c.<n> (2.13.c.5); its version line never says \"color\".\nif i3lock --version 2>&1 | grep -qE '[0-9]\\.c\\.[0-9]'; then\n\tblank='#00000000' clear='#ffffff22' accent='#ca4a00' wrong='#880000bb' verifying='#bb00bbbb'\n\toptions=(\n\t\t--insidever-color=\"$clear\" --ringver-color=\"$verifying\"\n\t\t--insidewrong-color=\"$clear\" --ringwrong-color=\"$wrong\"\n\t\t--inside-color=\"$blank\" --ring-color=\"$accent\" --line-color=\"$blank\" --separator-color=\"$accent\"\n\t\t--verif-color=\"$accent\" --wrong-color=\"$accent\" --time-color=\"$accent\" --date-color=\"$accent\"\n\t\t--layout-color=\"$accent\" --keyhl-color=\"$wrong\" --bshl-color=\"$wrong\"\n\t\t--screen 1 --blur 5 --ring-width=7.0 --clock --indicator\n\t\t--time-str=\"%H:%M:%S\" --date-str=\"%A, %Y-%m-%d\"\n\t\t--time-font=sans-serif --date-font=sans-serif --verif-font=sans-serif\n\t\t--wrong-font=sans-serif --layout-font=sans-serif --keylayout 1\n\t\t--show-failed-attempts --ignore-empty-password\n\t)\nelse\n\toptions=(--color=000000 --show-failed-attempts --ignore-empty-password)\n\tshot=\"${XDG_RUNTIME_DIR:-/tmp}/screen-lock.png\"\n\tif command -v magick >/dev/null && magick import -window root -resize 25% -blur 0x3 -resize 400% \"$shot\" 2>/dev/null; then\n\t\toptions+=(--image=\"$shot\")\n\tfi\nfi\n\nif [[ -e /dev/fd/${XSS_SLEEP_LOCK_FD:--1} ]]; then\n\tkill_i3lock() { pkill -xu \"$EUID\" \"$@\" i3lock; }\n\ttrap kill_i3lock TERM INT\n\ti3lock \"${options[@]}\" {XSS_SLEEP_LOCK_FD}<&-\n\texec {XSS_SLEEP_LOCK_FD}<&-\n\twhile kill_i3lock -0; do\n\t\tsleep 0.5\n\tdone\nelse\n\ttrap 'kill %%' TERM INT\n\ti3lock --nofork \"${options[@]}\" &\n\twait\nfi\n"
|
||||
"template": "files/bin/screen-lock"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -66,6 +67,11 @@
|
||||
"loads": [
|
||||
"screen-lock-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user