Merge pull request 'A test that reads another repository judges what the check clones, never the desktop's checkout (issue 432)' (#217) from fix/432-a-test-reads-what-it-carries into main
This commit was merged in pull request #217.
This commit is contained in:
@@ -6,8 +6,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The check anybody can run is the check registration runs (novox/hq issue 148, ADR 0037): a manifest
|
// The check anybody can run is the check registration runs (novox/hq issue 148, ADR 0037): a manifest
|
||||||
@@ -56,10 +58,7 @@ func TestModuleCheckJudgesBetweenTheManifestsGiven(t *testing.T) {
|
|||||||
|
|
||||||
// The real catalogue passes the command, the way it passes the test that used to be the only check.
|
// The real catalogue passes the command, the way it passes the test that used to be the only check.
|
||||||
func TestModuleCheckPassesTheCatalogue(t *testing.T) {
|
func TestModuleCheckPassesTheCatalogue(t *testing.T) {
|
||||||
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
root := beside.Catalogue(t)
|
||||||
if _, err := os.Stat(root); err != nil {
|
|
||||||
t.Skipf("catalogue sibling not present: %v", err)
|
|
||||||
}
|
|
||||||
paths, err := manifestsUnder(root)
|
paths, err := manifestsUnder(root)
|
||||||
if err != nil || len(paths) == 0 {
|
if err != nil || len(paths) == 0 {
|
||||||
t.Fatalf("no manifests under %s: %v", root, err)
|
t.Fatalf("no manifests under %s: %v", root, err)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/overlay"
|
"github.com/novox/mesh-controller/internal/overlay"
|
||||||
@@ -239,13 +241,12 @@ func TestATakeoverIsNotComposedForAHubPlacedOffItsTunnel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// theResolver is the catalogue's dnsmasq module as it is, or the test is skipped where the
|
// theResolver is the catalogue's dnsmasq module as it is (internal/beside).
|
||||||
// catalogue is not beside this checkout.
|
|
||||||
func theResolver(t *testing.T) catalogue.Manifest {
|
func theResolver(t *testing.T) catalogue.Manifest {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/dnsmasq/module.json")
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), "dnsmasq", "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m, err := catalogue.ParseManifest(raw)
|
m, err := catalogue.ParseManifest(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Package beside is where a test finds another repository of the mesh it reads: the catalogue's manifests,
|
||||||
|
// the node-engine's genesis template (novox/hq issue 432).
|
||||||
|
//
|
||||||
|
// A test used to read the checkout beside this one, `../../../mesh-catalog`, so its verdict depended on
|
||||||
|
// whatever sat on the machine running it: a stale or dirty checkout failed it on a desktop, and where none
|
||||||
|
// was beside it skipped and said nothing. Now there are two inputs, both named, and no third:
|
||||||
|
//
|
||||||
|
// - In a merge check, the build seat clones each core repository beside the one checked (the catalogue
|
||||||
|
// at its main, the node-engine at the commit the mesh runs) and says where in MESH_CHECK_BESIDE. A
|
||||||
|
// test judges against those clones, so agreement with the other repository is checked where
|
||||||
|
// `mesh/repo-check` runs; a repository missing there fails the test, never skips it. Which clones a
|
||||||
|
// check gets is chosen from the inventory: mesh-catalog by the source of the `nats` module, mesh-host
|
||||||
|
// by the source of `mesh-host`. In a mesh where either module has no source repository nothing is
|
||||||
|
// cloned, and these tests fail loudly with "not beside this check": a cause in the setup, not in the
|
||||||
|
// change. And in a delivery group that holds a mesh-catalog pull request, these tests read the
|
||||||
|
// catalogue's main, not the group's head.
|
||||||
|
// - Anywhere else, the test judges against the copy captured in this repository's testdata/beside, at the
|
||||||
|
// commit testdata/beside/CAPTURED names. Never against a developer's own checkout: to judge one, set
|
||||||
|
// MESH_CHECK_BESIDE to the directory holding it, as the check does.
|
||||||
|
//
|
||||||
|
// The captured manifests are named module.json.captured, and put back as module.json in a directory of
|
||||||
|
// the test's own: a module.json anywhere in this repository is a module of it to the forge's announcer and
|
||||||
|
// the planner, and a merge would build and register a hundred copies of the catalogue's.
|
||||||
|
package beside
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Env is where a merge check says the repositories cloned beside the one checked are (internal/builder's
|
||||||
|
// EnvBeside, repeated here so a test does not import the builder).
|
||||||
|
const Env = "MESH_CHECK_BESIDE"
|
||||||
|
|
||||||
|
// CapturedSuffix is what a captured file's name carries that the repository's own does not.
|
||||||
|
const CapturedSuffix = ".captured"
|
||||||
|
|
||||||
|
// Dir is the named repository a test reads — the clone beside a merge check, or the captured copy — and
|
||||||
|
// says which in the test's log.
|
||||||
|
func Dir(t testing.TB, repository string) string {
|
||||||
|
t.Helper()
|
||||||
|
if root := os.Getenv(Env); root != "" {
|
||||||
|
dir := filepath.Join(root, repository)
|
||||||
|
if _, err := os.Stat(dir); err != nil {
|
||||||
|
t.Fatalf("%s is not beside this check in %s=%s, so its agreement with this repository cannot be "+
|
||||||
|
"judged here: %v", repository, Env, root, err)
|
||||||
|
}
|
||||||
|
t.Logf("judged against %s as cloned beside this check", dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
from := filepath.Join(Captured(), repository)
|
||||||
|
if _, err := os.Stat(from); err != nil {
|
||||||
|
t.Fatalf("no captured copy of %s at %s: %v", repository, from, err)
|
||||||
|
}
|
||||||
|
dir := filepath.Join(t.TempDir(), repository)
|
||||||
|
err := filepath.WalkDir(from, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, _ := filepath.Rel(from, path)
|
||||||
|
into := filepath.Join(dir, strings.TrimSuffix(rel, CapturedSuffix))
|
||||||
|
if d.IsDir() {
|
||||||
|
return os.MkdirAll(into, 0o755)
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(into, raw, 0o644)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("the captured copy of %s could not be laid out: %v", repository, err)
|
||||||
|
}
|
||||||
|
t.Logf("judged against the copy of %s captured in testdata/beside (see CAPTURED); a merge check "+
|
||||||
|
"judges it against the repository's own clone", repository)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catalogue is the catalogue's modules directory, as Dir finds the catalogue.
|
||||||
|
func Catalogue(t testing.TB) string {
|
||||||
|
t.Helper()
|
||||||
|
return filepath.Join(Dir(t, "mesh-catalog"), "modules")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captured is this repository's testdata/beside, found from this file rather than from the working
|
||||||
|
// directory, so a test in any package reaches it.
|
||||||
|
func Captured() string {
|
||||||
|
_, file, _, _ := runtime.Caller(0)
|
||||||
|
return filepath.Join(filepath.Dir(file), "..", "..", "testdata", "beside")
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,10 +75,10 @@ func TestTheAgreementCheckCatchesASubscriptionThatMatchesNothing(t *testing.T) {
|
|||||||
|
|
||||||
func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat) {
|
func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
root := beside.Catalogue(t)
|
||||||
entries, err := os.ReadDir(root)
|
entries, err := os.ReadDir(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("catalogue sibling not present: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var emitters []AnEmitter
|
var emitters []AnEmitter
|
||||||
var consumers []AConsumer
|
var consumers []AConsumer
|
||||||
@@ -119,7 +120,7 @@ func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(emitters) == 0 {
|
if len(emitters) == 0 {
|
||||||
t.Skip("no manifests found beside this checkout")
|
t.Fatalf("no module under %s emits anything, so this proved nothing", root)
|
||||||
}
|
}
|
||||||
return emitters, consumers, seats
|
return emitters, consumers, seats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **The first user list the installer carries must be the one the controller would compose.**
|
// **The first user list the installer carries must be the one the controller would compose.**
|
||||||
@@ -76,13 +78,14 @@ func theCarriedAccounts(t *testing.T) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// theTemplate is the installer's bundle, as resources.
|
// theTemplate is the installer's bundle, as resources: the node-engine's, as a merge check clones it at
|
||||||
|
// the commit the mesh runs, or as captured in testdata/beside (internal/beside, novox/hq issue 432).
|
||||||
func theTemplate(t *testing.T) []map[string]any {
|
func theTemplate(t *testing.T) []map[string]any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join("..", "..", "..", "mesh-host", "examples", "foundation-first-node-nats.lock")
|
path := filepath.Join(beside.Dir(t, "mesh-host"), "examples", "foundation-first-node-nats.lock")
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("the host's checkout is not beside this one: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// The template is JSON with line comments, which is how every one of them is written.
|
// The template is JSON with line comments, which is how every one of them is written.
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **The word does not come back through a manifest** (novox/hq ADR 0131). A module that wants
|
// **The word does not come back through a manifest** (novox/hq ADR 0131). A module that wants
|
||||||
@@ -28,12 +30,12 @@ func TestAManifestRequiringAmqpIsRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// And the catalogue as checked out beside this repository names it nowhere — the three modules that
|
// And the catalogue (internal/beside) names it nowhere — the three modules that did are removed under
|
||||||
// did are removed under design 28 task 5.4, not converted.
|
// design 28 task 5.4, not converted.
|
||||||
func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
|
func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
|
||||||
modules, err := filepath.Glob("../../../mesh-catalog/modules/*/module.json")
|
modules, err := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json"))
|
||||||
if err != nil || len(modules) == 0 {
|
if err != nil || len(modules) == 0 {
|
||||||
t.Skip("the catalogue is not checked out beside this repository")
|
t.Fatalf("no manifests in the catalogue, so this proved nothing: %v", err)
|
||||||
}
|
}
|
||||||
for _, path := range modules {
|
for _, path := range modules {
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The module holding mesh-broker is the bus, and its account is granted the bus's snapshot API and
|
// The module holding mesh-broker is the bus, and its account is granted the bus's snapshot API and
|
||||||
@@ -34,9 +37,9 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
|
|||||||
// dump into its snapshots, it has the account the dump runs as, and the dump runs the snapshot
|
// dump into its snapshots, it has the account the dump runs as, and the dump runs the snapshot
|
||||||
// program in the bus's own container.
|
// program in the bus's own container.
|
||||||
func TestTheCataloguesBusIsBackedUpBySnapshot(t *testing.T) {
|
func TestTheCataloguesBusIsBackedUpBySnapshot(t *testing.T) {
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/nats/module.json")
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), "nats", "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("the catalogue is not checked out beside this repository")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m, err := ParseManifest(raw)
|
m, err := ParseManifest(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,26 +5,21 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestEveryCatalogueManifestParses runs the real catalogue through the real gate.
|
// TestEveryCatalogueManifestParses runs the real catalogue through the real gate.
|
||||||
//
|
//
|
||||||
// Not a fixture: the point is whether the manifests as written are accepted by the control plane that
|
// Not a fixture: the point is whether the manifests as written are accepted by the control plane that
|
||||||
// will read them, and a copy of one manifest proves nothing about the other seventy-one.
|
// will read them, and a copy of one manifest proves nothing about the other seventy-one.
|
||||||
// catalogueRoot is the catalogue these checks run over: MESH_CATALOGUE when set, else the checkout
|
// catalogueRoot is the catalogue these checks run over: in a merge check the clone the build seat put
|
||||||
// beside this one, the way the main layout has it. A check that only ran when somebody remembered a
|
// beside this one, elsewhere the copy captured in testdata/beside (internal/beside). A check that only
|
||||||
// variable was a check nobody ran (novox/hq issue 134, 2026-09-30); it skips only when there is no
|
// ran when somebody remembered a variable was a check nobody ran (novox/hq issue 134), and one that read
|
||||||
// catalogue to be found at all.
|
// whatever checkout sat beside judged the machine, not the change (novox/hq issue 432): it never skips.
|
||||||
func catalogueRoot(t *testing.T) string {
|
func catalogueRoot(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if root := os.Getenv("MESH_CATALOGUE"); root != "" {
|
return beside.Dir(t, "mesh-catalog")
|
||||||
return root
|
|
||||||
}
|
|
||||||
sibling := filepath.Join("..", "..", "..", "mesh-catalog")
|
|
||||||
if _, err := os.Stat(filepath.Join(sibling, "modules")); err != nil {
|
|
||||||
t.Skip("no catalogue beside this checkout and MESH_CATALOGUE unset")
|
|
||||||
}
|
|
||||||
return sibling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEveryCatalogueManifestParses(t *testing.T) {
|
func TestEveryCatalogueManifestParses(t *testing.T) {
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **A machine trusts the mesh's authority because a module put its root there** (novox/hq ADR
|
// **A machine trusts the mesh's authority because a module put its root there** (novox/hq ADR
|
||||||
@@ -16,9 +19,9 @@ import (
|
|||||||
// itself — a plain client trusting an internal name on a machine holding this, and failing on one
|
// itself — a plain client trusting an internal name on a machine holding this, and failing on one
|
||||||
// that does not — is the lab's, and cannot be had here.
|
// that does not — is the lab's, and cannot be had here.
|
||||||
func TestCaTrustRendersTheAuthorityItWasBoundTo(t *testing.T) {
|
func TestCaTrustRendersTheAuthorityItWasBoundTo(t *testing.T) {
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/ca-trust/module.json")
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), "ca-trust", "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m, err := ParseManifest(raw)
|
m, err := ParseManifest(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -134,27 +134,61 @@ func TestThePOSIXEnvironmentSourcedTwiceLeavesPATHAsOnce(t *testing.T) {
|
|||||||
|
|
||||||
// The environment.d rendering, read by the service manager's own generator where this machine has
|
// The environment.d rendering, read by the service manager's own generator where this machine has
|
||||||
// one — the same reader an account's user manager runs, so the PATH it composes is the one asserted.
|
// one — the same reader an account's user manager runs, so the PATH it composes is the one asserted.
|
||||||
|
//
|
||||||
|
// The generator also reads the machine's own environment.d (/etc, /run, /usr/lib, /usr/local/lib), and
|
||||||
|
// no option points it elsewhere: a desktop whose snapd appends its bin directory failed this, on main,
|
||||||
|
// for a file the mesh never wrote (novox/hq issue 432). So the generator is run twice, once without the
|
||||||
|
// mesh's file, and what the machine's own files make of PATH and set is held out of the verdict.
|
||||||
|
//
|
||||||
|
// A machine without the generator — the build seat's toolchain image holds no systemd — cannot judge
|
||||||
|
// this and says so: there the rendering is held byte for byte by
|
||||||
|
// TestTheEnvironmentRendersForTheServiceManagerByteForByte, and this reading only where systemd runs.
|
||||||
func TestTheServiceManagerReadsTheSystemdRenderingAsMeant(t *testing.T) {
|
func TestTheServiceManagerReadsTheSystemdRenderingAsMeant(t *testing.T) {
|
||||||
generator := "/usr/lib/systemd/user-environment-generators/30-systemd-environment-d-generator"
|
generator := "/usr/lib/systemd/user-environment-generators/30-systemd-environment-d-generator"
|
||||||
if _, err := os.Stat(generator); err != nil {
|
if _, err := os.Stat(generator); err != nil {
|
||||||
t.Skip("no environment.d generator on this machine")
|
t.Skip("NOT JUDGED: no environment.d generator on this machine, so the service manager's reading " +
|
||||||
|
"of the rendering is judged only where systemd runs; the rendering itself is held byte for byte " +
|
||||||
|
"by TestTheEnvironmentRendersForTheServiceManagerByteForByte")
|
||||||
}
|
}
|
||||||
|
const base = "/usr/bin:/bin"
|
||||||
|
read := func(conf string) map[string]string {
|
||||||
|
t.Helper()
|
||||||
config := t.TempDir()
|
config := t.TempDir()
|
||||||
if err := os.MkdirAll(filepath.Join(config, "environment.d"), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(config, "environment.d"), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(config, "environment.d", "50-mesh.conf"), []byte(composedSystemd), 0o644); err != nil {
|
if conf != "" {
|
||||||
|
if err := os.WriteFile(filepath.Join(config, "environment.d", "50-mesh.conf"), []byte(conf), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cmd := exec.Command(generator)
|
cmd := exec.Command(generator)
|
||||||
cmd.Env = []string{"PATH=/usr/bin:/bin", "HOME=" + config, "XDG_CONFIG_HOME=" + config}
|
cmd.Env = []string{"PATH=" + base, "HOME=" + config, "XDG_CONFIG_HOME=" + config}
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v\n%s", err, out)
|
t.Fatalf("%v\n%s", err, out)
|
||||||
}
|
}
|
||||||
want := "PATH=/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts"
|
vars := map[string]string{}
|
||||||
if !strings.Contains(string(out), want+"\n") || !strings.Contains(string(out), "GOPATH=/home/op/go\n") {
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||||
t.Fatalf("the service manager read\n%s", out)
|
if k, v, ok := strings.Cut(line, "="); ok {
|
||||||
|
vars[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
machine, read50 := read(""), read(composedSystemd)
|
||||||
|
|
||||||
|
// What the machine's own files do to PATH: nothing, or append to it. One that replaces or prepends
|
||||||
|
// leaves nothing this test can say about the mesh's file, and says so rather than guess.
|
||||||
|
added, ok := strings.CutPrefix(machine["PATH"], base)
|
||||||
|
if machine["PATH"] != "" && !ok {
|
||||||
|
t.Skipf("NOT JUDGED: this machine's own environment.d sets PATH to %s, not %s with something after it, so "+
|
||||||
|
"what the mesh's file adds cannot be told apart from it", machine["PATH"], base)
|
||||||
|
}
|
||||||
|
want := "/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts" + added
|
||||||
|
if read50["PATH"] != want || read50["GOPATH"] != "/home/op/go" {
|
||||||
|
t.Fatalf("the service manager read PATH=%s GOPATH=%s, not PATH=%s GOPATH=/home/op/go (the machine's own "+
|
||||||
|
"files add %q to PATH)", read50["PATH"], read50["GOPATH"], want, added)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,24 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The catalogue's foundation modules as they are, parsed by the real parser (novox/hq ADR 0100):
|
// The catalogue's foundation modules as they are — in a merge check the catalogue cloned beside it,
|
||||||
|
// elsewhere the copy captured in testdata/beside (internal/beside, novox/hq issue 432) — parsed by the
|
||||||
|
// real parser (novox/hq ADR 0100):
|
||||||
// the store and the broker say which of their ports the mesh guards on an adopted node, and the
|
// the store and the broker say which of their ports the mesh guards on an adopted node, and the
|
||||||
// filter module loads its table through a unit of its own whose stop deletes only that table.
|
// filter module loads its table through a unit of its own whose stop deletes only that table.
|
||||||
func catalogueManifest(t *testing.T, module string) Manifest {
|
func catalogueManifest(t *testing.T, module string) Manifest {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/" + module + "/module.json")
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), module, "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m, err := ParseManifest(raw)
|
m, err := ParseManifest(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,8 +130,11 @@ func TestTheForgesPortIsGivenLikeAnyOtherProvidersPort(t *testing.T) {
|
|||||||
// seat's holder the answer when more than one module provides it — so a carried copy would be a
|
// seat's holder the answer when more than one module provides it — so a carried copy would be a
|
||||||
// second answer to the same question, free to drift from the first. Asserted gone, not merely
|
// second answer to the same question, free to drift from the first. Asserted gone, not merely
|
||||||
// unused.
|
// unused.
|
||||||
|
//
|
||||||
|
// The builder is the build-agent on every machine since novox/hq ADR 0190; this read the retired
|
||||||
|
// `builder` and, finding no manifest, skipped unseen from then until novox/hq issue 432.
|
||||||
func TestTheBuilderRequiresTheRegistryTheNpmSeatDelivers(t *testing.T) {
|
func TestTheBuilderRequiresTheRegistryTheNpmSeatDelivers(t *testing.T) {
|
||||||
builder := catalogueManifest(t, "builder")
|
builder := catalogueManifest(t, "build-agent")
|
||||||
seat, _ := SeatNamed("npm-package-registry")
|
seat, _ := SeatNamed("npm-package-registry")
|
||||||
var requires bool
|
var requires bool
|
||||||
for _, r := range builder.Requires {
|
for _, r := range builder.Requires {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A bind mount the module never declared is refused where it is written (novox/hq 04-ISSUES/026,
|
// A bind mount the module never declared is refused where it is written (novox/hq 04-ISSUES/026,
|
||||||
@@ -67,16 +69,12 @@ func TestTheRuntimeSocketIsGrantedByTheCapabilityAndNotOtherwise(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **Every manifest in the catalogue beside this checkout passes**, so the rule is not one the
|
// **Every manifest in the catalogue (internal/beside) passes**, so the rule is not one the catalogue
|
||||||
// catalogue is already breaking. Skipped, aloud, where the catalogue is not there.
|
// is already breaking.
|
||||||
func TestEveryCatalogueManifestDeclaresWhatItMounts(t *testing.T) {
|
func TestEveryCatalogueManifestDeclaresWhatItMounts(t *testing.T) {
|
||||||
root := os.Getenv("MESH_CATALOG")
|
files, _ := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json"))
|
||||||
if root == "" {
|
|
||||||
root = "../../../mesh-catalog"
|
|
||||||
}
|
|
||||||
files, _ := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
t.Skipf("no catalogue at %s (set MESH_CATALOG to a checkout)", root)
|
t.Fatal("no manifests in the catalogue, so this proved nothing")
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
raw, err := os.ReadFile(file)
|
raw, err := os.ReadFile(file)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **A manifest holds no subject** (novox/hq design 29 §1).
|
// **A manifest holds no subject** (novox/hq design 29 §1).
|
||||||
@@ -17,10 +19,10 @@ import (
|
|||||||
// held by construction is one a later field breaks quietly, with the symptom appearing as a
|
// held by construction is one a later field breaks quietly, with the symptom appearing as a
|
||||||
// permission that does not match a subject rather than as a manifest that was wrong.
|
// permission that does not match a subject rather than as a manifest that was wrong.
|
||||||
func TestNoManifestContainsASubject(t *testing.T) {
|
func TestNoManifestContainsASubject(t *testing.T) {
|
||||||
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
root := beside.Catalogue(t)
|
||||||
entries, err := os.ReadDir(root)
|
entries, err := os.ReadDir(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("catalogue sibling not present: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anything in the mesh's own subject space, and anything shaped like a wire address.
|
// Anything in the mesh's own subject space, and anything shaped like a wire address.
|
||||||
@@ -63,7 +65,7 @@ func TestNoManifestContainsASubject(t *testing.T) {
|
|||||||
walk(e.Name(), "", m)
|
walk(e.Name(), "", m)
|
||||||
}
|
}
|
||||||
if checked == 0 {
|
if checked == 0 {
|
||||||
t.Skip("no manifests read")
|
t.Fatal("no manifests read, so this proved nothing")
|
||||||
}
|
}
|
||||||
if len(found) > 0 {
|
if len(found) > 0 {
|
||||||
t.Errorf("a manifest names a subject, so reorganising the subject space would mean "+
|
t.Errorf("a manifest names a subject, so reorganising the subject space would mean "+
|
||||||
@@ -91,13 +93,14 @@ func TestEveryManifestsEventNamesAreLocal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// theCatalogue is every manifest beside this checkout, parsed the way registration parses one.
|
// theCatalogue is every manifest of the catalogue internal/beside finds, parsed the way registration
|
||||||
|
// parses one.
|
||||||
func theCatalogue(t *testing.T) []Manifest {
|
func theCatalogue(t *testing.T) []Manifest {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
root := beside.Catalogue(t)
|
||||||
entries, err := os.ReadDir(root)
|
entries, err := os.ReadDir(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("catalogue sibling not present: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var out []Manifest
|
var out []Manifest
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -115,7 +118,7 @@ func theCatalogue(t *testing.T) []Manifest {
|
|||||||
out = append(out, m)
|
out = append(out, m)
|
||||||
}
|
}
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
t.Skip("no manifests found beside this checkout")
|
t.Fatalf("no manifests under %s, so this proved nothing", root)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The public issuer is the proxy's own fact (novox/hq ADR 0226), and the folding of it must not
|
// The public issuer is the proxy's own fact (novox/hq ADR 0226), and the folding of it must not
|
||||||
@@ -86,22 +89,22 @@ func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The modules ADR 0226 retired stay retired, and nothing asks for what they provided.
|
// The modules ADR 0226 retired stay retired, and nothing asks for what they provided. A module is
|
||||||
|
// in the catalogue when its manifest is: a directory left behind with no manifest in it (a build's
|
||||||
|
// leftovers, untracked by git) is not a module, and failed this on a desktop (novox/hq issue 432).
|
||||||
func TestTheRetiredNetworkingModulesAreNotInTheCatalogue(t *testing.T) {
|
func TestTheRetiredNetworkingModulesAreNotInTheCatalogue(t *testing.T) {
|
||||||
if _, err := os.Stat("../../../mesh-catalog/modules"); err != nil {
|
modules := beside.Catalogue(t)
|
||||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
|
||||||
}
|
|
||||||
for _, gone := range []string{"public-acme", "dhcpcd", "cloudflare-dns"} {
|
for _, gone := range []string{"public-acme", "dhcpcd", "cloudflare-dns"} {
|
||||||
if _, err := os.Stat("../../../mesh-catalog/modules/" + gone); err == nil {
|
if _, err := os.Stat(filepath.Join(modules, gone, "module.json")); err == nil {
|
||||||
t.Errorf("%s is in the catalogue again; ADR 0226 retired it", gone)
|
t.Errorf("%s is in the catalogue again; ADR 0226 retired it", gone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir("../../../mesh-catalog/modules")
|
entries, err := os.ReadDir(modules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/" + e.Name() + "/module.json")
|
raw, err := os.ReadFile(filepath.Join(modules, e.Name(), "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ func TestTheCataloguesBarRendersEveryExampleOfTheShape(t *testing.T) {
|
|||||||
holder := catalogueManifest(t, "i3status-rust")
|
holder := catalogueManifest(t, "i3status-rust")
|
||||||
s, _ := SeatNamed(BarSeat)
|
s, _ := SeatNamed(BarSeat)
|
||||||
if !placesKind(holder, s, BarKindBlock) {
|
if !placesKind(holder, s, BarKindBlock) {
|
||||||
t.Skip("the catalogue beside this checkout has a bar that places no blocks yet")
|
// It places them since the catalogue's bar took the seat; a skip here hid a bar that stopped
|
||||||
|
// (novox/hq issue 432).
|
||||||
|
t.Fatal("the catalogue's bar places no blocks, so none of the shape's examples would render")
|
||||||
}
|
}
|
||||||
tmpl, err := holderTemplate(holder, s, BarKindBlock)
|
tmpl, err := holderTemplate(holder, s, BarKindBlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defends novox/hq ADR 0110: a seat is a module assignment from a closed set.
|
// Defends novox/hq ADR 0110: a seat is a module assignment from a closed set.
|
||||||
@@ -193,13 +195,13 @@ func TestAClaimThatIsMalformedIsRefusedOnceForThat(t *testing.T) {
|
|||||||
|
|
||||||
// Every module in use claims a seat in the set, so closing it refuses nothing that runs.
|
// Every module in use claims a seat in the set, so closing it refuses nothing that runs.
|
||||||
//
|
//
|
||||||
// Read from the catalogue beside this checkout and from this repository's own manifest, the two
|
// Read from the catalogue (internal/beside) and from this repository's own manifest, the two places a
|
||||||
// places a manifest lives (ADR 0069). The private-network module's manifest is composed in code,
|
// manifest lives (ADR 0069). The private-network module's manifest is composed in code, and its claim
|
||||||
// and its claim is checked where it is composed.
|
// is checked where it is composed.
|
||||||
func TestEveryManifestInUseClaimsASeatTheMeshDefines(t *testing.T) {
|
func TestEveryManifestInUseClaimsASeatTheMeshDefines(t *testing.T) {
|
||||||
paths, _ := filepath.Glob("../../../mesh-catalog/modules/*/module.json")
|
paths, _ := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json"))
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
t.Skip("the catalogue is not beside this checkout")
|
t.Fatal("no manifests in the catalogue, so this proved nothing")
|
||||||
}
|
}
|
||||||
paths = append(paths, "../../module.json")
|
paths = append(paths, "../../module.json")
|
||||||
var checked int
|
var checked int
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **The showcase module is parsed by the real parser, in the real test suite.**
|
// **The showcase module is parsed by the real parser, in the real test suite.**
|
||||||
@@ -11,9 +14,9 @@ import (
|
|||||||
// Written as a test rather than a script so it runs whenever anything about manifests changes —
|
// Written as a test rather than a script so it runs whenever anything about manifests changes —
|
||||||
// which is exactly when a module using all of it would quietly stop being valid.
|
// which is exactly when a module using all of it would quietly stop being valid.
|
||||||
func TestTheShowcaseModuleIsAValidManifest(t *testing.T) {
|
func TestTheShowcaseModuleIsAValidManifest(t *testing.T) {
|
||||||
raw, err := os.ReadFile("../../../mesh-catalog/modules/showcase/module.json")
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), "showcase", "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m, err := ParseManifest(raw)
|
m, err := ParseManifest(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,18 +10,22 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **The bus the tests run is the bus the mesh runs** (novox/hq ADR 0227 rule 9, to-be 45 §9). The server
|
// **The bus the tests run is the bus the mesh runs** (novox/hq ADR 0227 rule 9, to-be 45 §9). The server
|
||||||
// linked into the tests is held to the release the catalogue's bus image is — read from beside this
|
// linked into the tests is held to the release the catalogue's bus image is — the catalogue a merge check
|
||||||
// checkout — and, in a merge check that has the facts snapshot, to the release the mesh's bus server says
|
// clones beside this one, or the copy captured in testdata/beside (internal/beside, novox/hq issue 432) —
|
||||||
// it runs. A bus upgrade moves the catalogue's pin; this then fails until the tests' pin moves with it,
|
// and, in a merge check that has the facts snapshot, to the release the mesh's bus server says it runs. A
|
||||||
// so the controller is never tested against a bus the mesh no longer runs, or not yet.
|
// bus upgrade moves the catalogue's pin; this then fails in the merge check until the tests' pin moves
|
||||||
|
// with it, so the controller is never tested against a bus the mesh no longer runs, or not yet.
|
||||||
func TestTheBusTheTestsRunIsTheBusTheMeshRuns(t *testing.T) {
|
func TestTheBusTheTestsRunIsTheBusTheMeshRuns(t *testing.T) {
|
||||||
dockerfile := filepath.Join("..", "..", "..", "mesh-catalog", "modules", "nats", "Dockerfile")
|
dockerfile := filepath.Join(beside.Catalogue(t), "nats", "Dockerfile")
|
||||||
raw, err := os.ReadFile(dockerfile)
|
raw, err := os.ReadFile(dockerfile)
|
||||||
switch {
|
if err != nil {
|
||||||
case err == nil:
|
t.Fatal(err)
|
||||||
|
}
|
||||||
pinned := regexp.MustCompile(`upstream: nats ([0-9.]+)-alpine`).FindSubmatch(raw)
|
pinned := regexp.MustCompile(`upstream: nats ([0-9.]+)-alpine`).FindSubmatch(raw)
|
||||||
if pinned == nil {
|
if pinned == nil {
|
||||||
t.Fatalf("%s says no release its digest is", dockerfile)
|
t.Fatalf("%s says no release its digest is", dockerfile)
|
||||||
@@ -30,11 +34,6 @@ func TestTheBusTheTestsRunIsTheBusTheMeshRuns(t *testing.T) {
|
|||||||
t.Errorf("the tests run bus %s and the catalogue's bus image is %s: move go.mod's nats-server pin "+
|
t.Errorf("the tests run bus %s and the catalogue's bus image is %s: move go.mod's nats-server pin "+
|
||||||
"(and `go mod vendor`) with the image", Version, pinned[1])
|
"(and `go mod vendor`) with the image", Version, pinned[1])
|
||||||
}
|
}
|
||||||
case os.IsNotExist(err):
|
|
||||||
t.Logf("the catalogue is not beside this checkout, so its bus image is not compared")
|
|
||||||
default:
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
path := os.Getenv("MESH_FACTS")
|
path := os.Getenv("MESH_FACTS")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
t.Logf("no MESH_FACTS: the bus the mesh runs is compared in a merge check, which has it")
|
t.Logf("no MESH_FACTS: the bus the mesh runs is compared in a merge check, which has it")
|
||||||
|
|||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
What a test reads of another repository when no merge check has cloned it beside this one (internal/beside,
|
||||||
|
novox/hq issue 432). Copied from the repositories at the commits below, never written by hand. Each
|
||||||
|
module.json is kept as module.json.captured: a module.json in this repository is a module of it to the
|
||||||
|
forge and the planner, and a merge would build and register it.
|
||||||
|
|
||||||
|
The copy carries the catalogue's private details: domains, first names in module names, a node name and
|
||||||
|
private addresses. That is acceptable while mesh-controller is private; if it ever goes public, this copy is
|
||||||
|
a second place to clean besides the catalogue itself.
|
||||||
|
|
||||||
|
mesh-catalog b9de001833b1b61b297182b8e3bcdae1cbdeace9 modules/*/module.json, modules/nats/Dockerfile
|
||||||
|
mesh-host bd5cc6980419c1bd4824be6d58dfebd2381a9de3 examples/foundation-first-node-nats.lock
|
||||||
|
|
||||||
|
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||||
|
|
||||||
|
rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host
|
||||||
|
mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host
|
||||||
|
git -C ../mesh-catalog archive <commit> modules | tar -x -C testdata/beside/mesh-catalog --wildcards \
|
||||||
|
'modules/*/module.json' 'modules/nats/Dockerfile'
|
||||||
|
find testdata/beside -name module.json -exec mv {} {}.captured \;
|
||||||
|
git -C ../mesh-host archive <commit> examples/foundation-first-node-nats.lock | tar -x -C testdata/beside/mesh-host
|
||||||
|
|
||||||
|
and write the commits here.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"module": "adwaita",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"adwaita_appearance",
|
||||||
|
"adwaita_cursor",
|
||||||
|
"adwaita_icons",
|
||||||
|
"adwaita_portal_check"
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"variables": {
|
||||||
|
"GTK_THEME": "Adwaita:dark",
|
||||||
|
"GTK2_RC_FILES": "/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc",
|
||||||
|
"QT_QPA_PLATFORMTHEME": "qt6ct",
|
||||||
|
"QT_STYLE_OVERRIDE": "Fusion",
|
||||||
|
"QT_SELECT": "6",
|
||||||
|
"XCURSOR_THEME": "Adwaita",
|
||||||
|
"XCURSOR_SIZE": "24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "xresources",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "! adwaita: the cursor, for X programs that take it from the resources.\nXcursor.theme: Adwaita\nXcursor.size: 24\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"for": "xinitrc",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The appearance (module adwaita): GSettings is where the portal reads dark or light, and the portal is\n# the only way it reaches Electron, Chromium, Firefox and flatpaks. Set at every session start to the\n# module's default; adwaita_appearance switches it for a session.\ngsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true\ngsettings set org.gnome.desktop.interface gtk-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface icon-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface cursor-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface cursor-size 24 || true\ngsettings set org.gnome.desktop.interface font-name 'Inter 11' || true\ngsettings set org.gnome.desktop.interface monospace-font-name 'JetBrainsMono Nerd Font 11' || true\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package-gnome-themes-extra",
|
||||||
|
"type": "package",
|
||||||
|
"package": "gnome-themes-extra"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-adwaita-icon-theme",
|
||||||
|
"type": "package",
|
||||||
|
"package": "adwaita-icon-theme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-adwaita-cursors",
|
||||||
|
"type": "package",
|
||||||
|
"package": "adwaita-cursors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-qt6ct",
|
||||||
|
"type": "package",
|
||||||
|
"package": "qt6ct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-xdg-desktop-portal-gtk",
|
||||||
|
"type": "package",
|
||||||
|
"package": "xdg-desktop-portal-gtk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gtk3",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/gtk-3.0/settings.ini",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208), for GTK 3 and GTK 4 alike. Replaced at\n# every push; adwaita_appearance switches dark and light for the running session.\n[Settings]\ngtk-theme-name=Adwaita\ngtk-icon-theme-name=Adwaita\ngtk-cursor-theme-name=Adwaita\ngtk-cursor-theme-size=24\ngtk-font-name=Inter 11\ngtk-application-prefer-dark-theme=1\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gtk4",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/gtk-4.0/settings.ini",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208), for GTK 3 and GTK 4 alike. Replaced at\n# every push; adwaita_appearance switches dark and light for the running session.\n[Settings]\ngtk-theme-name=Adwaita\ngtk-icon-theme-name=Adwaita\ngtk-cursor-theme-name=Adwaita\ngtk-cursor-theme-size=24\ngtk-font-name=Inter 11\ngtk-application-prefer-dark-theme=1\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qt6ct",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/qt6ct/qt6ct.conf",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[Appearance]\ncolor_scheme_path=/usr/share/qt6ct/colors/darker.conf\ncustom_palette=true\nicon_theme=Adwaita\nstandard_dialogs=default\nstyle=Fusion\n\n[Fonts]\nfixed=\"JetBrainsMono Nerd Font,11,-1,5,50,0,0,0,0,0\"\ngeneral=\"Inter,11,-1,5,50,0,0,0,0,0\"\n\n[Interface]\nactivate_item_on_single_click=1\nbuttonbox_layout=0\ncursor_flash_time=1000\ndialog_buttons_have_icons=1\ndouble_click_interval=400\ngui_effects=@Invalid()\nkeyboard_scheme=2\nmenus_have_icons=true\nshow_shortcuts_in_context_menus=true\nstylesheets=@Invalid()\ntoolbutton_style=4\nunderline_shortcut=1\nwheel_scroll_lines=3\n\n[Troubleshooting]\nforce_raster_widgets=1\nignored_applications=@Invalid()\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "portals",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/xdg-desktop-portal/portals.conf",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208). Replaced at every push.\n#\n# Which portal backend answers each interface. i3 is not a desktop xdg-desktop-portal knows, so with\n# no preference it uses whichever backend happens to be installed: fine while gtk is the only one,\n# wrong the day another arrives as somebody else's dependency. Named instead. gtk also serves\n# org.freedesktop.appearance (dark or light) from GSettings, which the session's start sets.\n[preferred]\ndefault=gtk\n# Secrets for sandboxed programs come from the keyring's backend. Without this line no backend answers\n# the interface: gtk does not implement it, and gnome-keyring's names only GNOME as its desktop.\norg.freedesktop.impl.portal.Secret=gnome-keyring\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cursor",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.icons/default/index.theme",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208): the default cursor theme, for programs that\n# read neither XCURSOR_THEME nor the X resources.\n[Icon Theme]\nName=Default\nInherits=Adwaita\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/adwaita-tools",
|
||||||
|
"binary": "adwaita-tools",
|
||||||
|
"loads": [
|
||||||
|
"adwaita-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"module": "artifact-store-tools",
|
||||||
|
"version": "1",
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.artifacts",
|
||||||
|
"seat:mesh-controller.collect"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"artifact_store_repositories",
|
||||||
|
"artifact_store_usage",
|
||||||
|
"artifact_store_references",
|
||||||
|
"artifact_store_collect"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/artifact-store-tools",
|
||||||
|
"binary": "artifact-store-tools",
|
||||||
|
"loads": [
|
||||||
|
"artifact-store-tools"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
{
|
||||||
|
"module": "asus-zephyrus-g14",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"profile.switched"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"zephyrus_brightness",
|
||||||
|
"zephyrus_battery",
|
||||||
|
"zephyrus_charge_limit",
|
||||||
|
"zephyrus_gpu_mode",
|
||||||
|
"zephyrus_profile",
|
||||||
|
"zephyrus_thermals",
|
||||||
|
"zephyrus_power_draw",
|
||||||
|
"zephyrus_profile_policy",
|
||||||
|
"zephyrus_fan_curves",
|
||||||
|
"zephyrus_keys",
|
||||||
|
"zephyrus_check"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "asusctl",
|
||||||
|
"type": "package",
|
||||||
|
"package": "asusctl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playerctl",
|
||||||
|
"type": "package",
|
||||||
|
"package": "playerctl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asusd",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "asusd.service",
|
||||||
|
"state": "running"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "supergfxd",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "supergfxd.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scripts",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/usr/local/lib/asus-zephyrus-g14",
|
||||||
|
"artifact": "scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nvidia-options",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/modprobe.d/g14-nvidia-power.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The discrete GPU's driver options on the ROG Zephyrus G14 (GA403, RTX 40 series, hybrid graphics).\n#\n# NVreg_DynamicPowerManagement=0x00 turns runtime D3 off. With it on, a change of power source sends\n# the driver an ACPI notification it fails to handle on this model (\"RmHandleDNotifierEvent: Failed to\n# handle ACPI D-Notifier event, status=0x62\"), and the GPU stops making progress until the machine is\n# powered off. Off costs a few idle watts in hybrid mode and keeps the machine up.\n#\n# NVreg_PreserveVideoMemoryAllocations=1 saves video memory across suspend, so what used the GPU still\n# works after waking. It needs nvidia-suspend, -hibernate and -resume to run around a sleep, which this\n# module's drop-ins on the sleep services ask for.\n#\n# A change here applies when the driver next loads: at the next boot.\noptions nvidia NVreg_PreserveVideoMemoryAllocations=1\noptions nvidia NVreg_DynamicPowerManagement=0x00\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "video-options",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/modprobe.d/video-brightness-switch.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The ACPI video driver does not change the backlight itself on the brightness keys: on this model it\n# moves the wrong one. The keys are triggerhappy's (see /etc/triggerhappy/triggers.d/asus-g14.conf).\n# Applies when the module next loads: at the next boot.\noptions video brightness_switch_enabled=0\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "suspend-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/systemd-suspend.service.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nvidia-on-suspend",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-suspend.service nvidia-resume.service\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hibernate-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/systemd-hibernate.service.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nvidia-on-hibernate",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-hibernate.service nvidia-resume.service\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "suspend-then-hibernate-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nvidia-on-suspend-then-hibernate",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-suspend-then-hibernate.service nvidia-resume.service\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "powerd-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/nvidia-powerd.service.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "powerd-opt-in",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# nvidia-powerd (Dynamic Boost) was the first error in the chain that hung this model's GPU on a change\n# of power source, and asusd starts it on mains. It runs only when the kernel command line says\n# zephyrus.nvidia-powerd — an explicit opt-in, at boot.\n[Unit]\nConditionKernelCommandLine=zephyrus.nvidia-powerd\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "backlight-rule",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/udev/rules.d/90-backlight.rules",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The backlights are writable by the video group, so the brightness keys and the module's brightness tool\n# move the panel without root.\nACTION==\"add\", SUBSYSTEM==\"backlight\", RUN+=\"/usr/bin/chgrp video /sys/class/backlight/%k/brightness\", RUN+=\"/usr/bin/chmod g+w /sys/class/backlight/%k/brightness\"\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "udev",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "systemd-udevd.service",
|
||||||
|
"reload-on": [
|
||||||
|
"backlight-rule"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upower-package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "upower"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upower-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/UPower/UPower.conf.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "low-battery",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# On low battery the machine suspends rather than powering off, at 7 % — s2idle still draws a little,\n# so it leaves headroom. A drop-in over the package's own UPower.conf, which stays the package's.\n[UPower]\nUsePercentageForPolicy=true\nPercentageLow=15.0\nPercentageCritical=10.0\nPercentageAction=7.0\nCriticalPowerAction=Suspend\nAllowRiskyCriticalPowerAction=true\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upower",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "upower.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"low-battery"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xorg-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/X11/xorg.conf.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "touchpad",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings, applied by X every time the device appears — at login and after every\n# resume, when the device is initialised again. This replaces the predecessor's sleep hook, which ran\n# xinput after a resume as a named person on a guessed display.\nSection \"InputClass\"\n Identifier \"asus-zephyrus-g14 touchpad\"\n MatchIsTouchpad \"on\"\n Option \"Tapping\" \"on\"\n Option \"NaturalScrolling\" \"true\"\n Option \"AccelSpeed\" \"0.15\"\nEndSection\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/zephyrus",
|
||||||
|
"binary": "zephyrus",
|
||||||
|
"loads": [
|
||||||
|
"zephyrus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scripts",
|
||||||
|
"kind": "archive",
|
||||||
|
"from": "files"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"seat": "node-hotkeys",
|
||||||
|
"kind": "trigger",
|
||||||
|
"content": "# The ROG Zephyrus G14's vendor keys, which reach no X client: media (the M-keys), panel brightness,\n# and the touchpad key. Each runs this module's own script, as the operator's account.\nKEY_PROG1\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media play-pause\nKEY_PROG3\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media previous\nKEY_PROG4\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media next\nKEY_BRIGHTNESSDOWN\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSDOWN\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSUP\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_BRIGHTNESSUP\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_F21\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seat": "node-display-session",
|
||||||
|
"kind": "config",
|
||||||
|
"content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are\n# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for\n# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's.\n\n# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the\n# screenshot grabs the pointer to select a region, which fails while the key is still held.\nbindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh\n\n# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the\n# external output.\nbindsym $mod+p exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display order\n\n# The keyboard backlight's level, shown when it changes.\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-kbd-notify\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seat": "node-display-session",
|
||||||
|
"kind": "config",
|
||||||
|
"content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The model's panel and touchpad.\n\n# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where\n# is the display server's (autorandr, run at every session start).\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display primary\n\n# The touchpad's settings again, by hand. X applies them itself whenever the device appears.\nbindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "after-wake",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The touchpad's settings again after waking, in the operator's session: X applies the module's\n# input class when the device appears, and this covers a wake that does not initialise it again.\n# Runs as root from the power module; the reset itself runs as the session's owner.\nsleep 2\nowner=$(ps -o user= -C i3 | head -n 1)\n[ -n \"$owner\" ] && runuser -u \"$owner\" -- /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\ntrue\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"module": "audit-logger",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "audit",
|
||||||
|
"consumes": [
|
||||||
|
"**"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"AUDIT_LOG": "${dir:trail}/audit.log",
|
||||||
|
"AUDIT_SPOOL": "${dir:spool}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "trail",
|
||||||
|
"path": "${dir:trail}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the audit trail, written nowhere else"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spool",
|
||||||
|
"path": "${dir:spool}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "events the trail could not take yet; after the bus gives one up, the only copy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trail",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spool",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"module": "avahi",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"avahi_status",
|
||||||
|
"avahi_browse",
|
||||||
|
"avahi_resolve",
|
||||||
|
"avahi_services"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "avahi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "daemon",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "avahi-daemon.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/avahi-tools",
|
||||||
|
"binary": "avahi-tools",
|
||||||
|
"loads": [
|
||||||
|
"avahi-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"module": "baserow",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "baserow"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "baserow",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": "${dir:state}/admin.secret"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the Baserow web UI and REST API, served by the image's own Caddy; a public name is the route's"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "uploaded files and media; the tables are in the database"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0755",
|
||||||
|
"owner": "9999:9999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "DATABASE_HOST=${bound:postgres-database:at}\nDATABASE_PORT=${bound:postgres-database:port}\nDATABASE_NAME=${bound:postgres-database:as}\nDATABASE_USER=${bound:postgres-database:as}\nDATABASE_PASSWORD_FILE=/run/secrets/database\nDISABLE_EMBEDDED_PSQL=true\nBASEROW_PUBLIC_URL=https://${bound:route:name}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "baserow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "baserow",
|
||||||
|
"image": "baserow/baserow@sha256:263ea6c4b72c9eccabcd975ffe9fdebf23913a293a514bec6a3897a5e0a5a080",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "baserow",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"80"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/baserow/data",
|
||||||
|
"${dir:state}/database.secret:/run/secrets/database:ro"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{\n \"password\": \"${secret:admin}\",\n \"host\": \"${bound:route:name}\"\n}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_BASEROW_URL": "http://127.0.0.1:${port:80}",
|
||||||
|
"MESH_BASEROW_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"module": "betterbird",
|
||||||
|
"version": "1",
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"prefs": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "// none",
|
||||||
|
"why": "Betterbird runs with its own defaults until a machine's assignment names a preference: one line of user_pref(\"name\", value); statements, which the module's user.js holds and Betterbird reads at its start (novox/hq issue 345)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"betterbird_status",
|
||||||
|
"betterbird_prefs",
|
||||||
|
"betterbird_user_js",
|
||||||
|
"betterbird_log",
|
||||||
|
"betterbird_restart",
|
||||||
|
"betterbird_check",
|
||||||
|
"betterbird_reminder_test"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "configuration-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:account-home}/.config/betterbird",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-js",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/betterbird/user.js",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "// Betterbird's user.js (module betterbird, novox/hq issue 345). Owned by the mesh: the node-engine\n// writes it here at every push, and the module's tools copy it whole into Betterbird's profile\n// (betterbird_user_js with apply, betterbird_restart), where Betterbird reads it at its start and lets it\n// win over the preferences Betterbird saves itself. Change the module's setting prefs, never this file or the profile's copy.\n//\n// Only comments and user_pref(\"name\", value); statements; the tools refuse anything else, and any\n// preference whose name can hold a credential.\n${setting:prefs}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/betterbird-tools",
|
||||||
|
"binary": "betterbird-tools",
|
||||||
|
"loads": [
|
||||||
|
"betterbird-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"module": "blueman",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"blueman_status",
|
||||||
|
"blueman_restart",
|
||||||
|
"blueman_check"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "blueman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/blueman-tools",
|
||||||
|
"binary": "blueman-tools",
|
||||||
|
"loads": [
|
||||||
|
"blueman-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"module": "bluetooth",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"bluetooth_controller",
|
||||||
|
"bluetooth_power",
|
||||||
|
"bluetooth_devices",
|
||||||
|
"bluetooth_scan",
|
||||||
|
"bluetooth_connect",
|
||||||
|
"bluetooth_disconnect",
|
||||||
|
"bluetooth_trust",
|
||||||
|
"bluetooth_pair",
|
||||||
|
"bluetooth_remove"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "stack",
|
||||||
|
"type": "package",
|
||||||
|
"package": "bluez"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "utilities",
|
||||||
|
"type": "package",
|
||||||
|
"package": "bluez-utils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "daemon",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "bluetooth.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/bluetooth-tools",
|
||||||
|
"binary": "bluetooth-tools",
|
||||||
|
"loads": [
|
||||||
|
"bluetooth-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"module": "build-agent",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "agent",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-build-agent",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": ["current", "kill", "pause", "resume"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"artifact-store",
|
||||||
|
"npm-package-registry"
|
||||||
|
],
|
||||||
|
"binds": {
|
||||||
|
"npm-package-registry": "${dir:mesh-state}/package-registry.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"npm-package-registry": "${dir:mesh-state}/package-registry.secret"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:mesh-state}/broker"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "workspace",
|
||||||
|
"path": "${dir:workspace}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "a build's working copy, cloned again for every build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workspace",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/build-agent.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MESH_BROKER_FILE=/run/mesh/broker\nMESH_NODE=${machine:name}\nMESH_REGISTRY=${bound:artifact-store:at}:${bound:artifact-store:port}\nMESH_PACKAGE_BINDING=/run/mesh/package-registry.json\nMESH_NPM_TOKEN_FILE=/run/mesh/package-registry.secret\nMESH_WORKSPACE=${dir:workspace}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-build-agent",
|
||||||
|
"artifact": "server",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:mesh-state}/build-agent.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:mesh-state}:/run/mesh:ro",
|
||||||
|
"${dir:workspace}:${dir:workspace}",
|
||||||
|
"/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"agent-env"
|
||||||
|
],
|
||||||
|
"network": "host"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "Dockerfile",
|
||||||
|
"compiles": "cmd/mesh-builder",
|
||||||
|
"context": {
|
||||||
|
"seat": "git",
|
||||||
|
"repository": "novox/mesh-controller",
|
||||||
|
"ref": "main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "GO_BASE",
|
||||||
|
"image": "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"arg": "ALPINE_BASE",
|
||||||
|
"image": "alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"module": "ca-trust",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "catrust",
|
||||||
|
"capabilities": [
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"internal-acme-ca"
|
||||||
|
],
|
||||||
|
"seats": [
|
||||||
|
{
|
||||||
|
"name": "the-mesh-trust-anchor",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "the-mesh-trust-anchor",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "anchor",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/anchor",
|
||||||
|
"mode": "0755",
|
||||||
|
"content": "#!/bin/sh\n# The mesh's internal certificate authority, trusted by this machine.\n#\n# Written by the mesh from the ca-trust module's manifest (novox/hq ADR 0147).\n# Editing it here lasts until the next apply.\n#\n# There is no prior trust to verify the fetch against \u2014 this is the thing that\n# establishes it \u2014 so it is made over the mesh's own private network, which is\n# what authenticates it (novox/hq ADR 0098, the same reasoning that lets the\n# route proxy fetch this root for itself). What comes back is checked here: a\n# body that is not a certificate is refused now, rather than believed and then\n# failed by whatever reads the trust store next.\nset -eu\n\nROOTS='https://${bound:internal-acme-ca:at}:${bound:internal-acme-ca:port}${bound:internal-acme-ca:roots}'\nANCHORS=/etc/ca-certificates/trust-source/anchors\nANCHOR=\"$ANCHORS/mesh-internal-ca.crt\"\n\n# Arch's layout, said out loud rather than assumed: a machine that keeps its\n# anchors elsewhere fails here, visibly, instead of writing a file nothing\n# reads. That failure is the signal that this belongs in the host, where one\n# operating system's difference lives (novox/hq ADR 0147, option 2).\n[ -d \"$ANCHORS\" ] || {\n\techo \"this machine keeps no trust anchors in $ANCHORS; ca-trust is written for that layout\" >&2\n\texit 1\n}\n\ncase \"${1:-}\" in\ninstall)\n\ttmp=$(mktemp)\n\ttrap 'rm -f \"$tmp\"' EXIT\n\t# The authority may still be starting, or this machine may have come up\n\t# before it: two minutes of asking, then an honest failure.\n\tn=0\n\twhile [ \"$n\" -lt 60 ]; do\n\t\tif curl --fail --silent --show-error --insecure --max-time 10 \\\n\t\t\t--output \"$tmp\" \"$ROOTS\" &&\n\t\t\tgrep -q 'BEGIN CERTIFICATE' \"$tmp\"; then\n\t\t\tinstall -m 0644 \"$tmp\" \"$ANCHOR\"\n\t\t\tupdate-ca-trust\n\t\t\texit 0\n\t\tfi\n\t\tn=$((n + 1))\n\t\tsleep 2\n\tdone\n\techo \"the authority at $ROOTS did not serve a certificate within two minutes\" >&2\n\texit 1\n\t;;\nremove)\n\t# What stopping the unit does, and therefore what being unassigned does.\n\trm -f \"$ANCHOR\"\n\tupdate-ca-trust\n\t;;\n*)\n\techo \"usage: $(basename \"$0\") install|remove\" >&2\n\texit 2\n\t;;\nesac\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/mesh-ca-trust.service",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[Unit]\nDescription=The mesh's internal certificate authority, trusted by this machine\n# novox/hq ADR 0147. Starting this unit places the mesh's root among this\n# machine's trust anchors; stopping it takes the root away again, which is what\n# the host does when the module is no longer assigned here.\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=${dir:state}/anchor install\nExecStop=${dir:state}/anchor remove\n\n[Install]\nWantedBy=multi-user.target\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trust",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "mesh-ca-trust.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"anchor",
|
||||||
|
"unit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"module": "claude-code",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "agent",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"mcp-endpoint"
|
||||||
|
],
|
||||||
|
"binds": {
|
||||||
|
"mcp-endpoint": "${dir:state}/mcp-endpoint.json"
|
||||||
|
},
|
||||||
|
"uses": [
|
||||||
|
"operator-channel"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
"servers",
|
||||||
|
"holdings",
|
||||||
|
"config",
|
||||||
|
"proposals"
|
||||||
|
],
|
||||||
|
"reads": [
|
||||||
|
"claude-licence-manager.bindings"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"claude_code_status",
|
||||||
|
"claude_code_render",
|
||||||
|
"claude_code_guard",
|
||||||
|
"claude_code_pull",
|
||||||
|
"claude_code_grant",
|
||||||
|
"claude_code_add_api_key",
|
||||||
|
"claude_code_registrations",
|
||||||
|
"claude_code_mcp_list",
|
||||||
|
"claude_code_mcp_register",
|
||||||
|
"claude_code_mcp_unregister",
|
||||||
|
"claude_code_skill_list",
|
||||||
|
"claude_code_skill_register",
|
||||||
|
"claude_code_skill_unregister",
|
||||||
|
"claude_code_agent_list",
|
||||||
|
"claude_code_agent_register",
|
||||||
|
"claude_code_agent_unregister",
|
||||||
|
"claude_code_command_list",
|
||||||
|
"claude_code_command_register",
|
||||||
|
"claude_code_command_unregister",
|
||||||
|
"claude_code_hook_list",
|
||||||
|
"claude_code_hook_register",
|
||||||
|
"claude_code_hook_unregister",
|
||||||
|
"claude_code_output_style_list",
|
||||||
|
"claude_code_output_style_register",
|
||||||
|
"claude_code_output_style_unregister",
|
||||||
|
"claude_code_instructions_list",
|
||||||
|
"claude_code_instructions_register",
|
||||||
|
"claude_code_instructions_unregister",
|
||||||
|
"claude_code_instructions_propose",
|
||||||
|
"claude_code_proposals",
|
||||||
|
"claude_code_settings_get",
|
||||||
|
"claude_code_settings_set",
|
||||||
|
"claude_code_settings_clear",
|
||||||
|
"claude_code_permission_add",
|
||||||
|
"claude_code_permission_remove",
|
||||||
|
"claude_code_config_list",
|
||||||
|
"claude_code_config_show",
|
||||||
|
"claude_code_config_status",
|
||||||
|
"claude_code_config_import",
|
||||||
|
"claude_code_home_show",
|
||||||
|
"claude_code_home_remove",
|
||||||
|
"claude_code_home_removed",
|
||||||
|
"claude_code_home_restore",
|
||||||
|
"claude_code_judge"
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "agent-home",
|
||||||
|
"path": "${dir:agent-home}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the operator's agent's own files: its memory, history and projects"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "claude-code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "managed",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/claude-code",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "start",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/usr/local/bin/claude-agent",
|
||||||
|
"mode": "0755",
|
||||||
|
"content": "#!/bin/sh\n# The mesh's (module claude-code, novox/hq ADR 0266): start the agent as the account agents run as on this\n# machine. Written whole at every push: an edit here is overwritten.\nagent='${machine:agent-account}'\nif [ \"$(id -un)\" = \"$agent\" ]; then\n\texec claude \"$@\"\nfi\n# Another account, the operator's, becomes the agent's through its own sudo: the person is the authority. The\n# operator's override of the guard travels only when it is set in this shell, as it would reach claude.\nif [ -n \"$MESH_GUARD_OVERRIDE\" ]; then\n\texec sudo -iu \"$agent\" env MESH_GUARD_OVERRIDE=\"$MESH_GUARD_OVERRIDE\" claude \"$@\"\nfi\nexec sudo -iu \"$agent\" claude \"$@\"\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent-account-name",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/claude-code/agent-account",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "${machine:agent-account}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent-account",
|
||||||
|
"type": "user",
|
||||||
|
"name": "${machine:agent-account}",
|
||||||
|
"home": "${machine:agent-home}",
|
||||||
|
"root": "${machine:agent-root}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent-home",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:agent-home}/.claude",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "${machine:agent-account}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "facts",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/facts.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"content": "{\n \"node\": \"${machine:name}\",\n \"console\": \"http://127.0.0.1:${bound:mcp-endpoint:port}/mcp\"\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"role\": \"\",\n \"mcp_servers\": {},\n \"managed_settings\": {},\n \"instructions\": {},\n \"output_styles\": {},\n \"skills\": {},\n \"commands\": {},\n \"agents\": {}\n}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "zsh",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The agent's session (module claude-code, novox/hq ADR 0266): where this machine names an account of the\n# agents' own, claude in an interactive zsh is claude-agent, which starts the session as that account. Where\n# agents run as the operator's account the file names that account, and nothing is added. claude-agent runs\n# the real claude: exec, sudo and its sh see no alias.\nif [[ -r /etc/claude-code/agent-account ]]; then\n\t() {\n\t\tlocal agent=$(</etc/claude-code/agent-account)\n\t\tif [[ -n $agent && $agent != $USERNAME ]]; then\n\t\t\talias claude=claude-agent\n\t\tfi\n\t}\nfi\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/claude-code",
|
||||||
|
"binary": "claude-code",
|
||||||
|
"loads": [
|
||||||
|
"claude-code"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_CLAUDE_CODE_STATE": "${dir:state}",
|
||||||
|
"MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json",
|
||||||
|
"MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"module": "claude-licence-manager",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "licmgr",
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"secret"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "claude_licences"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret",
|
||||||
|
"secret": {
|
||||||
|
"grant-key": "${dir:state}/grant.key"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seats": [
|
||||||
|
{
|
||||||
|
"name": "anthropic-licence-manager",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"licences",
|
||||||
|
"bindings",
|
||||||
|
"bind",
|
||||||
|
"switch",
|
||||||
|
"release",
|
||||||
|
"refresh",
|
||||||
|
"usage",
|
||||||
|
"adopt",
|
||||||
|
"current",
|
||||||
|
"public-key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "anthropic-licence-manager",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"licences",
|
||||||
|
"bindings",
|
||||||
|
"bind",
|
||||||
|
"switch",
|
||||||
|
"release",
|
||||||
|
"refresh",
|
||||||
|
"usage",
|
||||||
|
"adopt",
|
||||||
|
"current",
|
||||||
|
"public-key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"licence.adopted",
|
||||||
|
"licence.refused",
|
||||||
|
"licence.failing",
|
||||||
|
"usage.read"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
"bindings"
|
||||||
|
],
|
||||||
|
"reads": [
|
||||||
|
"claude-code.holdings"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-url",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/database.url",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"cadence_minutes\": 240,\n \"floor_minutes\": 60,\n \"failures_to_notify\": 3,\n \"cooldown_hours\": 24,\n \"refresh_warn_days\": 3\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prepare",
|
||||||
|
"type": "process",
|
||||||
|
"name": "claude-licence-manager-prepare",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"./claude-licence-manager",
|
||||||
|
"prepare"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/claude-licence-manager",
|
||||||
|
"binary": "claude-licence-manager",
|
||||||
|
"loads": [
|
||||||
|
"claude-licence-manager"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url",
|
||||||
|
"MESH_LICENCE_STATE": "${dir:state}",
|
||||||
|
"MESH_LICENCE_KEY_FILE": "${dir:state}/grant.key",
|
||||||
|
"MESH_LICENCE_SETTINGS": "${dir:state}/settings.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"module": "clipmenu",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-clipboard",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"history",
|
||||||
|
"copy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"clipmenu_paste",
|
||||||
|
"clipmenu_clear",
|
||||||
|
"clipmenu_delete"
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"variables": {
|
||||||
|
"CM_SELECTIONS": "clipboard",
|
||||||
|
"CM_MAX_CLIPS": "500",
|
||||||
|
"CM_HISTLENGTH": "15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "xinitrc",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The clipboard's history (module clipmenu, novox/hq ADR 0208): clipmenud collects every copy from\n# here on, once per session. It keeps the history in the account's runtime directory, so a reboot\n# forgets it, and with it every password that was ever copied.\n# Through the module's xsel, which reads only a selection offering text (see the README).\nPATH=\"/usr/local/lib/mesh-clipmenu:$PATH\" clipmenud &\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "clipmenu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "greenclip",
|
||||||
|
"type": "package",
|
||||||
|
"package": "rofi-greenclip",
|
||||||
|
"absent": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-only",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/usr/local/lib/mesh-clipmenu/xsel",
|
||||||
|
"mode": "0755",
|
||||||
|
"content": "#!/bin/sh\n# xsel as clipmenud sees it, written by the mesh (module clipmenu). Replaced at every push.\n#\n# clipmenud records text, and reads a selection with `timeout 1 xsel -o`. A selection holding an\n# image (a screenshot copied as image/png) is sent in pieces; one second is too short for megabytes,\n# timeout kills xsel half way, and the program owning the image waits for ever for a reader that is\n# gone. From then on nothing can ask the clipboard anything: pastes hang, and an Electron app that\n# asks on its main thread freezes. So a read goes ahead only when the selection offers text.\ncase \" $* \" in\n*\" -o \"* | *\" --output \"*)\n\tselection=clipboard\n\tcase \" $* \" in\n\t*\" --primary \"* | *\" -p \"*) selection=primary ;;\n\t*\" --secondary \"* | *\" -s \"*) selection=secondary ;;\n\tesac\n\ttargets=$(timeout 1 xclip -selection \"$selection\" -t TARGETS -o 2>/dev/null) || exit 1\n\tprintf '%s\\n' \"$targets\" | grep -qxE 'UTF8_STRING|STRING|TEXT|text/plain(;charset=utf-8)?' || exit 1\n\t;;\nesac\nexec /usr/bin/xsel \"$@\"\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/clipmenu-tools",
|
||||||
|
"binary": "clipmenu-tools",
|
||||||
|
"loads": [
|
||||||
|
"clipmenu-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"seat": "node-display-session",
|
||||||
|
"kind": "config",
|
||||||
|
"content": "# The clipboard's history key (module clipmenu, novox/hq ADR 0208). Owned by the mesh: replaced at\n# every push. clipmenu shows the history through `dmenu`, the node's dmenu-compatible command, which\n# the holder of node-launcher answers (rofi on the workstations); the chosen entry is put back on the\n# clipboard.\nbindsym $mod+period exec --no-startup-id clipmenu -p Clipboard\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"module": "confluence",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "confl",
|
||||||
|
"own-secrets": {
|
||||||
|
"token": "${dir:state}/token"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/config.json",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{}",
|
||||||
|
"mode": "0600"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_CONFLUENCE_TOKEN_FILE": "${dir:state}/token",
|
||||||
|
"MESH_CONFLUENCE_CONFIG_FILE": "${dir:state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"module": "cups",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"cups_printers",
|
||||||
|
"cups_queue",
|
||||||
|
"cups_cancel",
|
||||||
|
"cups_print",
|
||||||
|
"cups_default",
|
||||||
|
"cups_resume",
|
||||||
|
"cups_drivers"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "cups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "driverless",
|
||||||
|
"type": "package",
|
||||||
|
"package": "cups-filters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "socket",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "cups.socket",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scheduler",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "cups.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/cups-tools",
|
||||||
|
"binary": "cups-tools",
|
||||||
|
"loads": [
|
||||||
|
"cups-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"module": "dbus",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-message-bus",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"bus.stalled",
|
||||||
|
"bus.recovered",
|
||||||
|
"bus.restarted",
|
||||||
|
"service.appeared",
|
||||||
|
"service.left",
|
||||||
|
"policy.denied"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"dbus_names",
|
||||||
|
"dbus_introspect",
|
||||||
|
"dbus_monitor",
|
||||||
|
"dbus_check",
|
||||||
|
"dbus_health"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "dbus",
|
||||||
|
"type": "package",
|
||||||
|
"package": "dbus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "broker",
|
||||||
|
"type": "package",
|
||||||
|
"package": "dbus-broker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "broker-units",
|
||||||
|
"type": "package",
|
||||||
|
"package": "dbus-broker-units"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "system-bus",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "dbus-broker.service",
|
||||||
|
"state": "running"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/dbus-tools",
|
||||||
|
"binary": "dbus-tools",
|
||||||
|
"loads": [
|
||||||
|
"dbus-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"module": "de-spiegel",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "spiegel",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "de-spiegel",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"smtp-user": "${dir:state}/smtp-user.secret",
|
||||||
|
"smtp-pass": "${dir:state}/smtp-pass.secret"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 35621,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the de-spiegel site and its /contact endpoint over http; its public name is a route grant, and route-proxy reaches it on this published port"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "SMTP_AUTH_USER=${secret:smtp-user}\nSMTP_AUTH_PASS=${secret:smtp-pass}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "de-spiegel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "de-spiegel",
|
||||||
|
"image": "registry-api.novox.be/novox/de-spiegel@sha256:e144b72ce9c145870470d765343549f2c60211728cd118b9ff0e4029f36342ba",
|
||||||
|
"network": "de-spiegel",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"35621"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "the application's own code reads SMTP_AUTH_USER/PASS from the environment (de-spiegel server/index.js); converting is that repository's change",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"module": "desk-channel",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "desk",
|
||||||
|
"runs-as": "desk-channel",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "channel",
|
||||||
|
"scope": "mesh",
|
||||||
|
"kind": "desktop",
|
||||||
|
"capabilities": [
|
||||||
|
"deliver",
|
||||||
|
"silent",
|
||||||
|
"loud",
|
||||||
|
"edit",
|
||||||
|
"private",
|
||||||
|
"choice",
|
||||||
|
"exact-render"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intake",
|
||||||
|
"scope": "mesh",
|
||||||
|
"kind": "desktop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"dunst.action-chosen",
|
||||||
|
"dunst.started",
|
||||||
|
"dunst.paused"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:node-notifier.send"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
{
|
||||||
|
"name": "shown",
|
||||||
|
"ttl-seconds": 2592000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:state}/broker"
|
||||||
|
},
|
||||||
|
"secrets-owner": "desk-channel",
|
||||||
|
"tools": [
|
||||||
|
"desk_channel_status"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"type": "user",
|
||||||
|
"name": "desk-channel",
|
||||||
|
"shell": "/usr/bin/nologin",
|
||||||
|
"home": "/var/lib/desk-channel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "desk-channel",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "desk-channel",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"desktop-machines\": []\n}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/desk-channel",
|
||||||
|
"binary": "desk-channel",
|
||||||
|
"loads": [
|
||||||
|
"desk-channel"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_DESK_SETTINGS": "${dir:state}/settings.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"module": "disk-load",
|
||||||
|
"version": "1",
|
||||||
|
"tools": [
|
||||||
|
"disk_load",
|
||||||
|
"disk_top",
|
||||||
|
"disk_filesystems",
|
||||||
|
"disk_devices"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/disk-load",
|
||||||
|
"binary": "disk-load",
|
||||||
|
"loads": [
|
||||||
|
"disk-load"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"module": "distribution",
|
||||||
|
"version": "1",
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "artifact-store",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "mesh-artifact-store",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "/var/lib/mesh/registry/broker"
|
||||||
|
},
|
||||||
|
"serves": {
|
||||||
|
"artifact-store": {
|
||||||
|
"port": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "registry",
|
||||||
|
"port": 5000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "every machine pulls images and artifacts from here"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "registry",
|
||||||
|
"path": "${dir:registry-data}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"backup": "none",
|
||||||
|
"measure": "shallow",
|
||||||
|
"why": "the artifact store: every image is built again from its source, and too large to copy every night (ADR 0214 left it out)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh/registry",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "registry-data",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh-registry",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-registry",
|
||||||
|
"image": "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373",
|
||||||
|
"ports": [
|
||||||
|
"5000:5000"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/mesh-registry:/var/lib/registry"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"REGISTRY_STORAGE_DELETE_ENABLED": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collect",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-registry-collect",
|
||||||
|
"image": "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373",
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/mesh-registry:/var/lib/registry"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"garbage-collect",
|
||||||
|
"/etc/docker/registry/config.yml"
|
||||||
|
],
|
||||||
|
"schedule": "30 3 * * *",
|
||||||
|
"while-stopped": [
|
||||||
|
"store"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"module": "dmenu",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"dmenu_menu",
|
||||||
|
"dmenu_session"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "dmenu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/dmenu-tools",
|
||||||
|
"binary": "dmenu-tools",
|
||||||
|
"loads": [
|
||||||
|
"dmenu-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"module": "docker-compose",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"docker_compose_projects",
|
||||||
|
"docker_compose_ps",
|
||||||
|
"docker_compose_logs",
|
||||||
|
"docker_compose_config",
|
||||||
|
"docker_compose_up",
|
||||||
|
"docker_compose_down",
|
||||||
|
"docker_compose_restart",
|
||||||
|
"docker_compose_pull",
|
||||||
|
"docker_compose_job"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "docker-compose"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/docker-compose-tools",
|
||||||
|
"binary": "docker-compose-tools",
|
||||||
|
"loads": [
|
||||||
|
"docker-compose-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"module": "docker",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager",
|
||||||
|
"privileged"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.images"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-container-runtime",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"docker_list",
|
||||||
|
"docker_inspect",
|
||||||
|
"docker_resolv_conf",
|
||||||
|
"docker_logs",
|
||||||
|
"docker_secrets_in_logs",
|
||||||
|
"docker_secrets_in_events",
|
||||||
|
"docker_stats",
|
||||||
|
"docker_start",
|
||||||
|
"docker_stop",
|
||||||
|
"docker_restart",
|
||||||
|
"docker_top",
|
||||||
|
"docker_images",
|
||||||
|
"docker_prune",
|
||||||
|
"docker_prune_images",
|
||||||
|
"docker_disk_usage",
|
||||||
|
"docker_networks",
|
||||||
|
"docker_volumes",
|
||||||
|
"docker_events",
|
||||||
|
"docker_daemon_config",
|
||||||
|
"docker_unlabelled",
|
||||||
|
"docker_problems",
|
||||||
|
"docker_ports"
|
||||||
|
],
|
||||||
|
"replaces": {
|
||||||
|
"docker_resolv_conf": [
|
||||||
|
"docker exec cat /etc/resolv.conf"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "docker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "buildx",
|
||||||
|
"type": "package",
|
||||||
|
"package": "docker-buildx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "socket",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "docker.socket",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "daemon",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/docker/daemon.json",
|
||||||
|
"mode": "0644",
|
||||||
|
"into": "json",
|
||||||
|
"content": "{\"live-restore\": true, \"insecure-registries\": [\"${seat:mesh-artifact-store:reach}\"]}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "docker.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"reload-on": [
|
||||||
|
"daemon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prune-service",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/docker-prune.service",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Generated by the mesh. Do not edit — module docker writes this file and replaces it at every push.\n[Unit]\nDescription=Prune dangling images and unused build cache (the mesh's docker module)\n# Never volumes, never a container, never an image a container uses: dangling\n# images and build cache nothing refers to, unused for a week. What a person\n# prunes beyond that is docker_prune's, by hand.\nAfter=docker.service\nConditionPathExists=/run/docker.sock\n\n[Service]\nType=oneshot\nNice=19\nIOSchedulingClass=idle\nExecStart=/usr/bin/docker image prune --force --filter until=168h\nExecStart=/usr/bin/docker builder prune --force --filter until=168h\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prune-timer",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/docker-prune.timer",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Generated by the mesh. Do not edit — module docker writes this file and replaces it at every push.\n[Unit]\nDescription=Weekly prune of dangling images and unused build cache (the mesh's docker module)\n\n[Timer]\nOnCalendar=weekly\nRandomizedDelaySec=1h\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prune",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "docker-prune.timer",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"prune-service",
|
||||||
|
"prune-timer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/docker-tools",
|
||||||
|
"binary": "docker-tools",
|
||||||
|
"loads": [
|
||||||
|
"docker-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"module": "dunst",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-notifier",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"send",
|
||||||
|
"history"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"dunst_pause",
|
||||||
|
"dunst_resume",
|
||||||
|
"dunst_close_all",
|
||||||
|
"dunst_rules",
|
||||||
|
"dunst_count",
|
||||||
|
"dunst_reload"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"font-size": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 10,
|
||||||
|
"why": "10 points of Inter reads well on a screen of about 100 DPI; a denser screen needs a larger size"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 250,
|
||||||
|
"why": "250 pixels fits a title of about forty characters at 10 points; a larger font needs a wider notification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "dunst"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "client",
|
||||||
|
"type": "package",
|
||||||
|
"package": "libnotify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "configuration-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:account-home}/.config/dunst",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dropins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:account-home}/.config/dunst/dunstrc.d",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "configuration",
|
||||||
|
"trusted": false,
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/dunst/dunstrc",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# dunst, the notifier (module dunst, novox/hq ADR 0208). Owned by the mesh: this file is replaced\n# at every push. Adopted from the laptop's file of 2026-10-04 (the two workstations differed in\n# position, transparency and corner radius; the laptop's square, opaque, top-right one matches the\n# rest of the desktop). Only what differs from dunst's defaults, and what the desktop relies on.\n#\n# Other modules' rules go in ~/.config/dunst/dunstrc.d/*.conf, which dunst reads after this file,\n# so a drop-in outranks it. dunst is started by D-Bus on the first notification: nothing starts it.\n# When the mesh rewrites this file (a font-size or width setting changed), it tells a running dunst\n# to read it again (its unit's reload, dunstctl reload); what is on screen stays.\n\n[global]\n monitor = 0\n follow = none\n\n # Geometry\n width = ${setting:width}\n height = (0, 300)\n origin = top-right\n offset = (10, 50)\n notification_limit = 20\n\n progress_bar = true\n progress_bar_height = 10\n progress_bar_frame_width = 1\n progress_bar_min_width = 150\n progress_bar_max_width = 300\n\n indicate_hidden = yes\n transparency = 0\n separator_height = 2\n padding = 8\n horizontal_padding = 8\n text_icon_padding = 0\n frame_width = 3\n frame_color = \"#de5200\"\n gap_size = 0\n separator_color = frame\n sort = yes\n corner_radius = 0\n\n # Text: the interface face (research 026/04)\n font = Inter ${setting:font-size}\n line_height = 0\n markup = full\n format = \"<b>%s</b>\\n%b\"\n alignment = left\n vertical_alignment = center\n show_age_threshold = 60\n ellipsize = middle\n ignore_newline = no\n stack_duplicates = true\n hide_duplicate_count = false\n show_indicators = yes\n\n # Icons, from the desktop's icon theme\n enable_recursive_icon_lookup = true\n icon_theme = Adwaita\n icon_position = left\n min_icon_size = 32\n max_icon_size = 128\n\n # History\n sticky_history = yes\n history_length = 20\n\n # The context menu is rofi (installed by the rofi module); -no-custom keeps typed text from becoming a choice. Not `dmenu`, the node-launcher's command in\n # ~/.local/bin: dunst runs in the account's service manager, whose PATH does not hold that\n # directory (seen on the laptop on 2026-10-10), so the menu never opened. Links open in the\n # desktop's default browser.\n dmenu = rofi -dmenu -no-custom -p dunst\n browser = /usr/bin/xdg-open\n always_run_script = true\n\n title = Dunst\n class = Dunst\n ignore_dbusclose = false\n\n # A tap answers (the touchpads have no middle button). do_action runs the notification's only\n # action, or opens the context menu when it has several; without actions it opens its one link,\n # if it has one. The notification then closes; a menu dismissed with Escape leaves it shown.\n mouse_left_click = do_action, close_current\n mouse_middle_click = do_action, close_current\n mouse_right_click = close_all\n\n[urgency_low]\n background = \"#000000\"\n foreground = \"#ffffff\"\n timeout = 10\n\n[urgency_normal]\n background = \"#000000\"\n foreground = \"#ffffff\"\n timeout = 10\n\n[urgency_critical]\n background = \"#000000\"\n foreground = \"#ffffff\"\n frame_color = \"#ff0000\"\n timeout = 0\n\n# What is shown once is never kept (the review of 2026-10-09): a notification marked secret (a link's\n# code, category mesh.secret) and any transient one leave no entry in the history, which\n# node-notifier.history serves to every caller of the mesh's verbs.\n[mesh-secret]\n category = \"mesh.secret\"\n history_ignore = yes\n\n[transient-history-ignore]\n match_transient = yes\n history_ignore = yes\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "notifier",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "dunst.service",
|
||||||
|
"scope": "user",
|
||||||
|
"user": "${machine:account}",
|
||||||
|
"reload-on": [
|
||||||
|
"configuration"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/dunst-tools",
|
||||||
|
"binary": "dunst-tools",
|
||||||
|
"loads": [
|
||||||
|
"dunst-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"emits": [
|
||||||
|
"action-chosen",
|
||||||
|
"started",
|
||||||
|
"paused"
|
||||||
|
],
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"seat": "node-display-session",
|
||||||
|
"kind": "config",
|
||||||
|
"content": "# The notifications' key (module dunst, novox/hq ADR 0208). Owned by the mesh: replaced at every push.\n# Super+N opens the menu (rofi) of the actions and links of every notification on screen, so one is\n# chosen from the keyboard as a tap chooses it on the notification.\nbindsym $mod+n exec --no-startup-id dunstctl context\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"module": "fail2ban",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"firewall"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-intrusion-prevention",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"status",
|
||||||
|
"banned",
|
||||||
|
"ban",
|
||||||
|
"unban"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"fail2ban_settings"
|
||||||
|
],
|
||||||
|
"jailing": {
|
||||||
|
"into": "/etc/fail2ban/jail.d/mesh.conf",
|
||||||
|
"filter-into": "/etc/fail2ban/filter.d"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "fail2ban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jail-d",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/fail2ban/jail.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "action-d",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/fail2ban/action.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "filter-d",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/fail2ban/filter.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "run-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/run/fail2ban",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jail-local",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/fail2ban/jail.local",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[INCLUDES]\n\nbefore = paths-arch.conf\n\n[DEFAULT]\n\n# Never act on the machine itself or on a tunnel peer: the mesh's private range is\n# ${machine:mesh-range}, named here rather than written as a value the module cannot\n# know (novox/hq ADR 0112). Without this, fail2ban could ban the mesh's own nodes.\n# **A ban list never holds a neighbour.** The mesh's own range is named rather than written\n# (novox/hq ADR 0112), and every private range beside it: a source on one is somebody's own\n# network, not the internet. On a machine behind a router that reflects local traffic, every\n# client in the house arrives as the gateway's address — so one mistyped local request banned\n# 192.168.1.1 on the home server and would have cut the whole house off from it (ADR 0186).\nignoreip = 127.0.0.1/8 ::1 ${machine:mesh-range} 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 fc00::/7 fe80::/10\n\n# Three failures in a day ban for a day (novox/hq ADR 0179). The attackers this mesh sees pace\n# themselves at one try every ten minutes, under any ten-minute window; a day's window counts\n# them, and a day's ban costs a person who mistyped three times once, from one address, while\n# the mesh's own range is never banned at all.\nbantime = 1d\nfindtime = 1d\nmaxretry = 3\n\n# Ban through iptables, not through a firewall front-end the machine may not have. ufw is\n# installed on two of this mesh's machines and absent on the other two, and fail2ban finds out\n# only at ban time: the service reports healthy, the jail counts the attempt, the ban command\n# exits 127, and nothing is blocked. Proven on 2026-09-28 -- 'ufw: command not found' on a\n# machine the mesh reported as protected.\n#\n# The action below is this module's own, already used by the recidive jail on every machine\n# here, and it bans in DOCKER-USER as well as INPUT, so a container's published port is\n# covered too.\nbanaction = iptables-allports-dualchain\nbanaction_allports = iptables-allports-dualchain\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jail-sshd",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/fail2ban/jail.d/sshd.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\nmaxretry = 3\nfindtime = 1d\nbantime = 1d\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/var/log/fail2ban.log",
|
||||||
|
"mode": "0640",
|
||||||
|
"create-once": true,
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jail-recidive",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/fail2ban/jail.d/recidive.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[recidive]\nenabled = true\nlogpath = /var/log/fail2ban.log\n# Ban in both INPUT (host services like SSH) and DOCKER-USER (container services)\nbanaction = iptables-allports-dualchain\n# Banned twice in two weeks, by any jail, is banned for four (novox/hq ADR 0179).\nbantime = 4w\nfindtime = 2w\nmaxretry = 2\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "action-dualchain",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/fail2ban/action.d/iptables-allports-dualchain.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Fail2Ban action: ban in both INPUT and DOCKER-USER chains\n# Used by recidive to block repeat offenders from both host and Docker services\n\n[INCLUDES]\n\nbefore = iptables.conf\n\n[Definition]\n\ntype = allports\n\nactionstart = { <iptables> -C f2b-<name> -j <returntype> >/dev/null 2>&1; } || { <iptables> -N f2b-<name> || true; <iptables> -A f2b-<name> -j <returntype>; }\n { <iptables> -C INPUT -p <protocol> -j f2b-<name> >/dev/null 2>&1; } || { <iptables> -I INPUT -p <protocol> -j f2b-<name>; }\n { <iptables> -C DOCKER-USER -p <protocol> -j f2b-<name> >/dev/null 2>&1; } || { <iptables> -I DOCKER-USER -p <protocol> -j f2b-<name>; }\n\nactionstop = <iptables> -D INPUT -p <protocol> -j f2b-<name> 2>/dev/null || true\n <iptables> -D DOCKER-USER -p <protocol> -j f2b-<name> 2>/dev/null || true\n <iptables> -F f2b-<name>\n <iptables> -X f2b-<name>\n\nactioncheck = <iptables> -n -L f2b-<name> >/dev/null\n\nactionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>\n\nactionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>\n\n[Init]\n\nchain = INPUT\nname = default\nprotocol = tcp\nblocktype = REJECT --reject-with icmp-port-unreachable\nreturntype = RETURN\nlockingopt = -w\niptables = iptables <lockingopt>\n\n[Init?family=inet6]\n\nblocktype = REJECT --reject-with icmp6-port-unreachable\niptables = ip6tables <lockingopt>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "logrotate",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/logrotate.d/fail2ban",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "/var/log/fail2ban.log {\n missingok\n notifempty\n postrotate\n /usr/bin/fail2ban-client flushlogs >/dev/null || true\n endscript\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "run",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "fail2ban.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"jail-local",
|
||||||
|
"jail-sshd",
|
||||||
|
"jail-recidive",
|
||||||
|
"action-dualchain",
|
||||||
|
"composed-jails"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/fail2ban-tools",
|
||||||
|
"binary": "fail2ban-tools",
|
||||||
|
"loads": [
|
||||||
|
"fail2ban-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"module": "feh",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"feh_set",
|
||||||
|
"feh_current"
|
||||||
|
],
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "xinitrc",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The wallpaper (module feh, novox/hq ADR 0208): the declared one, set once per session.\n\"$HOME/.fehbg\"\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "feh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wallpapers",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "${machine:account-home}/.local/share/feh/wallpapers",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"artifact": "wallpapers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fehbg",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.fehbg",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755",
|
||||||
|
"content": "#!/bin/sh\n# The wallpaper (module feh, novox/hq ADR 0208). Owned by the mesh: replaced at every push. The\n# session's start runs it, and so may anything that wants the declared wallpaper back. The image is\n# the module's own, in ~/.local/share/feh/wallpapers. feh_set changes the wallpaper for a session\n# without touching this file.\nfeh --no-fehbg --bg-fill \"$HOME/.local/share/feh/wallpapers/default.jpg\"\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "wallpapers",
|
||||||
|
"kind": "archive",
|
||||||
|
"from": "wallpaper"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/feh-tools",
|
||||||
|
"binary": "feh-tools",
|
||||||
|
"loads": [
|
||||||
|
"feh-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"seat": "node-display-session",
|
||||||
|
"kind": "config",
|
||||||
|
"content": "# The wallpaper's key (module feh, novox/hq ADR 0208). Owned by the mesh: replaced at every push.\n# It puts the declared wallpaper back, after a monitor change or a wallpaper set for the session.\nbindsym $mod+Shift+b exec --no-startup-id ~/.fehbg\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"module": "flatpak",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"flatpak_list",
|
||||||
|
"flatpak_runtimes",
|
||||||
|
"flatpak_remotes",
|
||||||
|
"flatpak_updates",
|
||||||
|
"flatpak_unused",
|
||||||
|
"flatpak_disk_usage",
|
||||||
|
"flatpak_install",
|
||||||
|
"flatpak_remove",
|
||||||
|
"flatpak_update",
|
||||||
|
"flatpak_remove_unused",
|
||||||
|
"flatpak_job"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "flatpak"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/flatpak-tools",
|
||||||
|
"binary": "flatpak-tools",
|
||||||
|
"loads": [
|
||||||
|
"flatpak-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"module": "fonts",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"fonts_families",
|
||||||
|
"fonts_match",
|
||||||
|
"fonts_glyph",
|
||||||
|
"fonts_sources",
|
||||||
|
"fonts_config",
|
||||||
|
"fonts_cache_rebuild"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "monospace",
|
||||||
|
"type": "package",
|
||||||
|
"package": "ttf-jetbrains-mono-nerd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interface",
|
||||||
|
"type": "package",
|
||||||
|
"package": "inter-font"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "symbols",
|
||||||
|
"type": "package",
|
||||||
|
"package": "ttf-nerd-fonts-symbols"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "noto",
|
||||||
|
"type": "package",
|
||||||
|
"package": "noto-fonts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emoji",
|
||||||
|
"type": "package",
|
||||||
|
"package": "noto-fonts-emoji"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "defaults",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/fontconfig/conf.d/50-mesh-fonts.conf",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "<?xml version=\"1.0\"?>\n<!DOCTYPE fontconfig SYSTEM \"urn:fontconfig:fonts.dtd\">\n<!--\n The mesh's file (module fonts, novox/hq research 026/04): which face each generic family means for\n this account, so every program that asks for monospace, sans-serif, serif or emoji gets the same\n face. Replaced at every push. Other files in this directory are yours.\n-->\n<fontconfig>\n <description>The mesh: the faces monospace, sans-serif, serif and emoji mean</description>\n\n <!--\n Families the desktop's files named before this module, mapped to monospace for as long as they\n are not installed: a configuration still naming one gets the chosen monospace face rather than\n fontconfig's last resort, sans-serif. Where the family is still installed, it is used as before.\n Placed first, so the monospace they lead to is resolved by the rule below.\n -->\n <alias><family>Hack Nerd Font</family><accept><family>monospace</family></accept></alias>\n <alias><family>MesloLGS NF</family><accept><family>monospace</family></accept></alias>\n <alias><family>Iosevka Nerd Font</family><accept><family>monospace</family></accept></alias>\n <alias><family>JetBrains Mono Nerd Font</family><accept><family>JetBrainsMono Nerd Font</family></accept></alias>\n\n <!--\n The decided faces, bound the same as the request: a program asking for monospace asks strongly,\n and a face added weakly loses to the distribution's own choice (measured with fontconfig 2.18).\n -->\n <alias binding=\"same\"><family>monospace</family><prefer><family>JetBrainsMono Nerd Font</family></prefer></alias>\n <alias binding=\"same\"><family>sans-serif</family><prefer><family>Inter</family><family>Noto Sans</family></prefer></alias>\n <alias binding=\"same\"><family>system-ui</family><prefer><family>Inter</family></prefer></alias>\n <alias binding=\"same\"><family>serif</family><prefer><family>Noto Serif</family></prefer></alias>\n <alias binding=\"same\"><family>emoji</family><prefer><family>Noto Color Emoji</family></prefer></alias>\n\n <!--\n Fallbacks for any face: an icon a face lacks comes from the Nerd Fonts symbols, an emoji from\n Noto Color Emoji. Appended last, so they never displace a face that has the character.\n -->\n <match target=\"pattern\">\n <edit name=\"family\" mode=\"append_last\"><string>Symbols Nerd Font</string></edit>\n <edit name=\"family\" mode=\"append_last\"><string>Noto Color Emoji</string></edit>\n </match>\n</fontconfig>\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/fonts-tools",
|
||||||
|
"binary": "fonts-tools",
|
||||||
|
"loads": [
|
||||||
|
"fonts-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"module": "forticlient",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "its adapter routes the company's domains on the machine a person works on: a build that breaks it cuts the person off from work names until a person pushes the next (hq ADR 0236, ADR 0247)"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display",
|
||||||
|
"split-dns"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"forticlient_status",
|
||||||
|
"forticlient_restart",
|
||||||
|
"forticlient_check"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "scheduler",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "forticlient.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "split-dns",
|
||||||
|
"type": "process",
|
||||||
|
"name": "forticlient-split-dns",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./forticlient-tools",
|
||||||
|
"split-dns"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/forticlient-tools",
|
||||||
|
"binary": "forticlient-tools",
|
||||||
|
"loads": [
|
||||||
|
"forticlient-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
{
|
||||||
|
"module": "gitea",
|
||||||
|
"version": "1",
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route",
|
||||||
|
"secret"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "gitea"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"web": {
|
||||||
|
"label": "git",
|
||||||
|
"endpoint": "web"
|
||||||
|
},
|
||||||
|
"internal-api-refused": {
|
||||||
|
"label": "git",
|
||||||
|
"path": "/api/internal",
|
||||||
|
"deny": true,
|
||||||
|
"priority": 100000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret",
|
||||||
|
"secret": {
|
||||||
|
"internal-token": "${dir:state}/internal-token.secret",
|
||||||
|
"admin": "${dir:state}/admin.secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"repo.created",
|
||||||
|
"issue.opened",
|
||||||
|
"pull.merged",
|
||||||
|
"pull.updated",
|
||||||
|
"pull.closed"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mesh-controller.checked"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 3000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the forge, over http"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ssh",
|
||||||
|
"port": 22,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "git over ssh, gitea's own unmodified sshd. Published on the machine's own side at 222, the mesh's fixed public convention \u2014 not 22, which the machine's own daemon holds and a module does not take"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"npm-package-registry": {
|
||||||
|
"scheme": "http",
|
||||||
|
"port": 3000,
|
||||||
|
"npm-path": "/api/packages/novox/npm/"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"scheme": "http",
|
||||||
|
"port": 3000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"npm-package-registry": "${dir:grants}/npm.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"npm-package-registry": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "npm-package-registry",
|
||||||
|
"scope": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "git",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every repository, its issues and attachments, and the package registry"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"npm-package-registry": {
|
||||||
|
"class": "rebuildable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "a consumer's packages are published again from its source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-state",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${dir:mesh-state}/state",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\nGITEA__database__DB_TYPE=postgres\nGITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nGITEA__database__NAME=${bound:postgres-database:as}\nGITEA__database__USER=${bound:postgres-database:as}\nGITEA__database__PASSWD=${secret:postgres-database}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1000:1000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "gitea",
|
||||||
|
"image": "gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef",
|
||||||
|
"env": {
|
||||||
|
"DB_TYPE": "postgres",
|
||||||
|
"USER_UID": "1000",
|
||||||
|
"USER_GID": "1000"
|
||||||
|
},
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"3000",
|
||||||
|
"222:22"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/data"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it",
|
||||||
|
"logging": "journald"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-bootstrap",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-gitea-admin",
|
||||||
|
"image": "gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef",
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"USER_UID": "1000",
|
||||||
|
"USER_GID": "1000",
|
||||||
|
"MESH_GITEA_ADMIN_USER": "mesh-admin"
|
||||||
|
},
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/data",
|
||||||
|
"${dir:state}/admin.secret:/run/secrets/admin:ro"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"/bin/sh",
|
||||||
|
"-c",
|
||||||
|
"su-exec git gitea admin user create --admin --username \"$MESH_GITEA_ADMIN_USER\" --email mesh-admin@localhost --password \"$(cat /run/secrets/admin)\" --must-change-password=false || true"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "npm-package-registry",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 40,
|
||||||
|
"in": "a Gitea user name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "git",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}",
|
||||||
|
"MESH_GITEA_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||||
|
"MESH_GITEA_ADMIN_USER": "mesh-admin",
|
||||||
|
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
|
||||||
|
"MESH_GITEA_STATE_DIR": "${dir:runtime-state}",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/npm.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "npm-registry",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/npm-registry",
|
||||||
|
"binary": "npm-registry",
|
||||||
|
"loads": [
|
||||||
|
"npm-registry"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}",
|
||||||
|
"MESH_GITEA_ADMIN_USER": "mesh-admin",
|
||||||
|
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
|
||||||
|
"MESH_NPM_OWNER": "novox"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jails": [
|
||||||
|
{
|
||||||
|
"name": "gitea",
|
||||||
|
"failregex": "^.*Failed authentication attempt for .* from <HOST>(?::\\d+)?\\s*$\n ^.*Invalid user .* from <HOST> port \\d+\\s*$\n ^.*User \\S+ from <HOST> not allowed because .*$",
|
||||||
|
"jail": "backend = systemd\njournalmatch = CONTAINER_NAME=gitea\nport = http,https,222\nmaxretry = 3\nfindtime = 1d\nbantime = 1d"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"module": "gitlab",
|
||||||
|
"version": "1",
|
||||||
|
"own-secrets": {
|
||||||
|
"token": "${dir:state}/token"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/config.json",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{}",
|
||||||
|
"mode": "0600"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_GITLAB_TOKEN_FILE": "${dir:state}/token",
|
||||||
|
"MESH_GITLAB_CONFIG_FILE": "${dir:state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"module": "gnome-keyring",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-secret-service",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"gnome_keyring_unlocked",
|
||||||
|
"gnome_keyring_lock",
|
||||||
|
"gnome_keyring_collections",
|
||||||
|
"gnome_keyring_ssh_keys"
|
||||||
|
],
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "xinitrc",
|
||||||
|
"slot": "first",
|
||||||
|
"code": "# The ssh agent (module gnome-keyring, novox/hq ADR 0208): gcr's, which the account's service manager\n# starts on first use from its socket. Named here, for the session and every terminal it starts, until\n# the account's environment can say a path under the runtime directory (see the module's README).\nSSH_AUTH_SOCK=\"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gcr/ssh\"\nexport SSH_AUTH_SOCK\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "gnome-keyring"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "library",
|
||||||
|
"type": "package",
|
||||||
|
"package": "libsecret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "manager",
|
||||||
|
"type": "package",
|
||||||
|
"package": "seahorse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pam-login",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/pam.d/login",
|
||||||
|
"mode": "0644",
|
||||||
|
"into": "block",
|
||||||
|
"at": "end",
|
||||||
|
"content": "# The login keyring (module gnome-keyring, novox/hq ADR 0208, ADR 0102): the password typed at the\n# login screen unlocks the keyring, and the login session starts the keyring daemon with it.\nauth optional pam_gnome_keyring.so\nsession optional pam_gnome_keyring.so auto_start\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pam-passwd",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/pam.d/passwd",
|
||||||
|
"mode": "0644",
|
||||||
|
"into": "block",
|
||||||
|
"at": "end",
|
||||||
|
"content": "# The login keyring (module gnome-keyring, novox/hq ADR 0208, ADR 0102): changing the account's\n# password changes the login keyring's with it, so the next login still unlocks it.\npassword optional pam_gnome_keyring.so\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/gnome-keyring-tools",
|
||||||
|
"binary": "gnome-keyring-tools",
|
||||||
|
"loads": [
|
||||||
|
"gnome-keyring-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"module": "grafana",
|
||||||
|
"version": "1",
|
||||||
|
"emits": [
|
||||||
|
"alert.firing"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": "${dir:mesh-state}/admin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 3000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the dashboards. Also 3000 inside, like the forge - which is the mesh's port assignment earning its keep"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "dashboards, users and alert rules"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "472:472"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-secret",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/admin.secret",
|
||||||
|
"mode": "0400",
|
||||||
|
"owner": "472:472",
|
||||||
|
"content": "${secret:admin}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-secret",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/oidc-client.secret",
|
||||||
|
"mode": "0400",
|
||||||
|
"owner": "472:472",
|
||||||
|
"content": "${secret:oidc-client}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/oidc.env",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "GF_SERVER_ROOT_URL=https://${bound:route:name}\nGF_AUTH_GENERIC_OAUTH_ENABLED=true\nGF_AUTH_GENERIC_OAUTH_NAME=Keycloak\nGF_AUTH_GENERIC_OAUTH_CLIENT_ID=${bound:oidc-client:as}\nGF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE=/run/secrets/oidc-client\nGF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile roles\nGF_AUTH_GENERIC_OAUTH_AUTH_URL=${bound:oidc-client:issuer}${bound:oidc-client:authorization-path}\nGF_AUTH_GENERIC_OAUTH_TOKEN_URL=${bound:oidc-client:issuer}${bound:oidc-client:token-path}\nGF_AUTH_GENERIC_OAUTH_API_URL=${bound:oidc-client:issuer}${bound:oidc-client:userinfo-path}\nGF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(roles[*], 'admin') && 'Admin' || contains(realm_access.roles[*], 'admin') && 'Admin' || 'Viewer'\nGF_AUTH_GENERIC_OAUTH_USE_PKCE=true\nGF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true\nGF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN=true\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "influxdb-secret",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/influxdb-api.secret",
|
||||||
|
"mode": "0400",
|
||||||
|
"owner": "472:472",
|
||||||
|
"content": "${secret:influxdb-api}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "influxdb-datasource",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/datasource-influxdb.yaml",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "apiVersion: 1\n# Written by the mesh from grafana's influxdb-api binding; grafana reads it at start. Its own name and\n# uid, so a data source somebody made in the UI is never overwritten, and read-only in the UI because\n# the mesh resets it. The password is read from the file the mesh delivers, never written here.\ndatasources:\n - name: InfluxDB (mesh)\n uid: mesh-influxdb-api\n type: influxdb\n access: proxy\n url: ${bound:influxdb-api:scheme}://${bound:influxdb-api:at}:${bound:influxdb-api:port}\n user: ${bound:influxdb-api:as}\n isDefault: false\n editable: false\n jsonData:\n dbName: ${bound:influxdb-api:bucket}\n httpMode: POST\n secureJsonData:\n password: $__file{/run/secrets/influxdb-api}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "grafana",
|
||||||
|
"image": "grafana/grafana@sha256:ac461fb352abc50da10a51c7d02462e9c05488f11f53f14b3ad79a8145f638a0",
|
||||||
|
"health": {
|
||||||
|
"kind": "http",
|
||||||
|
"endpoint": "web",
|
||||||
|
"path": "/"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"3000"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/var/lib/grafana",
|
||||||
|
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
||||||
|
"${dir:state}/oidc-client.secret:/run/secrets/oidc-client:ro",
|
||||||
|
"${dir:state}/influxdb-api.secret:/run/secrets/influxdb-api:ro",
|
||||||
|
"${dir:state}/datasource-influxdb.yaml:/etc/grafana/provisioning/datasources/mesh-influxdb.yaml:ro"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GF_SECURITY_ADMIN_PASSWORD__FILE": "/run/secrets/admin"
|
||||||
|
},
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/oidc.env"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"oidc-env",
|
||||||
|
"oidc-secret",
|
||||||
|
"influxdb-datasource",
|
||||||
|
"influxdb-secret"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{\n \"user\": \"admin\",\n \"password\": \"${secret:admin}\"\n}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route",
|
||||||
|
"oidc-client",
|
||||||
|
"influxdb-api"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "grafana",
|
||||||
|
"endpoint": "web"
|
||||||
|
},
|
||||||
|
"oidc-client": {
|
||||||
|
"label": "grafana",
|
||||||
|
"endpoint": "web",
|
||||||
|
"callback": "/login/generic_oauth"
|
||||||
|
},
|
||||||
|
"influxdb-api": {
|
||||||
|
"access": "read"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json",
|
||||||
|
"oidc-client": "${dir:state}/oidc.json",
|
||||||
|
"influxdb-api": "${dir:state}/influxdb.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"oidc-client": "${dir:mesh-state}/oidc-client",
|
||||||
|
"influxdb-api": "${dir:mesh-state}/influxdb-api"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_GRAFANA_URL": "http://127.0.0.1:${port:3000}",
|
||||||
|
"MESH_GRAFANA_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"module": "hello-web",
|
||||||
|
"slug": "hello",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "hello",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8080,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the demo web page; only the route-proxy reaches it, and the public name is a route grant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "page",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/index.html",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "hello from hello-web, routed by the mesh\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "hello-web"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "hello-web",
|
||||||
|
"network": "hello-web",
|
||||||
|
"ports": [
|
||||||
|
"8080"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:state}/index.html:/www/index.html:ro"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\n'; cat /www/index.html; } | nc -l -p 8080; done"
|
||||||
|
],
|
||||||
|
"artifact": "server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"kind": "upstream",
|
||||||
|
"from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"module": "home-assistant",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "hass",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"state.changed"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"token": "${dir:mesh-state}/token"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8123,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the dashboard, the API and the companion apps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sonos-events",
|
||||||
|
"port": 1400,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the Sonos integration's event callback: speakers push their state changes here"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webrtc",
|
||||||
|
"port": 18555,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the bundled go2rtc's WebRTC port, which camera streams to a browser use"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"path": "${dir:config}",
|
||||||
|
"class": "valuable",
|
||||||
|
"active": "1d",
|
||||||
|
"why": "the house's configuration, automations and history; written all the time"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "written",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "home-assistant",
|
||||||
|
"image": "ghcr.io/home-assistant/home-assistant@sha256:d8922685169707fd91e8b9729902d975f06157d005e422874d201e0261dda196",
|
||||||
|
"network": "host",
|
||||||
|
"env": {
|
||||||
|
"TZ": "Etc/UTC"
|
||||||
|
},
|
||||||
|
"volumes": [
|
||||||
|
"${dir:config}:/config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "provisions-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/provisions.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MESH_HOMEASSISTANT_URL=http://127.0.0.1:${port:8123}\nMESH_HOMEASSISTANT_TOKEN_FILE=${dir:mesh-state}/token\nMESH_PROVISIONS_DIR=${dir:state}\nMESH_WRITTEN_DIR=${dir:written}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "provisions",
|
||||||
|
"type": "process",
|
||||||
|
"name": "home-assistant-provisions",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"node",
|
||||||
|
"provisions/index.js"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/provisions.env"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"provisions-env",
|
||||||
|
"bound-mqtt-topic",
|
||||||
|
"secret-mqtt-topic",
|
||||||
|
"bound-sonarr-api",
|
||||||
|
"secret-sonarr-api",
|
||||||
|
"bound-radarr-api",
|
||||||
|
"secret-radarr-api",
|
||||||
|
"bound-lidarr-api",
|
||||||
|
"secret-lidarr-api"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"lidarr-api",
|
||||||
|
"mqtt-topic",
|
||||||
|
"radarr-api",
|
||||||
|
"route",
|
||||||
|
"sonarr-api"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"mqtt-topic": {
|
||||||
|
"topics": [
|
||||||
|
"#"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "home-assistant",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json",
|
||||||
|
"mqtt-topic": "${dir:state}/mqtt-topic.json",
|
||||||
|
"sonarr-api": "${dir:state}/sonarr-api.json",
|
||||||
|
"radarr-api": "${dir:state}/radarr-api.json",
|
||||||
|
"lidarr-api": "${dir:state}/lidarr-api.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"mqtt-topic": "${dir:state}/mqtt-topic.secret",
|
||||||
|
"sonarr-api": "${dir:state}/sonarr-api.secret",
|
||||||
|
"radarr-api": "${dir:state}/radarr-api.secret",
|
||||||
|
"lidarr-api": "${dir:state}/lidarr-api.secret"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisions/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_HOMEASSISTANT_URL": "http://127.0.0.1:${port:8123}",
|
||||||
|
"MESH_HOMEASSISTANT_TOKEN_FILE": "${dir:mesh-state}/token",
|
||||||
|
"MESH_HOMEASSISTANT_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"module": "hostname",
|
||||||
|
"version": "1",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-hostname",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"entries",
|
||||||
|
"add",
|
||||||
|
"remove"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "own",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/hosts",
|
||||||
|
"mode": "0644",
|
||||||
|
"into": "block",
|
||||||
|
"at": "start",
|
||||||
|
"content": "# The machine's own names (module hostname, novox/hq ADR 0199, ADR 0223). Every line outside this\n# block is the operator's: kept across every push, changed through the node-hostname verbs add and\n# remove, and given back when this module goes. The mesh's names are not here: the mesh's resolver\n# answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\t${machine:name}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "name",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/hostname",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "${setting:hostname}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/hostname-tools",
|
||||||
|
"binary": "hostname-tools",
|
||||||
|
"loads": [
|
||||||
|
"hostname-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"module": "icecast",
|
||||||
|
"version": "1",
|
||||||
|
"requires": [
|
||||||
|
"route",
|
||||||
|
"secret"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "icecast",
|
||||||
|
"endpoint": "stream"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"secret": {
|
||||||
|
"source": "${dir:state}/source.secret",
|
||||||
|
"admin": "${dir:state}/admin.secret",
|
||||||
|
"relay": "${dir:state}/relay.secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"stream.started",
|
||||||
|
"stream.stopped"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "stream",
|
||||||
|
"port": 8000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "streams in from sources (HTTP PUT) and out to listeners, plus the status and admin pages; a public name is its route"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "logs",
|
||||||
|
"path": "${dir:logs}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "the streaming server's logs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "logs",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "100:101"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/icecast.xml",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "<icecast>\n <!-- Written by the mesh (modules/icecast). Passwords arrive as secrets rendered into this file,\n never as environment: the image's entrypoint seds ICECAST_* variables into the file only\n when they are set, and none are. -->\n <location>Earth</location>\n <admin>icemaster@localhost</admin>\n <limits>\n <clients>100</clients>\n <sources>2</sources>\n <queue-size>524288</queue-size>\n <client-timeout>30</client-timeout>\n <header-timeout>15</header-timeout>\n <source-timeout>10</source-timeout>\n <burst-on-connect>1</burst-on-connect>\n <burst-size>65535</burst-size>\n </limits>\n <authentication>\n <source-password>${secret:source}</source-password>\n <relay-password>${secret:relay}</relay-password>\n <admin-user>admin</admin-user>\n <admin-password>${secret:admin}</admin-password>\n </authentication>\n <!-- The name icecast writes into playlists (.m3u/.xspf: http://<hostname>:<port>/<mount>) and\n would announce to YP (none configured). A machine's own name belongs to its assignment, and\n an assignment merges only into JSON; this XML cannot take it, so the neutral default stays. -->\n <hostname>localhost</hostname>\n <listen-socket>\n <port>8000</port>\n </listen-socket>\n <http-headers>\n <header name=\"Access-Control-Allow-Origin\" value=\"*\" />\n </http-headers>\n <fileserve>1</fileserve>\n <paths>\n <basedir>/usr/share/icecast</basedir>\n <logdir>/var/log/icecast</logdir>\n <webroot>/usr/share/icecast/web</webroot>\n <adminroot>/usr/share/icecast/admin</adminroot>\n <alias source=\"/\" destination=\"/status.xsl\"/>\n </paths>\n <logging>\n <accesslog>access.log</accesslog>\n <errorlog>error.log</errorlog>\n <loglevel>3</loglevel>\n <logsize>10000</logsize>\n </logging>\n <security>\n <chroot>0</chroot>\n <!-- Starts as root, reads this 0600 root-owned file, then drops to the image's icecast user\n (uid 100, group icecast 101) before serving. -->\n <changeowner>\n <user>icecast</user>\n <group>icecast</group>\n </changeowner>\n </security>\n</icecast>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "icecast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "icecast",
|
||||||
|
"image": "infiniteproject/icecast@sha256:cd506cf3dfe31ce05fd37d7e672dbd1213e7255cc93d28ecf5a3b547af4e162c",
|
||||||
|
"network": "icecast",
|
||||||
|
"ports": [
|
||||||
|
"8000"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:state}/icecast.xml:/etc/icecast.xml:ro",
|
||||||
|
"${dir:logs}:/var/log/icecast"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"server-conf"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_ICECAST_URL": "http://127.0.0.1:${port:8000}",
|
||||||
|
"MESH_ICECAST_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"module": "influxdb",
|
||||||
|
"version": "1",
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "influxdb-api",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"in": "an InfluxDB v1 authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": "${dir:state}/admin.secret",
|
||||||
|
"admin-token": "${dir:state}/admin-token.secret"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"port": 8086,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "queries, writes and the web UI, over http; consumers granted influxdb-api sign in with the mesh's credential, and a name is a route grant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"influxdb-api": {
|
||||||
|
"scheme": "http",
|
||||||
|
"port": 8086,
|
||||||
|
"org": "mesh",
|
||||||
|
"bucket": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"influxdb-api": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"influxdb-api": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every consumer's time series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"path": "${dir:config}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the server's own configuration and its operator token, made at its first start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"influxdb-api": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "a consumer's measurements are the only copy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1000:1000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1000:1000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "DOCKER_INFLUXDB_INIT_MODE=setup\nDOCKER_INFLUXDB_INIT_USERNAME=admin\nDOCKER_INFLUXDB_INIT_PASSWORD_FILE=/run/secrets/admin\nDOCKER_INFLUXDB_INIT_ADMIN_TOKEN_FILE=/run/secrets/admin-token\nDOCKER_INFLUXDB_INIT_ORG=mesh\nDOCKER_INFLUXDB_INIT_BUCKET=default\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "influxdb",
|
||||||
|
"image": "influxdb@sha256:f75e48af0598e8aec7986e991a848d19a119101a7d563a2e5db1dfaac9c45daa",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"8086"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/var/lib/influxdb2",
|
||||||
|
"${dir:config}:/etc/influxdb2",
|
||||||
|
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
||||||
|
"${dir:state}/admin-token.secret:/run/secrets/admin-token:ro"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "influxdb",
|
||||||
|
"endpoint": "api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_INFLUXDB_URL": "http://127.0.0.1:${port:8086}",
|
||||||
|
"MESH_INFLUXDB_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||||
|
"MESH_INFLUXDB_TOKEN_FILE": "${dir:state}/admin-token.secret",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"module": "invoicing",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "invoice",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"mongodb-database",
|
||||||
|
"s3-bucket",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"mongodb-database": {
|
||||||
|
"name": "invoicing"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"site": {
|
||||||
|
"label": "invoicing",
|
||||||
|
"endpoint": "web"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"label": "invoicing-api",
|
||||||
|
"endpoint": "api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"mongodb-database": "${dir:state}/database.json",
|
||||||
|
"s3-bucket": "${dir:state}/store.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"mongodb-database": "${dir:state}/database.secret",
|
||||||
|
"s3-bucket": "${dir:state}/store.secret"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the invoicing web frontend; a public name is a route grant later"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"port": 9000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the invoicing REST API the frontend and integrations call"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/api.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "invoicing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app",
|
||||||
|
"type": "container",
|
||||||
|
"name": "invoicing-app",
|
||||||
|
"image": "registry-api.novox.be/novox/invoicing-app@sha256:1e6ed40822f07169b24867cbfe8fc1ab3ef6a15ad642f3b3a2882a8e15c3dbec",
|
||||||
|
"network": "invoicing",
|
||||||
|
"env": {
|
||||||
|
"UID": "2201",
|
||||||
|
"GID": "2201"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"80"
|
||||||
|
],
|
||||||
|
"names-on-purpose": {
|
||||||
|
"registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api",
|
||||||
|
"type": "container",
|
||||||
|
"name": "invoicing-api",
|
||||||
|
"image": "registry-api.novox.be/novox/invoicing-api@sha256:efa6fba1fa9ba78849e94e958d33793a76654df0468e3012da9c02c54c265354",
|
||||||
|
"network": "invoicing",
|
||||||
|
"env": {
|
||||||
|
"UID": "2201",
|
||||||
|
"GID": "2201"
|
||||||
|
},
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/api.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"9000"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "the application's own code reads MONGO_URL and MINIO_SECRET from the environment (invoicing-app server/src/config.js); converting is that repository's change",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"module": "jira",
|
||||||
|
"version": "1",
|
||||||
|
"own-secrets": {
|
||||||
|
"token": "${dir:state}/token"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/config.json",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{}",
|
||||||
|
"mode": "0600"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_JIRA_TOKEN_FILE": "${dir:state}/token",
|
||||||
|
"MESH_JIRA_CONFIG_FILE": "${dir:state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"module": "keycloak",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "every person's sign-in to every site goes through it, and its new version migrates its database on start: a person takes each build, after a backup (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "oidc-client",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 255,
|
||||||
|
"in": "a Keycloak client id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "keycloak"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "keycloak",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"user.created",
|
||||||
|
"user.deleted",
|
||||||
|
"password.reset",
|
||||||
|
"client.created",
|
||||||
|
"client.retired",
|
||||||
|
"group.created",
|
||||||
|
"role.created",
|
||||||
|
"admin.repaired",
|
||||||
|
"admin.unrepaired"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8080,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "anything the mesh runs that authenticates a person"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"oidc-client": {
|
||||||
|
"authorization-path": "/protocol/openid-connect/auth",
|
||||||
|
"token-path": "/protocol/openid-connect/token",
|
||||||
|
"userinfo-path": "/protocol/openid-connect/userinfo",
|
||||||
|
"issuer": "${setting:issuer}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"oidc-client": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"oidc-client": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": "${dir:state}/admin.secret"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"consumers": {
|
||||||
|
"oidc-client": {
|
||||||
|
"class": "rebuildable",
|
||||||
|
"in": "postgres-database",
|
||||||
|
"why": "a consumer's client is made again from its declaration, with a new secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/admin.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "KEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=${secret:admin}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/database.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\nKC_DB_USERNAME=${bound:postgres-database:as}\nKC_DB_PASSWORD=${secret:postgres-database}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "keycloak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hostname",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/hostname.env",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "KC_HOSTNAME=https://${bound:route:name}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "keycloak",
|
||||||
|
"image": "quay.io/keycloak/keycloak@sha256:ecd43971114b0c764f8a3288dddab73f98cb473daccc4feaffe4dc14adeaf866",
|
||||||
|
"network": "keycloak",
|
||||||
|
"args": [
|
||||||
|
"start-dev"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"KC_DB": "postgres",
|
||||||
|
"KC_HTTP_ENABLED": "true",
|
||||||
|
"KC_HEALTH_ENABLED": "true",
|
||||||
|
"KC_PROXY_HEADERS": "xforwarded"
|
||||||
|
},
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/admin.env",
|
||||||
|
"${dir:state}/database.env",
|
||||||
|
"${dir:state}/hostname.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"8080"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "KC_DB_PASSWORD is convertible through a generated keycloak.conf (db-password=); KEYCLOAK_ADMIN_PASSWORD is env-only before Keycloak 26; not yet converted",
|
||||||
|
"restart-on": [
|
||||||
|
"hostname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/keycloak-provider",
|
||||||
|
"binary": "keycloak-provider",
|
||||||
|
"loads": [
|
||||||
|
"keycloak-provider"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_KEYCLOAK_URL": "http://127.0.0.1:${port:8080}",
|
||||||
|
"MESH_KEYCLOAK_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||||
|
"MESH_KEYCLOAK_PASSWORD_FILE": "${dir:state}/admin.secret",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
||||||
|
"MESH_KEYCLOAK_CONTAINER": "keycloak"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"module": "lab",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime",
|
||||||
|
"virtualisation"
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "work",
|
||||||
|
"path": "${dir:work}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "the lab's runs, each thrown away"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "work",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh-lab-runs",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/lab.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MESH_LAB_FORGE=${setting:forge}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "git",
|
||||||
|
"type": "package",
|
||||||
|
"package": "git"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "make",
|
||||||
|
"type": "package",
|
||||||
|
"package": "make"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "python",
|
||||||
|
"type": "package",
|
||||||
|
"package": "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file",
|
||||||
|
"type": "package",
|
||||||
|
"package": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iproute2",
|
||||||
|
"type": "package",
|
||||||
|
"package": "iproute2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "npm",
|
||||||
|
"type": "package",
|
||||||
|
"package": "npm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "go",
|
||||||
|
"type": "package",
|
||||||
|
"package": "go"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "incus",
|
||||||
|
"type": "package",
|
||||||
|
"package": "incus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_LAB_WORK": "${dir:work}",
|
||||||
|
"MESH_LAB_ENV_FILE": "${dir:state}/lab.env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"module": "letta",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "letta",
|
||||||
|
"extensions": [
|
||||||
|
"vector"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "letta",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"server-password": {
|
||||||
|
"path": "${dir:state}/server-password.secret",
|
||||||
|
"taken": "at-start"
|
||||||
|
},
|
||||||
|
"openai-api-key": {
|
||||||
|
"path": "${dir:state}/openai-api-key.secret",
|
||||||
|
"taken": "at-start",
|
||||||
|
"issued-by": "outside"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8283,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the Letta agent server REST API and web UI, password-protected (--secure); a public name is the route's"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "LETTA_PG_URI=postgresql://${bound:postgres-database:as}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\nPGPASSFILE=/run/secrets/pgpass\nLETTA_SERVER_PASSWORD=${secret:server-password}\nOPENAI_API_KEY=${secret:openai-api-key}\nSECURE=true\nTZ=Europe/Brussels\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pgpass",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/pgpass",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "*:*:*:${bound:postgres-database:as}:${secret:postgres-database}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "start",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/start.sh",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "#!/bin/sh\n# Generated by the mesh. Do not edit: module letta writes this file and replaces it at every push.\n#\n# letta 0.6.8 prints secrets it is given to its log (novox/hq issue 268):\n# letta/server/rest_api/app.py prints its server password when it starts in secure mode;\n# startup.sh, alembic/env.py and letta/server/server.py print LETTA_PG_URI whole.\n# The URI carries no password (libpq reads it from PGPASSFILE), and the one print of the server\n# password is rewritten before the server starts. Either one failing refuses the start: a letta\n# that does not start says why here, and one that leaks says nothing.\nset -e\napp=/app/letta/server/rest_api/app.py\nsed -i 's/Using secure mode with password: {random_password}/Using secure mode (the password is not printed)/' \"$app\"\nif grep -q 'print(.*random_password' \"$app\"; then\n echo \"letta: $app still prints the server password; not starting (novox/hq issue 268)\" >&2\n exit 1\nfi\ncase \"$LETTA_PG_URI\" in\n *://*:*@*)\n echo \"letta: LETTA_PG_URI carries a password, and letta prints that URI; not starting (novox/hq issue 268)\" >&2\n exit 1\n ;;\nesac\nexec ./letta/server/startup.sh\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "letta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "letta",
|
||||||
|
"image": "letta/letta@sha256:bfd1e49ce45b9a208c941e832c1d1d194017ff210a3784b0ca6c323aed767a29",
|
||||||
|
"network": "letta",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"8283"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:state}/pgpass:/run/secrets/pgpass:ro",
|
||||||
|
"${dir:state}/start.sh:/run/letta/start.sh:ro"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"sh",
|
||||||
|
"/run/letta/start.sh"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "letta 0.6.x reads its settings from the environment only (pydantic settings, no secrets_dir or _FILE twin): LETTA_SERVER_PASSWORD and OPENAI_API_KEY have no file source. The database password is not here: LETTA_PG_URI, which letta prints at start, names no password, and libpq reads it from the mounted pgpass file (PGPASSFILE)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{\n \"password\": \"${secret:server-password}\"\n}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_LETTA_URL": "http://127.0.0.1:${port:8283}",
|
||||||
|
"MESH_LETTA_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"module": "local-model-consumer",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "local",
|
||||||
|
"requires": [
|
||||||
|
"model-access"
|
||||||
|
],
|
||||||
|
"binds": {
|
||||||
|
"model-access": "${dir:state}/model.json"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "openai-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:config}/openai.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "OPENAI_BASE_URL=http://${bound:model-access:at}:${bound:model-access:port}/v1\nOPENAI_MODEL=${bound:model-access:model}\nOPENAI_API_KEY=local\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"module": "localization",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"localization_get",
|
||||||
|
"localization_time_zone",
|
||||||
|
"localization_locales",
|
||||||
|
"localization_keymaps"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "locale",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/locale.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# The mesh's (module localization, novox/hq to-be 42): the system locale. Written whole at every\n# push; an edit here is overwritten. Read at the next login.\nLANG=en_US.UTF-8\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "keymap",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/vconsole.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# The mesh's (module localization, novox/hq to-be 42): the console keymap. Written whole at every\n# push; an edit here is overwritten. Read at the next boot.\nKEYMAP=us\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time-zone",
|
||||||
|
"type": "process",
|
||||||
|
"name": "localization-time-zone",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./localization-tools",
|
||||||
|
"set-time-zone",
|
||||||
|
"Europe/Brussels"
|
||||||
|
],
|
||||||
|
"run-once": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/localization-tools",
|
||||||
|
"binary": "localization-tools",
|
||||||
|
"loads": [
|
||||||
|
"localization-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"module": "logrotate",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"logrotate_status",
|
||||||
|
"logrotate_configs",
|
||||||
|
"logrotate_check",
|
||||||
|
"logrotate_big_logs",
|
||||||
|
"logrotate_force",
|
||||||
|
"logrotate_journal_usage",
|
||||||
|
"logrotate_journal_vacuum"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "logrotate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/logrotate.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# The mesh's (module logrotate, novox/hq to-be 42): the base configuration every rotation inherits.\n# Written whole at every push; an edit here is overwritten. Each package's own rules are in\n# /etc/logrotate.d and stay the packages'.\n\n# Weekly, four weeks kept, a new empty log created after each rotation.\nweekly\nrotate 4\ncreate\n\n# Rotated logs are compressed, one rotation late, so a program still writing to the file it had open\n# loses nothing to the compression.\ncompress\ndelaycompress\n\n# A package's replaced configuration is never read as a rule.\ntabooext + .pacorig .pacnew .pacsave\n\ninclude /etc/logrotate.d\n\n/var/log/wtmp {\n monthly\n create 0664 root utmp\n minsize 1M\n rotate 1\n}\n\n/var/log/btmp {\n missingok\n monthly\n create 0600 root utmp\n rotate 1\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "timer",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "logrotate.timer",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/logrotate-tools",
|
||||||
|
"binary": "logrotate-tools",
|
||||||
|
"loads": [
|
||||||
|
"logrotate-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,680 @@
|
|||||||
|
{
|
||||||
|
"module": "mailu",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "people's mail: any change to how its containers are declared recreates them together in one send, and the mail is down for everyone until they are up again — a person chooses the moment (hq ADR 0242, issue 295)"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route",
|
||||||
|
"secret"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "mailu"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"web": {
|
||||||
|
"label": "mail",
|
||||||
|
"endpoint": "web-tls",
|
||||||
|
"scheme": "https",
|
||||||
|
"insecure": true
|
||||||
|
},
|
||||||
|
"acme": {
|
||||||
|
"label": "mail",
|
||||||
|
"path": "/.well-known/acme-challenge",
|
||||||
|
"endpoint": "web",
|
||||||
|
"priority": 100
|
||||||
|
},
|
||||||
|
"autoconfig": {
|
||||||
|
"label": "autoconfig",
|
||||||
|
"endpoint": "autoconfig"
|
||||||
|
},
|
||||||
|
"autodiscover": {
|
||||||
|
"label": "autodiscover",
|
||||||
|
"endpoint": "autoconfig"
|
||||||
|
},
|
||||||
|
"automx": {
|
||||||
|
"label": "automx",
|
||||||
|
"endpoint": "autoconfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret",
|
||||||
|
"secret": {
|
||||||
|
"secret-key": "${dir:state}/secret-key.secret",
|
||||||
|
"admin": "${dir:state}/admin.secret",
|
||||||
|
"api-token": "${dir:state}/api-token.secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emits": [
|
||||||
|
"user.created",
|
||||||
|
"user.deleted",
|
||||||
|
"alias.created",
|
||||||
|
"alias.deleted"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "smtp",
|
||||||
|
"port": 25,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "mail from other mail servers",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pop3",
|
||||||
|
"port": 110,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "POP3, kept at parity with the predecessor; pruning legacy protocols is its own deliberate change",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "imap",
|
||||||
|
"port": 143,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "IMAP with STARTTLS, kept at parity",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "smtps",
|
||||||
|
"port": 465,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "submission over TLS",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "submission",
|
||||||
|
"port": 587,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "submission; also what the smtp provision serves consumers",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "imaps",
|
||||||
|
"port": 993,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "IMAP over TLS",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pop3s",
|
||||||
|
"port": 995,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "anywhere",
|
||||||
|
"why": "POP3 over TLS, kept at parity",
|
||||||
|
"fixed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 7080,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the web front over http; only the ACME HTTP-01 passthrough is routed here — everything else 301s to https and would loop a proxy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web-tls",
|
||||||
|
"port": 7443,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the web front over its own TLS (admin, webmail, API); its public name is a route grant reaching it here"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autoconfig",
|
||||||
|
"port": 4243,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "automx: mail client autoconfiguration; the autoconfig, autodiscover and automx names are route grants reaching it here"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin-api",
|
||||||
|
"port": 8080,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "machine",
|
||||||
|
"why": "the admin API, which this module's own code reaches on loopback from the node's runtime now that it runs outside the mailu network"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "mail",
|
||||||
|
"path": "${dir:data-mail}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every mailbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dkim",
|
||||||
|
"path": "${dir:data-dkim}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the domain's signing keys; a new one means a new DNS record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data-data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the server's own data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dav",
|
||||||
|
"path": "${dir:data-dav}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "calendars and contacts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "webmail",
|
||||||
|
"path": "${dir:data-webmail}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the webmail's own data: its users' settings and address books"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "queue",
|
||||||
|
"path": "${dir:data-mailqueue}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "mail accepted and not yet delivered, kept nowhere else"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "filter",
|
||||||
|
"path": "${dir:data-filter}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "the spam filter's learned statistics; it learns again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "certs",
|
||||||
|
"path": "${dir:data-certs}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "certificates, issued again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "automx",
|
||||||
|
"path": "${dir:data-automx}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "client autoconfiguration, written again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fetchmail",
|
||||||
|
"path": "${dir:data-fetchmail}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "which remote messages were fetched; lost, some are fetched twice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clamav",
|
||||||
|
"path": "${dir:data-clamav}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "virus signatures, downloaded again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "redis",
|
||||||
|
"path": "${dir:data-redis}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "the filter's working set"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"smtp": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "mail",
|
||||||
|
"why": "a consumer's mailbox holds the only copy of its mail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-automx",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/mailu.env",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "ADMIN_ADDRESS=mailu-admin\nANTISPAM_ADDRESS=mailu-antispam\nANTIVIRUS_ADDRESS=mailu-antivirus\nIMAP_ADDRESS=mailu-imap\nSMTP_ADDRESS=mailu-smtp\nFRONT_ADDRESS=mailu-front\nWEBMAIL_ADDRESS=mailu-webmail\nWEBDAV_ADDRESS=mailu-webdav\nREDIS_ADDRESS=mailu-redis\nPORTS=25,80,443,465,993,995,4190,110,143,587\nDOMAIN=${setting:domain}\nHOSTNAMES=${bound:route:name-web}\nPOSTMASTER=admin\nSITENAME=${setting:sitename}\nWEBSITE=${setting:website}\nTLS_FLAVOR=letsencrypt\nSUBNET=192.168.203.0/24\nCOMPOSE_PROJECT_NAME=mailu\nANTIVIRUS=clamav\nWEBMAIL=roundcube\nWEBDAV=radicale\nFETCHMAIL_ENABLED=True\nFETCHMAIL_DELAY=600\nADMIN=true\nWEB_ADMIN=/admin\nWEB_WEBMAIL=/webmail\nWEBROOT_REDIRECT=/webmail\nAPI=true\nWEB_API=/api\nAUTH_RATELIMIT_IP=6000/hour\nAUTH_RATELIMIT_USER=1000/day\nCREDENTIAL_ROUNDS=12\nPASSWORD_SCHEME=PBKDF2\nDISABLE_STATISTICS=True\nMESSAGE_SIZE_LIMIT=50000000\nMESSAGE_RATELIMIT=200/day\nRECIPIENT_DELIMITER=+\nPOSTFIX_MYNETWORKS=127.0.0.0/8 [::1]/128\nRELAYNETS=\nRELAYHOST=\nREJECT_UNLISTED_RECIPIENT=\nDB_FLAVOR=postgresql\nINITIAL_ADMIN_ACCOUNT=admin\nINITIAL_ADMIN_DOMAIN=${setting:domain}\nINITIAL_ADMIN_MODE=ifmissing\nSMTP_PORT=25\nSMTPS_PORT=465\nSUBMISSION_PORT=587\nPOP3_PORT=110\nPOP3S_PORT=995\nIMAP_PORT=143\nIMAPS_PORT=993\nHTTP_PORT=7080\nHTTPS_PORT=7443\nAUTOMX_PORT=4243\nAMX_SMTP_ADDRESS=${bound:route:name-web}\nAMX_SMTP_PORT=587\nAMX_IMAP_ADDRESS=${bound:route:name-web}\nAMX_IMAP_PORT=143\nAMX_MAIL_DOMAINS=${setting:domain}\nDMARC_RUA=admin\nDMARC_RUF=admin\nLETSENCRYPT_SHORTCHAIN=True\nTZ=Etc/UTC\nLOG_LEVEL=INFO\nWELCOME=false\nREAL_IP_HEADER=X-Real-IP\nREAL_IP_FROM=${setting:proxy-address}\nCOMPRESSION=\nCOMPRESS_LEVEL=\nCOMPRESSION_LEVEL=\nBIND_ADDRESS4=127.0.0.1\nBIND_ADDRESS6=::1\nMAILU_VERSION=1.9\nDOCKER_ORG=mailu\nDOCKER_PREFIX=\nWELCOME_SUBJECT=Welcome to your new email account\nWELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "secret-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/secret.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "SECRET_KEY=${secret:secret-key}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/database.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "DB_FLAVOR=postgresql\nDB_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nDB_USER=${bound:postgres-database:as}\nDB_NAME=${bound:postgres-database:as}\nDB_PW=${secret:postgres-database}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/admin.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "INITIAL_ADMIN_PW=${secret:admin}\nAPI_TOKEN=${secret:api-token}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-certs",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-dkim",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-mail",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-mailqueue",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-filter",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-clamav",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-redis",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-webmail",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-dav",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-fetchmail",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-overrides-nginx",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-overrides-dovecot",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-overrides-postfix",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-overrides-rspamd",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-overrides-roundcube",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "mailu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "resolver",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-resolver",
|
||||||
|
"image": "ghcr.io/mailu/unbound@sha256:3a0fdfb364a63f4f9259526e013c1ef40f5f14de3621ce1560804b3a5909584a",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env",
|
||||||
|
"${dir:state}/secret.env"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||||
|
"ip": "192.168.203.254"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "redis",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-redis",
|
||||||
|
"image": "redis@sha256:4bed291aa5efb9f0d77b76ff7d4ab71eee410962965d052552db1fb80576431d",
|
||||||
|
"network": "mailu",
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-redis}:/data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-admin",
|
||||||
|
"image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"ports": [
|
||||||
|
"8080"
|
||||||
|
],
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env",
|
||||||
|
"${dir:state}/secret.env",
|
||||||
|
"${dir:state}/database.env",
|
||||||
|
"${dir:state}/admin.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-data}:/data",
|
||||||
|
"${dir:data-dkim}:/dkim"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "imap",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-imap",
|
||||||
|
"image": "ghcr.io/mailu/dovecot@sha256:7f0ed5db996fbdc00adc5c5e38a08492e04f7eb4a9fbd66a03aa9a28ddf23993",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-mail}:/mail",
|
||||||
|
"${dir:data-overrides-dovecot}:/overrides:ro"
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "smtp",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-smtp",
|
||||||
|
"image": "ghcr.io/mailu/postfix@sha256:e2e49f39e53b80eac9e7a2f18d9df11edeb4914fd62dbba89b3155e8e034f62e",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-mailqueue}:/queue",
|
||||||
|
"${dir:data-overrides-postfix}:/overrides:ro"
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "antispam",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-antispam",
|
||||||
|
"image": "ghcr.io/mailu/rspamd@sha256:ff3666d8a61f17d309c5c6f6bcf4d40470b82299ca706ac650301175bb1a079d",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-filter}:/var/lib/rspamd",
|
||||||
|
"${dir:data-overrides-rspamd}:/etc/rspamd/override.d:ro"
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "antivirus",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-antivirus",
|
||||||
|
"image": "clamav/clamav-debian@sha256:b12ef8fefddbba7d88de59bea8a32622f365339154adf02d38fd089112e6745a",
|
||||||
|
"network": "mailu",
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-clamav}:/var/lib/clamav"
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "webmail",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-webmail",
|
||||||
|
"image": "ghcr.io/mailu/webmail@sha256:bdbee44cdb05a4658f0e3b62cc448de55ca8f8aea172279fda594826144c04f6",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env",
|
||||||
|
"${dir:state}/secret.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-webmail}:/data",
|
||||||
|
"${dir:data-overrides-roundcube}:/overrides:ro"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "webdav",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-webdav",
|
||||||
|
"image": "ghcr.io/mailu/radicale@sha256:690ed6edf189dfef100a5a8b37c195ebf5d9241ac5f23f2f44b8b7b75726e3de",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env",
|
||||||
|
"${dir:state}/secret.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-dav}:/data"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fetchmail",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-fetchmail",
|
||||||
|
"image": "ghcr.io/mailu/fetchmail@sha256:f881c8412d3bbe73d638469b48321558d6403a9d45bfa043c1e52c752103d42d",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env",
|
||||||
|
"${dir:state}/secret.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-fetchmail}:/data"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "front",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-front",
|
||||||
|
"image": "ghcr.io/mailu/nginx@sha256:36f98897cd1bc9d27628bbb4e04bdf60147af2ec7507d6da77f002c4f256896d",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"25",
|
||||||
|
"110",
|
||||||
|
"143",
|
||||||
|
"465",
|
||||||
|
"587",
|
||||||
|
"993",
|
||||||
|
"995",
|
||||||
|
"7080:80",
|
||||||
|
"7443:443"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-certs}:/certs",
|
||||||
|
"${dir:data-overrides-nginx}:/overrides:ro"
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"192.168.203.254"
|
||||||
|
],
|
||||||
|
"logging": "journald"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "automx",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mailu-automx",
|
||||||
|
"artifact": "automx",
|
||||||
|
"network": "mailu",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mailu.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"4243"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data-automx}:/data"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "PYTHON_BASE",
|
||||||
|
"image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_MAILU_URL": "http://127.0.0.1:${port:8080}/api/v1",
|
||||||
|
"MESH_MAILU_API_KEY_FILE": "${dir:state}/api-token.secret",
|
||||||
|
"MESH_MAILU_IMAP_CONTAINER": "mailu-imap",
|
||||||
|
"MESH_MAILU_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "automx",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "automx/Dockerfile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "smtp",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 64,
|
||||||
|
"in": "a mailbox's local part"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"smtp": {
|
||||||
|
"port": 587,
|
||||||
|
"domain": "${setting:domain}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"smtp": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"smtp": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"jails": [
|
||||||
|
{
|
||||||
|
"name": "mailu-front",
|
||||||
|
"failregex": "^.*(?:imap|pop3|submission|managesieve)-login: .*\\(auth failed, \\d+ attempts(?: in \\d+ secs)?\\):.*rip=<HOST>(?:,|$)",
|
||||||
|
"jail": "backend = systemd\njournalmatch = CONTAINER_NAME=mailu-front\nport = smtp,submission,submissions,imap,imaps,pop3,pop3s\nmaxretry = 3\nfindtime = 1d\nbantime = 1d"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"module": "marrytts",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"port": 59125,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the MaryTTS text-to-speech HTTP API and web interface"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "marrytts",
|
||||||
|
"image": "synesthesiam/marytts@sha256:45970ecb3e21a2981c66c60563a70cf00be8e95c02565e7d74b3a73dcec7db2c",
|
||||||
|
"env": {
|
||||||
|
"TZ": "Europe/Brussels"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"59125"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"module": "matrix",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"port": 6167,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "Conduit's client-server and federation APIs over plain HTTP. Both arrive through the route on 443: Conduit answers /.well-known/matrix/server with <its name>:443, so other homeservers federate through the proxy and nothing needs the traditional 8448"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "Element Web, the static browser client, served by the image's nginx; reached through its route"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"homeserver": {
|
||||||
|
"label": "matrix",
|
||||||
|
"endpoint": "client"
|
||||||
|
},
|
||||||
|
"element": {
|
||||||
|
"label": "element",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "db",
|
||||||
|
"path": "${dir:db}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every room, message and account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "db",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "conduit-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/conduit.toml",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (modules/matrix). Conduit reads this file (CONDUIT_CONFIG); nothing comes\n# from the environment. server_name is the homeserver's permanent identity: every user id, room id\n# and signature in the database carries it, so it is the name this module is served under\n# (${bound:route:name-homeserver}) and never changes once a database exists.\n[global]\nserver_name = \"${bound:route:name-homeserver}\"\ndatabase_backend = \"rocksdb\"\ndatabase_path = \"/var/lib/matrix-conduit/\"\naddress = \"0.0.0.0\"\nport = 6167\nmax_request_size = 20000000\nallow_registration = false\nallow_federation = true\nallow_check_for_updates = true\ntrusted_servers = [\"matrix.org\"]\n",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"matrix.org": "the federation's public key server, trusted by default; the world's, not this mesh's"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "element-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/element.json",
|
||||||
|
"mode": "0644",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"default_server_name\": \"${bound:route:name-homeserver}\",\n \"default_server_config\": {\n \"m.homeserver\": {\n \"base_url\": \"https://${bound:route:name-homeserver}\"\n },\n \"m.identity_server\": {\n \"base_url\": \"https://vector.im\"\n }\n },\n \"brand\": \"Element\",\n \"integrations_ui_url\": \"https://scalar.vector.im/\",\n \"integrations_rest_url\": \"https://scalar.vector.im/api\",\n \"integrations_widgets_urls\": [\n \"https://scalar.vector.im/_matrix/integrations/v1\",\n \"https://scalar.vector.im/api\",\n \"https://scalar-staging.vector.im/_matrix/integrations/v1\",\n \"https://scalar-staging.vector.im/api\",\n \"https://scalar-staging.riot.im/scalar/api\"\n ],\n \"bug_report_endpoint_url\": \"https://element.io/bugreports/submit\",\n \"uisi_autorageshake_app\": \"element-auto-uisi\",\n \"show_labs_settings\": true,\n \"room_directory\": {\n \"servers\": [\n \"${bound:route:name-homeserver}\",\n \"matrix.org\",\n \"gitter.im\",\n \"libera.chat\"\n ]\n },\n \"enable_presence_by_hs_url\": {\n \"https://matrix.org\": false,\n \"https://matrix-client.matrix.org\": false\n },\n \"terms_and_conditions_links\": [\n {\n \"url\": \"https://element.io/privacy\",\n \"text\": \"Privacy Policy\"\n },\n {\n \"url\": \"https://element.io/cookie-policy\",\n \"text\": \"Cookie Policy\"\n }\n ],\n \"features\": {\n \"feature_video_rooms\": true,\n \"feature_rust_crypto\": true\n },\n \"element_call\": {\n \"url\": \"https://call.element.dev\"\n }\n}\n",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"matrix.org": "the public room directory and the federation's largest homeserver; the world's",
|
||||||
|
"matrix-client.matrix.org": "the same homeserver's client endpoint; the world's",
|
||||||
|
"vector.im": "Element's public identity server; the world's",
|
||||||
|
"scalar.vector.im": "Element's public integration manager; the world's",
|
||||||
|
"scalar-staging.vector.im": "Element's staging integration manager, named by the upstream default config; the world's",
|
||||||
|
"scalar-staging.riot.im": "the same, under its former name; the world's",
|
||||||
|
"element.io": "Element's bug reports, privacy and cookie pages; the world's",
|
||||||
|
"gitter.im": "a public room directory; the world's",
|
||||||
|
"libera.chat": "a public room directory; the world's",
|
||||||
|
"call.element.dev": "Element Call's public instance; the world's"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "matrix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "homeserver",
|
||||||
|
"type": "container",
|
||||||
|
"name": "matrix",
|
||||||
|
"image": "matrixconduit/matrix-conduit@sha256:b0d24248e94f944ca49f90f10c429e3d65f4472bdde25661ecea9840134fb133",
|
||||||
|
"network": "matrix",
|
||||||
|
"env": {
|
||||||
|
"CONDUIT_CONFIG": "/etc/conduit/conduit.toml"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"6167"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:db}:/var/lib/matrix-conduit",
|
||||||
|
"${dir:state}/conduit.toml:/etc/conduit/conduit.toml:ro"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"conduit-conf"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "element",
|
||||||
|
"type": "container",
|
||||||
|
"name": "element-web",
|
||||||
|
"image": "vectorim/element-web@sha256:a8f415462ab8d2600a592ba1b92bea51efe5a4d10eb738aab9bed769f7099613",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"network": "matrix",
|
||||||
|
"ports": [
|
||||||
|
"80"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:state}/element.json:/app/config.json:ro"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"element-conf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"module": "memory-pressure",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"pressure.high",
|
||||||
|
"pressure.cleared"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"memory_status",
|
||||||
|
"memory_top",
|
||||||
|
"memory_oom_history",
|
||||||
|
"memory_zram",
|
||||||
|
"memory_oomd",
|
||||||
|
"memory_guard"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "zram-generator",
|
||||||
|
"type": "package",
|
||||||
|
"package": "zram-generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zram",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/zram-generator.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead.\n#\n# Compressed swap in RAM: fast, at a higher priority than any swap on disk, so it takes the everyday\n# pressure before anything spills to a disk. Half the RAM, at most 16 GiB, compressed with zstd.\n# The swap on disk, its size and whether one exists at all are the machine's, not this module's.\n#\n# Read by the generator at boot: a change applies at the next boot.\n[zram0]\nzram-size = min(ram / 2, 16384)\ncompression-algorithm = zstd\nswap-priority = 100\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sysctl-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/sysctl.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swap-tunables",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/sysctl.d/90-memory-pressure.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead.\n#\n# Swap-in reads one page, not eight: read-ahead exists to amortise a disk's seek, and compressed swap in\n# RAM has none — the speculation only costs decompression and memory.\nvm.page-cluster = 0\n#\n# vm.swappiness is deliberately left alone. The usual zram advice (150-180) holds only while the\n# compressed swap has room; once it is full, a higher swappiness moves pages to the disk swap, the thing\n# being avoided. Raise it only together with zram-size.\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sysctl",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "systemd-sysctl.service",
|
||||||
|
"restart-on": [
|
||||||
|
"swap-tunables"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oomd-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/oomd.conf.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oomd-limits",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/oomd.conf.d/memory-pressure.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead.\n#\n# systemd-oomd kills the worst unit before the kernel's OOM killer freezes the machine: when swap is\n# 90 % used, or when a watched unit stalls on memory for 20 s above its limit.\n[OOM]\nSwapUsedLimit=90%\nDefaultMemoryPressureLimit=60%\nDefaultMemoryPressureDurationSec=20s\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root-slice-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/-.slice.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root-slice",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/-.slice.d/10-oomd.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead.\n#\n# systemd-oomd may act on swap exhaustion across the whole machine.\n[Slice]\nManagedOOMSwap=kill\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-manager-drop-ins",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/systemd/system/user@.service.d",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-manager",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/user@.service.d/10-oomd.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead.\n#\n# systemd-oomd may kill the worst unit in a person's service manager when its memory pressure stays\n# above 80 % — instead of the kernel freezing the machine.\n[Service]\nManagedOOMMemoryPressure=kill\nManagedOOMMemoryPressureLimit=80%\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oomd",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "systemd-oomd.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"oomd-limits",
|
||||||
|
"root-slice",
|
||||||
|
"user-manager"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/memory-pressure",
|
||||||
|
"binary": "memory-pressure",
|
||||||
|
"loads": [
|
||||||
|
"memory-pressure"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-catalog",
|
||||||
|
"slug": "catalog",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "mesh-catalog",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "mesh_catalog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"consumes": [
|
||||||
|
"mesh-build-machine.built",
|
||||||
|
"mesh-controller.built-before"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"registered",
|
||||||
|
"upgraded",
|
||||||
|
"rebuild-needed",
|
||||||
|
"catching-up"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-url",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/database.url",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prepare",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-catalog-prepare",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"node",
|
||||||
|
"prepare/index.js"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"prepare/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-cli",
|
||||||
|
"version": "1",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "program",
|
||||||
|
"type": "archive",
|
||||||
|
"artifact": "program",
|
||||||
|
"path": "/usr/local/bin"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shell": [
|
||||||
|
{
|
||||||
|
"for": "zsh",
|
||||||
|
"slot": "normal",
|
||||||
|
"code": "# The operator's command (module mesh-cli, novox/hq ADR 0272): nox is mesh-cli, in interactive zsh only.\n# A script, sudo and a document meant to work everywhere say mesh-cli.\nalias nox=mesh-cli\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "program",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-cli",
|
||||||
|
"binary": "mesh-cli"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-delivery",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "deliver",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "mesh-delivery",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"deliveries",
|
||||||
|
"times",
|
||||||
|
"show",
|
||||||
|
"groups",
|
||||||
|
"what-if",
|
||||||
|
"checks",
|
||||||
|
"table",
|
||||||
|
"stalled",
|
||||||
|
"recheck",
|
||||||
|
"release",
|
||||||
|
"stop",
|
||||||
|
"close",
|
||||||
|
"retire-history"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"gitea.pull.updated",
|
||||||
|
"gitea.pull.merged",
|
||||||
|
"gitea.pull.closed",
|
||||||
|
"mesh-controller.checked",
|
||||||
|
"mesh-controller.plan-moved"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"transition",
|
||||||
|
"group"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.delivery-plan",
|
||||||
|
"seat:mesh-controller.delivery-order",
|
||||||
|
"seat:mesh-controller.delivery-check",
|
||||||
|
"seat:mesh-controller.deliver",
|
||||||
|
"seat:mesh-controller.delivery-stop",
|
||||||
|
"seat:mesh-controller.delivery-walks",
|
||||||
|
"gitea.gitea_note_append",
|
||||||
|
"gitea.gitea_delivery_view",
|
||||||
|
"gitea.gitea_commit_status",
|
||||||
|
"gitea.gitea_commit_statuses",
|
||||||
|
"gitea.gitea_contains",
|
||||||
|
"gitea.gitea_open_pulls"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
"deliveries",
|
||||||
|
"groups"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"delivery_status"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-delivery",
|
||||||
|
"binary": "mesh-delivery",
|
||||||
|
"loads": [
|
||||||
|
"mesh-delivery"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-vault",
|
||||||
|
"version": "1",
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "secret",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"in": "a vault entry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"provisioned",
|
||||||
|
"rotated",
|
||||||
|
"deprovisioned"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mesh-vault.provisioned",
|
||||||
|
"mesh-vault.rotated",
|
||||||
|
"mesh-vault.deprovisioned"
|
||||||
|
],
|
||||||
|
"receives": {
|
||||||
|
"secret": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"secret": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"keeps": "/var/lib/mesh-vault/root",
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"path": "${dir:state}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the vault's own keys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ledger",
|
||||||
|
"path": "${dir:ledger}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every secret the vault keeps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root",
|
||||||
|
"path": "${dir:root}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the vault's root material"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"secret": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "ledger",
|
||||||
|
"why": "a secret given to a consumer is kept nowhere else in the clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ledger",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
||||||
|
"MESH_VAULT_LEDGER": "${dir:ledger}",
|
||||||
|
"MESH_VAULT_ROOT": "${dir:root}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "mesh-vault",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-watcher",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "watch",
|
||||||
|
"consumes": [
|
||||||
|
"mesh-controller.doctor-heartbeat"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"mesh-watcher.watcher_ping",
|
||||||
|
"seat:mesh-controller.seats"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"telegram-token": "${dir:state}/telegram-token"
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"watcher_status",
|
||||||
|
"watcher_last_heard",
|
||||||
|
"watcher_test",
|
||||||
|
"watcher_ping"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"telegram-chat-id\": \"\"\n}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-watcher",
|
||||||
|
"binary": "mesh-watcher",
|
||||||
|
"loads": [
|
||||||
|
"mesh-watcher"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_WATCHER_SETTINGS": "${dir:state}/settings.json",
|
||||||
|
"MESH_WATCHER_TELEGRAM_TOKEN_FILE": "${dir:state}/telegram-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"module": "messenger",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "msgr",
|
||||||
|
"runs-as": "messenger",
|
||||||
|
"seats": [
|
||||||
|
{
|
||||||
|
"name": "operator-channel",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"open",
|
||||||
|
"history",
|
||||||
|
"notify",
|
||||||
|
"asks"
|
||||||
|
],
|
||||||
|
"accepts": [
|
||||||
|
"ask",
|
||||||
|
"cancel"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"decided"
|
||||||
|
],
|
||||||
|
"by-caller": [
|
||||||
|
"ask",
|
||||||
|
"cancel",
|
||||||
|
"decided"
|
||||||
|
],
|
||||||
|
"records": [
|
||||||
|
"asks"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "channel",
|
||||||
|
"scope": "mesh",
|
||||||
|
"kinded": true,
|
||||||
|
"accepts": [
|
||||||
|
"show",
|
||||||
|
"edit",
|
||||||
|
"send"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intake",
|
||||||
|
"scope": "mesh",
|
||||||
|
"kinded": true,
|
||||||
|
"emits": [
|
||||||
|
"choice",
|
||||||
|
"link",
|
||||||
|
"failed",
|
||||||
|
"standing"
|
||||||
|
],
|
||||||
|
"proofs": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "operator-channel",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"open",
|
||||||
|
"history",
|
||||||
|
"notify",
|
||||||
|
"asks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uses": [
|
||||||
|
"channel"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mesh-controller.condition-raised",
|
||||||
|
"mesh-controller.condition-changed",
|
||||||
|
"mesh-controller.condition-cleared",
|
||||||
|
"intake.choice",
|
||||||
|
"intake.link",
|
||||||
|
"intake.failed",
|
||||||
|
"intake.standing"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"refused"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.conditions",
|
||||||
|
"seat:mesh-controller.root-free",
|
||||||
|
"seat:issue-tracker.tracking"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
"open",
|
||||||
|
"sent",
|
||||||
|
{
|
||||||
|
"name": "asks",
|
||||||
|
"ttl-seconds": 2592000
|
||||||
|
},
|
||||||
|
"identities"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:state}/broker"
|
||||||
|
},
|
||||||
|
"secrets-owner": "messenger",
|
||||||
|
"tools": [
|
||||||
|
"messenger_status",
|
||||||
|
"messenger_recent",
|
||||||
|
"messenger_test",
|
||||||
|
"messenger_preview",
|
||||||
|
"messenger_check",
|
||||||
|
"messenger_identities",
|
||||||
|
"messenger_unlink"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"type": "user",
|
||||||
|
"name": "messenger",
|
||||||
|
"shell": "/usr/bin/nologin",
|
||||||
|
"home": "/var/lib/messenger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": ".",
|
||||||
|
"owner": "messenger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"time-zone\": \"\"\n}\n",
|
||||||
|
"owner": "messenger"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/messenger",
|
||||||
|
"binary": "messenger",
|
||||||
|
"loads": [
|
||||||
|
"messenger"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_MESSENGER_SETTINGS": "${dir:state}/settings.json",
|
||||||
|
"MESH_MESSENGER_STATE": "${dir:state}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"module": "minio",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "holds the photos themselves (irreplaceable, kept by photos) for its consumers: a person takes each build, after a backup (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "s3-bucket",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 20,
|
||||||
|
"in": "an S3 access key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"api": {
|
||||||
|
"label": "files-api",
|
||||||
|
"endpoint": "s3"
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"label": "files",
|
||||||
|
"endpoint": "console"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"bucket.created",
|
||||||
|
"bucket.removed"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "s3",
|
||||||
|
"port": 9000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the S3 endpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "console",
|
||||||
|
"port": 9001,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the admin console"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"s3-bucket": {
|
||||||
|
"scheme": "http",
|
||||||
|
"region": "eu-west",
|
||||||
|
"port": 9000,
|
||||||
|
"bucket": "${consumer:as:dns}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"s3-bucket": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"s3-bucket": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"root": "${dir:state}/root.secret"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "every consumer's bucket and its objects"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"s3-bucket": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "a consumer's objects are the only copy of what it stored; a consumer that keeps something irreplaceable here says so (kept-by)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/root.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MINIO_ROOT_USER=meshroot\nMINIO_BROWSER_REDIRECT_URL=https://${bound:route:name-console}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/minio-store",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "minio-net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "minio",
|
||||||
|
"image": "docker.io/pgsty/minio@sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372",
|
||||||
|
"network": "minio-net",
|
||||||
|
"args": [
|
||||||
|
"server",
|
||||||
|
"/data",
|
||||||
|
"--console-address",
|
||||||
|
":9001"
|
||||||
|
],
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/root.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"9000",
|
||||||
|
"9001"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/minio-store:/data",
|
||||||
|
"${dir:state}/root.secret:/run/secrets/root:ro"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root",
|
||||||
|
"MINIO_REGION": "eu-west"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "client",
|
||||||
|
"type": "package",
|
||||||
|
"package": "minio-client"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_MINIO_ENDPOINT": "http://127.0.0.1:${port:9000}",
|
||||||
|
"MESH_MINIO_ROOT_USER": "meshroot",
|
||||||
|
"MESH_MINIO_ROOT_PASSWORD_FILE": "${dir:state}/root.secret",
|
||||||
|
"MESH_MINIO_REGION": "eu-west",
|
||||||
|
"MESH_MINIO_MC_BIN": "mcli",
|
||||||
|
"MESH_MINIO_MC_CONFIG": "${dir:state}/mc",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"module": "model-usage",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "usage",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "model_usage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"consumes": [
|
||||||
|
"*.usage.*"
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"path": "${dir:state}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "the mesh's own files for it; its records are in its database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spool",
|
||||||
|
"path": "${dir:spool}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "usage events the store could not take yet; after the bus gives one up, the only copy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-url",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/database.url",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spool",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prepare",
|
||||||
|
"type": "process",
|
||||||
|
"name": "model-usage-prepare",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"node",
|
||||||
|
"prepare/index.js"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"prepare/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url",
|
||||||
|
"USAGE_SPOOL": "${dir:spool}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"module": "mongodb",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "a provider whose restart drops every consumer on its machine, and which holds the photos' albums (irreplaceable, kept by photos): a person takes each build, after a backup (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "mongodb-database",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 63,
|
||||||
|
"in": "a MongoDB database name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"database.provisioned",
|
||||||
|
"database.deprovisioned"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mongodb.database.provisioned",
|
||||||
|
"mongodb.database.deprovisioned"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "database",
|
||||||
|
"port": 27017,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "modules on any machine that were granted a database"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"mongodb-database": {
|
||||||
|
"port": 27017
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"mongodb-database": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"mongodb-database": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"root": "${dir:state}/root.secret"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"backup": {
|
||||||
|
"dump": "docker exec mongodb-server sh -c 'printf \"password: %s\\n\" \"$(cat /run/secrets/root)\" > /tmp/.backup.yaml && mongodump --quiet --config /tmp/.backup.yaml --username root --authenticationDatabase admin --archive; s=$?; rm -f /tmp/.backup.yaml; exit $s' > ${dir:dumps}/all.archive.partial && mv ${dir:dumps}/all.archive.partial ${dir:dumps}/all.archive",
|
||||||
|
"into": "dumps"
|
||||||
|
},
|
||||||
|
"why": "every consumer's database; copied by the dump, not as live files"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dumps",
|
||||||
|
"path": "${dir:dumps}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "last night's dump, made again every night"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"mongodb-database": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "a consumer's documents are the only copy of what it wrote; a consumer that keeps something irreplaceable here says so (kept-by)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dumps",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "mongodb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-root",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server-root.secret",
|
||||||
|
"mode": "0400",
|
||||||
|
"owner": "999:999",
|
||||||
|
"content": "${secret:root}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mongodb-server",
|
||||||
|
"image": "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3",
|
||||||
|
"health": {
|
||||||
|
"kind": "tcp",
|
||||||
|
"endpoint": "database"
|
||||||
|
},
|
||||||
|
"network": "mongodb",
|
||||||
|
"env": {
|
||||||
|
"MONGO_INITDB_ROOT_USERNAME": "root",
|
||||||
|
"MONGO_INITDB_ROOT_PASSWORD_FILE": "/run/secrets/root"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"27017"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/data/db",
|
||||||
|
"${dir:state}/server-root.secret:/run/secrets/root:ro"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_PROVISION_MONGODB": "mongodb://root@127.0.0.1:${port:27017}/admin?authSource=admin",
|
||||||
|
"MESH_PROVISION_PASSWORD_FILE": "${dir:state}/root.secret",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"module": "mosquitto",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "mosq",
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "mqtt-topic",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"in": "a Mosquitto client and topic prefix"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"topic.provisioned",
|
||||||
|
"topic.deprovisioned"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mosquitto.topic.provisioned",
|
||||||
|
"mosquitto.topic.deprovisioned"
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"mqtt-topic": {
|
||||||
|
"scheme": "mqtt",
|
||||||
|
"port": 1883
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"mqtt-topic": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"mqtt-topic": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": {
|
||||||
|
"path": "${dir:mesh-state}/admin",
|
||||||
|
"taken": "at-start"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "mqtt",
|
||||||
|
"port": 1883,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "modules on any machine that were granted a topic namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mqtt-websockets",
|
||||||
|
"port": 8081,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the same broker over MQTT-on-WebSockets, for browser clients"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "retained messages and sessions; devices publish them again"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"mqtt-topic": {
|
||||||
|
"class": "rebuildable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "retained messages are published again by the devices"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1883:1883"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/mosquitto.conf",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "1883:1883",
|
||||||
|
"content": "persistence true\npersistence_location /mosquitto/data\n\nlog_dest stdout\nlog_type warning\nlog_type error\nlog_type notice\n\n# Every client authenticates; identities and their per-topic ACLs are managed\n# at runtime by the dynamic security plugin, whose store the plugin itself owns.\nallow_anonymous false\nplugin /usr/lib/mosquitto_dynamic_security.so\nplugin_opt_config_file /mosquitto/data/dynamic-security.json\n\n# MQTT listener\nlistener 1883\n\n# MQTT-over-WebSockets listener\nlistener 8081\nprotocol websockets\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "mosquitto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bootstrap-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/bootstrap.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MESH_PROVISION_MQTT=127.0.0.1:${port:1883}\nMESH_PROVISION_ADMIN_USER=mesh-admin\nMESH_PROVISION_PASSWORD_FILE=${dir:mesh-state}/admin\nMESH_DYNSEC_FILE=${dir:data}/dynamic-security.json\nMESH_MQTT_CTRL_IMAGE=eclipse-mosquitto@sha256:38c0da4f2ef84284d47b3b3eeea1cb3bdeabe81ee10caf0cd5c5ff61ee3ea408\nMESH_MQTT_CTRL_CONTAINER=mosquitto\nMESH_MQTT_ADMIN_APPLIED_FILE=${dir:state}/admin.applied\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bootstrap",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mosquitto-bootstrap",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"node",
|
||||||
|
"bootstrap/index.js"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/bootstrap.env"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"bootstrap-env",
|
||||||
|
"needs-admin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mosquitto",
|
||||||
|
"image": "eclipse-mosquitto@sha256:38c0da4f2ef84284d47b3b3eeea1cb3bdeabe81ee10caf0cd5c5ff61ee3ea408",
|
||||||
|
"health": {
|
||||||
|
"kind": "tcp",
|
||||||
|
"endpoint": "mqtt"
|
||||||
|
},
|
||||||
|
"network": "mosquitto",
|
||||||
|
"ports": [
|
||||||
|
"1883",
|
||||||
|
"8081"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/mosquitto/data",
|
||||||
|
"${dir:state}/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js",
|
||||||
|
"bootstrap/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
||||||
|
"MESH_PROVISION_MQTT": "127.0.0.1:${port:1883}",
|
||||||
|
"MESH_PROVISION_ADMIN_USER": "mesh-admin",
|
||||||
|
"MESH_PROVISION_PASSWORD_FILE": "${dir:mesh-state}/admin",
|
||||||
|
"MESH_MQTT_CTRL_CONTAINER": "mosquitto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
{
|
||||||
|
"module": "mounts",
|
||||||
|
"version": "1",
|
||||||
|
"requires": [
|
||||||
|
"nfs-share"
|
||||||
|
],
|
||||||
|
"reads": [
|
||||||
|
"nfs-server.exports"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.data"
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-mounts",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"list",
|
||||||
|
"test",
|
||||||
|
"mount",
|
||||||
|
"unmount",
|
||||||
|
"adopt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"mounts_list",
|
||||||
|
"mounts_exports",
|
||||||
|
"mounts_health"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
{
|
||||||
|
"name": "shares",
|
||||||
|
"per-machine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"shares": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "none",
|
||||||
|
"why": "a machine mounts no share of another until its assignment names one: space-separated name=mountpoint, each optionally :ro (hq ADR 0263 rule 5)"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "none",
|
||||||
|
"why": "a machine has no occasional source until its assignment names one: space-separated name=smb://[<user>@]<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro (hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password-<name> (hq ADR 0283)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"smb-password-*": {
|
||||||
|
"path": "${dir:state}/smb-password-*.secret",
|
||||||
|
"issued-by": "outside"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "nfs-utils",
|
||||||
|
"type": "package",
|
||||||
|
"package": "nfs-utils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cifs-utils",
|
||||||
|
"type": "package",
|
||||||
|
"package": "cifs-utils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/mesh-mounts",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/mesh-mounts/mounts.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bus-dir-parent",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh-mounts",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bus-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh-mounts/from-bus",
|
||||||
|
"mode": "0755",
|
||||||
|
"owner": "${machine:account}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "apply",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-mounts-apply",
|
||||||
|
"artifact": "tools-go",
|
||||||
|
"run": [
|
||||||
|
"./mounts",
|
||||||
|
"apply"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "watch",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-mounts-watch",
|
||||||
|
"artifact": "tools-go",
|
||||||
|
"run": [
|
||||||
|
"./mounts",
|
||||||
|
"watch"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "tool",
|
||||||
|
"tool": "mounts_health",
|
||||||
|
"interval": "60s",
|
||||||
|
"timeout": "10s",
|
||||||
|
"looks": 2,
|
||||||
|
"grace": "90s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mounts",
|
||||||
|
"binary": "mounts",
|
||||||
|
"loads": [
|
||||||
|
"mounts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
"module": "mssql",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "a provider whose restart drops every consumer on its machine, and whose new version may upgrade its databases in place: a person takes each build, after a backup (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "mssql-database",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": {
|
||||||
|
"max": 128,
|
||||||
|
"in": "a SQL Server login and database name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"database.provisioned",
|
||||||
|
"database.deprovisioned"
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"mssql.database.provisioned",
|
||||||
|
"mssql.database.deprovisioned"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "database",
|
||||||
|
"port": 1433,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "modules on any machine that were granted a database, and the people who were given its address; the machine port is the assignment's to pin"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"mssql-database": {}
|
||||||
|
},
|
||||||
|
"receives": {
|
||||||
|
"mssql-database": "${dir:grants}/mesh.json"
|
||||||
|
},
|
||||||
|
"grants": {
|
||||||
|
"mssql-database": "${dir:grants}"
|
||||||
|
},
|
||||||
|
"own-secrets": {
|
||||||
|
"sa": "${dir:state}/sa.secret",
|
||||||
|
"reader": "${dir:state}/reader.secret"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"backup": {
|
||||||
|
"dump": "{ cat ${dir:state}/sa.secret; echo; cat ${dir:state}/backup.sql; } | docker exec -i mssql sh -c 'read -r p; SQLCMDPASSWORD=\"$p\" exec /opt/mssql-tools18/bin/sqlcmd -C -b -S localhost -U sa -i /dev/stdin'",
|
||||||
|
"into": "dumps"
|
||||||
|
},
|
||||||
|
"why": "every consumer's database; copied by the dump, not as live files"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dumps",
|
||||||
|
"path": "${dir:dumps}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "last night's dump, made again every night"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumers": {
|
||||||
|
"mssql-database": {
|
||||||
|
"class": "valuable",
|
||||||
|
"in": "data",
|
||||||
|
"why": "a consumer's rows are the only copy of what it wrote"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grants",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sa-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/sa.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "ACCEPT_EULA=Y\nMSSQL_SA_PASSWORD=${secret:sa}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "10001:0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dumps",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${dir:data}/backup",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "10001:0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "mssql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mssql",
|
||||||
|
"image": "mcr.microsoft.com/mssql/server@sha256:4402d880dd4c34bfa7d8705e56a86cd6c88da80a1f6bbbe741f999e76264a090",
|
||||||
|
"network": "mssql",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/sa.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"1433"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/var/opt/mssql"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "the image documents only MSSQL_SA_PASSWORD, no _FILE and no configuration field; not convertible without a wrapper entrypoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "backup-sql",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/backup.sql",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "SET NOCOUNT ON;\nDECLARE @n sysname, @s nvarchar(max);\nDECLARE c CURSOR LOCAL FAST_FORWARD FOR\n SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL;\nOPEN c;\nFETCH NEXT FROM c INTO @n;\nWHILE @@FETCH_STATUS = 0\nBEGIN\n SET @s = N'BACKUP DATABASE ' + QUOTENAME(@n) + N' TO DISK = N''/var/opt/mssql/backup/' + REPLACE(@n, N'''', N'''''') + N'.bak'' WITH INIT, COPY_ONLY, CHECKSUM';\n EXEC (@s);\n FETCH NEXT FROM c INTO @n;\nEND\nCLOSE c;\nDEALLOCATE c;\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js",
|
||||||
|
"provisioner/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_PROVISION_MSSQL": "mssql://sa@127.0.0.1:${port:1433}/master",
|
||||||
|
"MESH_PROVISION_PASSWORD_FILE": "${dir:state}/sa.secret",
|
||||||
|
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
||||||
|
"MESH_MSSQL_READER_PASSWORD_FILE": "${dir:state}/reader.secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"module": "n8n",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "n8n"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "n8n",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"accesses": [
|
||||||
|
{
|
||||||
|
"id": "media",
|
||||||
|
"mode": "read-write"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 5678,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the n8n editor, its REST API and the webhook endpoints workflows are triggered through; a public name is the route's"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the instance's encryption key, settings and binary data; workflows are in the database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cache",
|
||||||
|
"path": "${dir:cache}",
|
||||||
|
"class": "cache",
|
||||||
|
"why": "scratch space"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1000:1000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cache",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "999:999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "database-secret",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/n8n-database.secret",
|
||||||
|
"mode": "0400",
|
||||||
|
"owner": "1000:1000",
|
||||||
|
"content": "${secret:postgres-database}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "N8N_HOST=${bound:route:name}\nN8N_PORT=5678\nN8N_PROTOCOL=https\nWEBHOOK_URL=https://${bound:route:name}/\nNODE_FUNCTION_ALLOW_BUILTIN=*\nNODE_FUNCTION_ALLOW_EXTERNAL=*\nDB_TYPE=postgresdb\nDB_POSTGRESDB_HOST=${bound:postgres-database:at}\nDB_POSTGRESDB_PORT=${bound:postgres-database:port}\nDB_POSTGRESDB_DATABASE=${bound:postgres-database:as}\nDB_POSTGRESDB_USER=${bound:postgres-database:as}\nDB_POSTGRESDB_PASSWORD_FILE=/run/secrets/database\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "n8n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "n8n",
|
||||||
|
"artifact": "server",
|
||||||
|
"network": "n8n",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"5678"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/home/node/.n8n",
|
||||||
|
"${dir:state}/n8n-database.secret:/run/secrets/database:ro",
|
||||||
|
"${access:media}:/media-library"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cache-server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "n8n-redis",
|
||||||
|
"image": "redis@sha256:8a1efc5f479551822b47424ccae982026b633f28818eab0387348120a61e10e2",
|
||||||
|
"network": "n8n",
|
||||||
|
"args": [
|
||||||
|
"redis-server",
|
||||||
|
"--appendonly",
|
||||||
|
"yes"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:cache}:/data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "browser",
|
||||||
|
"type": "container",
|
||||||
|
"name": "n8n-selenium",
|
||||||
|
"image": "selenium/standalone-chrome@sha256:9ae1c78e9b2ca9fe4b22e57873b5ee34aeb8e814e3122293eef4c3abe4c5f448",
|
||||||
|
"network": "n8n",
|
||||||
|
"env": {
|
||||||
|
"SE_ENABLE_TRACING": "false",
|
||||||
|
"SE_NODE_MAX_SESSIONS": "5",
|
||||||
|
"SE_NODE_OVERRIDE_MAX_SESSIONS": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "N8N_BASE",
|
||||||
|
"image": "n8nio/n8n@sha256:4846eb2f4b874ab04cde7fc1e249d2ddaec66e9aea64439beb2972cfea88e3c0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "Dockerfile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# nats's server image: the upstream server, plus an entrypoint that reloads it in place when the
|
||||||
|
# mesh rewrites its configuration. See entrypoint.sh for why that belongs here and not in the host.
|
||||||
|
#
|
||||||
|
# **Pinned to the multi-architecture index digest, not a platform's.** `docker manifest inspect`
|
||||||
|
# reports a platform manifest per architecture and the index that lists them; pinning a platform's
|
||||||
|
# digest builds on this workstation and fails on any node of another architecture, with an error
|
||||||
|
# that names a manifest rather than the mistake. This is the index — `docker pull` reports the same
|
||||||
|
# one, and `RepoDigests` confirms it.
|
||||||
|
#
|
||||||
|
# **The release the digest is, said here because a digest does not say it:**
|
||||||
|
#
|
||||||
|
# upstream: nats 2.11.17-alpine
|
||||||
|
#
|
||||||
|
# Kept equal to the server version cmd/nats-tools tests against (its go.mod), and a test there fails
|
||||||
|
# when they differ: that test is what says the server delivers every message to a consumer with
|
||||||
|
# several filters. 2.10.29 did not — it moved such a consumer past a message now and then without
|
||||||
|
# handing it over, and the controller never heard of a merge (novox/hq issue 266).
|
||||||
|
#
|
||||||
|
# Unlike every other module's Dockerfile, this builds no TypeScript and uses no mesh base image:
|
||||||
|
# the module's code is the server, which upstream already built. There is no BUILD_BASE here on
|
||||||
|
# purpose — the server is not compiled; only the snapshot program below is. The upstream image is
|
||||||
|
# declared in the manifest under build.on and arrives as NATS_BASE, like every other base the mesh
|
||||||
|
# copies into its own store before a build (novox/hq ADR 0097); the digest above is the index one
|
||||||
|
# for the reason given. So does GO_BASE, the toolchain the snapshot program is built with.
|
||||||
|
#
|
||||||
|
# **One thing is compiled: the bus's snapshot program** (novox/hq ADR 0235). The machine's backup
|
||||||
|
# holder runs the module's declared dump — `docker exec` into this container — and the program asks
|
||||||
|
# the server for each stream through the snapshot API, writing a tar on stdout. It is here, beside
|
||||||
|
# the server, for two reasons: the dump runs where the server is without mounting anything into it,
|
||||||
|
# and a restore needs nats-server itself — the very binary this image already carries — to fill a new
|
||||||
|
# store. Its own Go module (snapshot/go.mod), so this build fetches only public modules.
|
||||||
|
ARG NATS_BASE
|
||||||
|
ARG GO_BASE
|
||||||
|
FROM ${GO_BASE} AS snapshot
|
||||||
|
WORKDIR /src
|
||||||
|
COPY snapshot/go.mod snapshot/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY snapshot/*.go ./
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o /mesh-nats-snapshot .
|
||||||
|
|
||||||
|
FROM ${NATS_BASE}
|
||||||
|
|
||||||
|
COPY entrypoint.sh /usr/local/bin/mesh-nats-entrypoint
|
||||||
|
RUN chmod 0755 /usr/local/bin/mesh-nats-entrypoint
|
||||||
|
COPY --from=snapshot /mesh-nats-snapshot /usr/local/bin/mesh-nats-snapshot
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/mesh-nats-entrypoint"]
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"module": "nats",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "the bus: replaced only as a planned step a person starts (`bus upgrade`), its streams snapshotted first and checked after (hq ADR 0236); the controller holds this whatever is said here"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "mesh-bus",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "mesh-broker",
|
||||||
|
"scope": "mesh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bus-users": "/var/lib/nats-module/conf/accounts.conf",
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:mesh-state}/broker"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"emits": [],
|
||||||
|
"consumes": [],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "bus",
|
||||||
|
"port": 4222,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the mesh bus — every link the mesh has, over TLS, reached across the overlay"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"nats_server",
|
||||||
|
"nats_connections",
|
||||||
|
"nats_closed_connections",
|
||||||
|
"nats_subscriptions",
|
||||||
|
"nats_streams",
|
||||||
|
"nats_backlog",
|
||||||
|
"nats_buckets",
|
||||||
|
"nats_users",
|
||||||
|
"nats_user_can"
|
||||||
|
],
|
||||||
|
"guards": [
|
||||||
|
8222
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "jetstream",
|
||||||
|
"path": "${dir:jetstream-data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"active": "1d",
|
||||||
|
"backup": {
|
||||||
|
"dump": "docker exec -i mesh-broker-nats mesh-nats-snapshot snapshot < ${dir:mesh-state}/broker > ${dir:snapshots}/bus.tar.partial && mv ${dir:snapshots}/bus.tar.partial ${dir:snapshots}/bus.tar || { rm -f ${dir:snapshots}/bus.tar.partial; exit 1; }",
|
||||||
|
"into": "snapshots"
|
||||||
|
},
|
||||||
|
"why": "the bus's streams and key-value buckets: the hand-act log, conditions, every module's state; written all the time, so copied by the server's own snapshot of each stream (novox/hq ADR 0235), never as live files"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "snapshots",
|
||||||
|
"path": "${dir:snapshots}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"why": "last night's snapshot of every stream with its manifest, made again every night"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "jetstream-data",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/mesh-broker-nats",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "conf-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/nats-module/conf",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "snapshots",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/var/lib/nats-module/conf/nats.conf",
|
||||||
|
"content": "# The nats module's own server settings. Declared by the module, because a port, a TLS path\n# and a store directory are properties of the container this module raises: they live in its\n# image and its mounts and change when it does.\n#\n# The mesh writes accounts.conf beside this one and nothing else. A controller that wrote the\n# whole file would have to be kept in step with a Dockerfile it never sees.\n\nport: 4222\n# Monitoring on every interface *inside* the container, so the publish below can reach it; the publish\n# is 127.0.0.1:8222 on the machine, so nothing beyond the machine reaches it, and the module guards 8222\n# too. Bound to the container's own loopback until 2026-10-04, the published port answered nothing\n# (connection reset), and the only way in was `docker exec`.\nhttp: 0.0.0.0:8222\n\n# The mesh's own broker certificate — the one every machine already pins by fingerprint and the\n# controller already trusts (MESH_BROKER_CERTIFICATE). Serving the new bus with it means no\n# machine's pin changes when it moves, and no second certificate exists to be wrong about.\ntls {\n cert_file: \"/tls/tls.crt\"\n key_file: \"/tls/tls.key\"\n}\n\n# **No `verify`, deliberately, and it was `verify: true` until a probe ran this image.** That\n# setting makes the server demand a *client* certificate, and nothing in the mesh presents one: a\n# host pins this server's exact certificate and authenticates with the password the mesh minted\n# (novox/hq ADR 0004, design 25 §4), and so does a module's runtime. With it on, every connection\n# in the mesh is refused at the TLS handshake, before any password is looked at — and the error is\n# \"client didn't provide a certificate\", which reads as a client fault.\n#\n# TLS is still required: a tls block is what makes it required, and verify only decides whether\n# client certificates are checked. What is given up is a second factor the mesh has no machinery\n# to issue or rotate — a certificate per module per node — and what is kept is stronger than a\n# name check in both directions: an exact pin outward, a per-user password inward.\n\njetstream {\n store_dir: \"/data\"\n}\n\n# Every user of the mesh, composed by the controller and rewritten whenever a module is\n# assigned, a node enrols or a person's access changes.\n#\n# **Relative, and in this same directory, because it has to be.** An absolute include path is\n# resolved relative to the including file's directory, not from the root: nats-server given\n# `include /etc/nats/accounts.conf` from /etc/nats-server/nats.conf looks for\n# /etc/nats-server/etc/nats/accounts.conf and refuses to start. Verified against the server.\ninclude accounts.conf\n",
|
||||||
|
"mode": "0644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "mesh-broker-nats",
|
||||||
|
"ports": [
|
||||||
|
"4222:4222",
|
||||||
|
"127.0.0.1:8222:8222"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/mesh-broker-nats:/data",
|
||||||
|
"/var/lib/nats-module/conf:/etc/nats:ro",
|
||||||
|
"${access:tls}:/tls:ro"
|
||||||
|
],
|
||||||
|
"artifact": "server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accesses": [
|
||||||
|
{
|
||||||
|
"id": "tls",
|
||||||
|
"path": "/var/lib/mesh-broker-tls",
|
||||||
|
"mode": "read"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "NATS_BASE",
|
||||||
|
"image": "nats@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"arg": "GO_BASE",
|
||||||
|
"image": "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "Dockerfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/nats-tools",
|
||||||
|
"binary": "nats-tools",
|
||||||
|
"loads": [
|
||||||
|
"nats-tools"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_NATS_MONITOR": "http://127.0.0.1:8222",
|
||||||
|
"MESH_NATS_CONTAINER": "mesh-broker-nats",
|
||||||
|
"MESH_NATS_USERS_FILE": "/etc/nats/accounts.conf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"module": "netcheck",
|
||||||
|
"version": "1",
|
||||||
|
"tools": [
|
||||||
|
"netcheck_tcp",
|
||||||
|
"netcheck_dns",
|
||||||
|
"netcheck_http",
|
||||||
|
"netcheck_listening"
|
||||||
|
],
|
||||||
|
"replaces": {
|
||||||
|
"netcheck_listening": [
|
||||||
|
"ss -lunt",
|
||||||
|
"ss -ltn",
|
||||||
|
"netstat -lnt"
|
||||||
|
],
|
||||||
|
"netcheck_dns": [
|
||||||
|
"dig",
|
||||||
|
"nslookup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools-go",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/netcheck",
|
||||||
|
"binary": "netcheck",
|
||||||
|
"loads": [
|
||||||
|
"netcheck"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tools-typescript",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"module": "networkmanager",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "the machine's network: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"slug": "nm",
|
||||||
|
"requires": [
|
||||||
|
"wildcard-resolution"
|
||||||
|
],
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager",
|
||||||
|
"uplink-networkmanager"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-uplink",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/uplink-tools",
|
||||||
|
"binary": "uplink-tools",
|
||||||
|
"loads": [
|
||||||
|
"uplink-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"facts": {
|
||||||
|
"resolvers": {
|
||||||
|
"path": "/etc/resolv.conf",
|
||||||
|
"template": "# Managed by the mesh, and written by the module holding this machine's uplink:\n# the program that manages the machine's network would otherwise rewrite this\n# file on every change of network, so its holder is the one that writes it\n# (novox/hq ADR 0117, ADR 0223). Replaced on every push; edit nothing here.\n#\n# Every resolver of the mesh, by address, and nothing else (novox/hq ADR 0223) \u2014\n# this machine's own first when it holds one, then the others by name. Each\n# answers the mesh's names from the same roster and forwards every other name, so\n# whichever answers first gives the one answer. There is no public resolver here:\n# a C library that asks every listed server at once and takes the first reply \u2014\n# musl, so every Alpine container \u2014 took a public resolver's \"no such name\" for\n# a mesh name and failed. A machine that reaches none of these has no names until\n# it does. Containers copy these lines from their machine.\n{{range index .Holders \"mesh-dns-resolver\"}}nameserver {{.Address}}\n{{end}}options timeout:1 attempts:2 edns0\n"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "networkmanager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/NetworkManager/conf.d/50-mesh.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Managed by the mesh (module networkmanager). Replaced on every push; edit the\n# catalogue instead.\n#\n# This machine's uplink is NetworkManager's, and this file is the whole of what\n# the mesh asks of it (novox/hq ADR 0117): leave the resolver file to the mesh,\n# and leave the private network's interface alone. Nothing more. The mesh never\n# declares a connection profile, an address, a route, a wireless network or its\n# credentials \u2014 those are joined at the machine, by the person using it, and\n# the link they make is the only channel the mesh reaches this machine over. A\n# push that got a link wrong could not be undone by the next one.\n#\n# A drop-in of the mesh's own, beside NetworkManager.conf and whatever else the\n# operator keeps in this directory. NetworkManager reads the files here sorted by\n# name and a later one wins a key it sets again \u2014 so a file of the operator's\n# that sorts after this one (any name starting with a letter does) and sets dns=\n# or unmanaged-devices= overrides it. That is the operator's to decide, and the\n# reason this file sets nothing but the two keys it must.\n#\n# NetworkManager itself is the machine's: the mesh never starts, stops, enables\n# or disables it (its service is declared with no state), because stopping it\n# takes every link down, this machine's channel to the mesh included \u2014 and a\n# module unassigned by mistake must not be able to do that. When this file\n# changes, a running NetworkManager is reloaded (its D-Bus Reload call, which\n# re-reads its configuration \u2014 NetworkManager(8)), never restarted.\n\n[main]\n# The resolver file is the mesh's: this module writes /etc/resolv.conf itself,\n# listing the mesh's resolvers (novox/hq ADR 0223). Without this line\n# NetworkManager rewrites that file on\n# every connectivity change \u2014 every network joined, every lease renewed \u2014\n# and the mesh's resolver is silently replaced while every surface of the mesh\n# still reads green. none: \"NetworkManager will not modify resolv.conf. This\n# implies rc-manager unmanaged\" (NetworkManager.conf(5), 1.58). On an adopted\n# machine the predecessor wrote the same line in a file of its own; both say one\n# thing, and the predecessor's is retired by hand after the take.\n#\n# Not NetworkManager's own global DNS ([global-dns-domain-*] with rc-manager=file):\n# it writes its own header and composes the options line itself, so it cannot\n# write the mesh's file byte for byte, and the three uplink modules would write\n# three different files for one fact. dns=none, and the file declared beside it.\ndns=none\n\n[keyfile]\n# mesh0 is the private network's interface: the mesh brings it up and the mesh\n# alone configures it. A manager that considers every interface its own could\n# try to configure it, or tear it down on a profile change.\n#\n# unmanaged-devices rather than a [device-mesh0] section with managed=0, because\n# NetworkManager.conf(5) says a device unmanaged by this key \"is strictly\n# unmanaged and cannot be overruled by using the API like nmcli device set\n# $IFNAME managed yes\", while device*.managed \"can be overruled at runtime via\n# D-Bus\". The same page adds that device*.managed \"may be a better choice\" for\n# exactly those reasons \u2014 for an interface the operator might want to hand back\n# at runtime. For the mesh's own interface, strict is the point.\n#\n# += rather than =: the same page documents appending to a list-valued key set\n# earlier (\"plugins+=another-plugin\") as an extension of its key file format,\n# and unmanaged-devices is a device list. = would replace whatever devices the\n# operator already keeps NetworkManager away from; += adds this one to them\n# (novox/hq ADR 0102: a list is added to, never replaced). A file of the\n# operator's read after this one that sets the key with = replaces it again;\n# that is the operator's to decide.\nunmanaged-devices+=interface-name:mesh0\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "service",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "NetworkManager.service",
|
||||||
|
"reload-on": [
|
||||||
|
"config"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"module": "nextcloud-client",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"nextcloud_status",
|
||||||
|
"nextcloud_log",
|
||||||
|
"nextcloud_restart",
|
||||||
|
"nextcloud_check"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "nextcloud-client"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/nextcloud-client-tools",
|
||||||
|
"binary": "nextcloud-client-tools",
|
||||||
|
"loads": [
|
||||||
|
"nextcloud-client-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"module": "nextcloud",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "ncloud",
|
||||||
|
"requires": [
|
||||||
|
"postgres-database",
|
||||||
|
"s3-bucket",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "nextcloud"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "drive",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json",
|
||||||
|
"s3-bucket": "${dir:state}/store.json",
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret",
|
||||||
|
"s3-bucket": "${dir:state}/store.secret"
|
||||||
|
},
|
||||||
|
"emits": [
|
||||||
|
"user.created",
|
||||||
|
"share.created"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": "${dir:state}/admin.secret"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "files and sync, over http; a public name is a route grant later"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "html",
|
||||||
|
"path": "${dir:html}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "the instance's configuration, its secret and installed apps; the files are in the object store"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=${bound:s3-bucket:bucket}\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "html",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0750",
|
||||||
|
"owner": "33:33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "nextcloud",
|
||||||
|
"image": "nextcloud@sha256:fb966733647ea03f0446b0c22eac9733c8eb616d37b960caca9d4c3010e14a08",
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"80"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:html}:/var/www/html"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "the image honours POSTGRES_PASSWORD_FILE and NEXTCLOUD_ADMIN_PASSWORD_FILE (entrypoint file_env); OBJECTSTORE_S3_SECRET has none and needs a generated config fragment; convertible, awaiting a bed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{}\n",
|
||||||
|
"merge": "json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"index.js",
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_NEXTCLOUD_URL": "http://127.0.0.1:${port:80}",
|
||||||
|
"MESH_NEXTCLOUD_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||||
|
"MESH_NEXTCLOUD_ADMIN_USER": "mesh-admin",
|
||||||
|
"MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
{
|
||||||
|
"module": "nfs-server",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "the folders other machines mount: a build that breaks the exports leaves every client's mount hanging or refused, and the gate on this one machine does not see the clients (hq ADR 0236, ADR 0263)"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager",
|
||||||
|
"service-manager"
|
||||||
|
],
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "nfs-share",
|
||||||
|
"scope": "mesh",
|
||||||
|
"identity": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"consumers": {
|
||||||
|
"nfs-share": {
|
||||||
|
"class": "none",
|
||||||
|
"why": "the shared folders are the operator's data (hq ADR 0051): what a client writes lands in them, and they are protected where the operator declares them, never by this module, which keeps nothing of a consumer's"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-nfs-server",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"exports",
|
||||||
|
"clients",
|
||||||
|
"test",
|
||||||
|
"reload",
|
||||||
|
"adopt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
{
|
||||||
|
"name": "exports",
|
||||||
|
"ttl-seconds": 120,
|
||||||
|
"per-machine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reads": [
|
||||||
|
"mounts.shares"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:mesh-controller.seats",
|
||||||
|
"seat:mesh-controller.data"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"grants": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "none",
|
||||||
|
"why": "which nodes may mount which share, and how, is this machine's to say (hq ADR 0263, review of mesh-catalog #136): share=node:rw|ro[,node:rw|ro], entries separated by spaces; none exports to no node, whatever a node wants"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"facts": {
|
||||||
|
"machines": {
|
||||||
|
"path": "/etc/nfs-server/machines",
|
||||||
|
"template": "# Written by the mesh (module nfs-server, novox/hq ADR 0263): every machine of the mesh and its private\n# address, from which the module takes a node's address. Replaced on every push.\n{{range .Machines}}{{.Name}} {{.Address}}\n{{end}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
"nfs_health"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "nfs",
|
||||||
|
"port": 2049,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"fixed": true,
|
||||||
|
"why": "the shares, to the mesh's machines only (hq ADR 0263): NFS version 4 alone, which needs no other port, and never the home network, where a device that is not a node could claim any user id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "nfs-utils"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nfs-conf",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/nfs.conf.d/50-mesh.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module nfs-server, novox/hq ADR 0263). Replaced on every push; a drop-in of\n# the operator's that sorts after this one overrides it, and is theirs.\n#\n# NFS version 4 only: a client needs port 2049 and nothing else, so the module opens nothing more\n# than that, to the private network. Version 3 needs rpcbind and mountd, on ports the mesh does not open.\n[nfsd]\nvers2=n\nvers3=n\nvers4=y\nvers4.0=n\nvers4.1=y\nvers4.2=y\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "run-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/run/nfs-server",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "nfs-server.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"nfs-conf"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wants-dir-parent",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/nfs-server",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wants-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/nfs-server/from-bus",
|
||||||
|
"mode": "0755",
|
||||||
|
"owner": "${machine:account}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "exports",
|
||||||
|
"type": "process",
|
||||||
|
"name": "nfs-server-exports",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./nfs-server",
|
||||||
|
"exports"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"config"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "tool",
|
||||||
|
"tool": "nfs_health",
|
||||||
|
"interval": "60s",
|
||||||
|
"timeout": "10s",
|
||||||
|
"looks": 2,
|
||||||
|
"grace": "90s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/etc/nfs-server",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/nfs-server/shares.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# Written by the mesh (module nfs-server, novox/hq ADR 0263) from this machine's assignment.\n# Replaced on every push; change the `shares` setting, never this file.\n#\n# The shares: name=folder, or name=folder:ro, one share per folder. The grants: share=node:rw|ro[,…],\n# which nodes may mount each and how. The module's process exports a share to a node's private address\n# only when it is granted here and that node's mounts module wants it, every client mapped to the\n# folder's owner, and only to addresses inside the range below.\nshares=${setting:shares}\ngrants=${setting:grants}\nrange=${machine:mesh-range}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/nfs-server",
|
||||||
|
"binary": "nfs-server",
|
||||||
|
"loads": [
|
||||||
|
"nfs-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"module": "nftables",
|
||||||
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "the machine's packet filter: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"firewall"
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-packet-filter",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"rules",
|
||||||
|
"reload",
|
||||||
|
"remove"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filtering": {
|
||||||
|
"into": "/etc/nftables.conf"
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "nftables"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "legacy-tools",
|
||||||
|
"type": "package",
|
||||||
|
"package": "iptables"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/mesh-filter.service",
|
||||||
|
"content": "[Unit]\nDescription=The mesh's packet filter, derived from what is assigned to this node\nWants=network-pre.target\nBefore=network-pre.target\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=nft -f /etc/nftables.conf\nExecReload=nft -f /etc/nftables.conf\nExecStop=nft delete table inet mesh\n\n[Install]\nWantedBy=multi-user.target\n",
|
||||||
|
"mode": "0644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stock-unit-stop",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/etc/systemd/system/nftables.service.d/mesh.conf",
|
||||||
|
"content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) — a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n",
|
||||||
|
"mode": "0644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "load",
|
||||||
|
"type": "service",
|
||||||
|
"unit": "mesh-filter.service",
|
||||||
|
"state": "running",
|
||||||
|
"boot": "enabled",
|
||||||
|
"restart-on": [
|
||||||
|
"unit",
|
||||||
|
"stock-unit-stop"
|
||||||
|
],
|
||||||
|
"reload-on": [
|
||||||
|
"filtering"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "front-end",
|
||||||
|
"type": "package",
|
||||||
|
"package": "ufw",
|
||||||
|
"absent": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"firewall_rules"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"module": "nm-applet",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"package-manager"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"x11-display"
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"nm_applet_status",
|
||||||
|
"nm_applet_restart",
|
||||||
|
"nm_applet_check"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "network-manager-applet"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/nm-applet-tools",
|
||||||
|
"binary": "nm-applet-tools",
|
||||||
|
"loads": [
|
||||||
|
"nm-applet-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"module": "node-env",
|
||||||
|
"version": "1",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-environment",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-config-dir",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:account-home}/.config/mesh",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "environment-d",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${machine:account-home}/.config/environment.d",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0755"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "posix",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/mesh/environment.sh",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# The operator account's environment, generated by the mesh (module node-env, novox/hq ADR 0203).\n# Do not edit: this file is replaced at every push. Every line names the module that contributed it.\n# Sourced by the login shell from its always-read startup file (for zsh, ~/.zshenv), so a script, a\n# login and the shell's execute verb all see it. Your own variables belong in your shell's own lines.\n${environment:posix}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "systemd",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/environment.d/50-mesh.conf",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "# The operator account's environment for its service manager and graphical session, generated by\n# the mesh (module node-env, novox/hq ADR 0203). Do not edit: this file is replaced at every push.\n# The same facts as ~/.config/mesh/environment.sh, in environment.d(5) syntax.\n${environment:systemd}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"module": "nodered",
|
||||||
|
"version": "1",
|
||||||
|
"emits": [
|
||||||
|
"flows.deployed"
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"admin": {
|
||||||
|
"path": "${dir:mesh-state}/admin",
|
||||||
|
"taken": "at-start"
|
||||||
|
},
|
||||||
|
"api-token": {
|
||||||
|
"path": "${dir:mesh-state}/api-token",
|
||||||
|
"taken": "at-start"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 1880,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the flow editor and the endpoints flows expose"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"path": "${dir:data}",
|
||||||
|
"class": "valuable",
|
||||||
|
"why": "flows and their credentials"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "mesh-state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "1000:1000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "written",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings-code",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.js",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "1000:1000",
|
||||||
|
"content": "// Node-RED's settings, written by the mesh from the nodered module. What an assignment may change\n// is settings.json beside this file (merged key by key); the credentials are the mesh's secrets and\n// reach Node-RED only through this file. The flows' own credentials stay encrypted in the user\n// directory under the key Node-RED keeps there (.config.runtime.json), which is data, not this.\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst crypto = require(\"crypto\");\n\nconst ADMIN_PASSWORD = \"${secret:admin}\";\nconst API_TOKEN = \"${secret:api-token}\";\nconst ADMIN = { username: \"admin\", permissions: \"*\" };\n\nconst settings = JSON.parse(fs.readFileSync(path.join(__dirname, \"settings.json\"), \"utf8\"));\n// The mesh's keys, not Node-RED's: endpoints lands in every merged file; timeZone is the\n// assignment's way to set the zone flows schedule and format in; mqtt names the broker nodes the\n// module's MQTT step keeps pointed at the mesh's broker; topics is what nodered asks the broker for.\nif (settings.timeZone) process.env.TZ = settings.timeZone;\ndelete settings.timeZone;\ndelete settings.endpoints;\ndelete settings.mqtt;\ndelete settings.topics;\n\nfunction same(a, b) {\n const x = crypto.createHash(\"sha256\").update(String(a)).digest();\n const y = crypto.createHash(\"sha256\").update(String(b)).digest();\n return crypto.timingSafeEqual(x, y);\n}\n\n// The admin secret is a password, or, accepted from an existing install, the bcrypt hash its\n// settings held, so the password people already use keeps working.\nfunction passwordMatches(given) {\n if (/^\\$2[aby]\\$\\d\\d\\$/.test(ADMIN_PASSWORD)) return require(\"bcryptjs\").compare(String(given), ADMIN_PASSWORD);\n return Promise.resolve(same(given, ADMIN_PASSWORD));\n}\n\nmodule.exports = Object.assign(settings, {\n uiPort: 1880,\n adminAuth: {\n type: \"credentials\",\n users: (username) => Promise.resolve(username === ADMIN.username ? ADMIN : null),\n authenticate: (username, password) =>\n username === ADMIN.username\n ? passwordMatches(password).then((ok) => (ok ? ADMIN : null))\n : Promise.resolve(null),\n // The module's own tools call the admin API with this bearer token.\n tokens: (token) => Promise.resolve(same(token, API_TOKEN) ? { username: \"mesh\", permissions: \"*\" } : null),\n },\n});\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settings",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "1000:1000",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"flowFile\": \"flows.json\",\n \"flowFilePretty\": true,\n \"diagnostics\": { \"enabled\": true, \"ui\": true },\n \"runtimeState\": { \"enabled\": false, \"ui\": false },\n \"logging\": { \"console\": { \"level\": \"info\", \"metrics\": false, \"audit\": false } },\n \"exportGlobalContextKeys\": false,\n \"externalModules\": {},\n \"editorTheme\": { \"projects\": { \"enabled\": false } },\n \"functionExternalModules\": true,\n \"debugMaxLength\": 1000,\n \"mqttReconnectTime\": 15000,\n \"serialReconnectTime\": 15000\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "nodered",
|
||||||
|
"image": "nodered/node-red@sha256:a649dd711d55490151a2c39a8e48ad0c44325488fbc0e66315f2d2e19e5e1ace",
|
||||||
|
"health": {
|
||||||
|
"kind": "runtime"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"TZ": "Etc/UTC"
|
||||||
|
},
|
||||||
|
"ports": [
|
||||||
|
"1880"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"--settings",
|
||||||
|
"/data/settings.js"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:data}:/data",
|
||||||
|
"${dir:state}/settings.js:/data/settings.js:ro",
|
||||||
|
"${dir:state}/settings.json:/data/settings.json:ro"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"settings-code",
|
||||||
|
"settings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runtime-config",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/config.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "{\n \"token\": \"${secret:api-token}\"\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mqtt-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/mqtt.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "MESH_NODERED_URL=http://127.0.0.1:${port:1880}\nMESH_NODERED_CONFIG_FILE=${dir:mesh-state}/config.json\nMESH_PROVISIONS_DIR=${dir:state}\nMESH_WRITTEN_DIR=${dir:written}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mqtt",
|
||||||
|
"type": "process",
|
||||||
|
"name": "nodered-mqtt",
|
||||||
|
"artifact": "code",
|
||||||
|
"run": [
|
||||||
|
"node",
|
||||||
|
"mqtt/index.js"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/mqtt.env"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"mqtt-env",
|
||||||
|
"bound-mqtt-topic",
|
||||||
|
"secret-mqtt-topic",
|
||||||
|
"settings"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"mqtt-topic",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"mqtt-topic": {
|
||||||
|
"topics": [
|
||||||
|
"#"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"label": "nodered",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json",
|
||||||
|
"mqtt-topic": "${dir:state}/mqtt-topic.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"mqtt-topic": "${dir:state}/mqtt-topic.secret"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js",
|
||||||
|
"mqtt/index.js"
|
||||||
|
],
|
||||||
|
"loads": [
|
||||||
|
"tools/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_NODERED_URL": "http://127.0.0.1:${port:1880}",
|
||||||
|
"MESH_NODERED_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"module": "ollama",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"name": "model-access",
|
||||||
|
"scope": "node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"port": 11434,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "machine",
|
||||||
|
"why": "consumers on this machine reaching the local model server's OpenAI-compatible API"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serves": {
|
||||||
|
"model-access": {
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"own": [
|
||||||
|
{
|
||||||
|
"id": "models",
|
||||||
|
"path": "${dir:state}",
|
||||||
|
"class": "rebuildable",
|
||||||
|
"backup": "none",
|
||||||
|
"measure": "shallow",
|
||||||
|
"why": "models, downloaded again, and too large to copy every night"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/services/ollama",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "ollama",
|
||||||
|
"image": "ollama/ollama@sha256:32931b46719f673c05fdbaa81ccb26da18ea4a1c57590a754874ab28ba269eb2",
|
||||||
|
"network": "host",
|
||||||
|
"env": {
|
||||||
|
"OLLAMA_HOST": "0.0.0.0:11434"
|
||||||
|
},
|
||||||
|
"volumes": [
|
||||||
|
"/services/ollama:/root/.ollama"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user