Merge pull request 'mesh-builder: read the genesis document until the registry binds, and refuse a registry build with neither (hq issue 121, ADR 0336)' (#289) from fix/121-genesis-package-registry into main
This commit was merged in pull request #289.
This commit is contained in:
+132
-33
@@ -18,11 +18,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -54,6 +56,8 @@ is dialled except the broker.
|
|||||||
MESH_BINDING a file the mesh wrote saying where the artifact store is
|
MESH_BINDING a file the mesh wrote saying where the artifact store is
|
||||||
MESH_PACKAGE_BINDING a file the mesh wrote saying where the package registry is
|
MESH_PACKAGE_BINDING a file the mesh wrote saying where the package registry is
|
||||||
MESH_NPM_TOKEN_FILE a file the mesh sealed holding the token for it
|
MESH_NPM_TOKEN_FILE a file the mesh sealed holding the token for it
|
||||||
|
MESH_GENESIS_PACKAGE_REGISTRY the document the installer sealed at genesis, read while no
|
||||||
|
binding is present (novox/hq ADR 0336)
|
||||||
MESH_NPM_REGISTRY a package registry URL, when the mesh has not said (a person, the bootstrap)
|
MESH_NPM_REGISTRY a package registry URL, when the mesh has not said (a person, the bootstrap)
|
||||||
MESH_NPM_TOKEN the token for it, likewise
|
MESH_NPM_TOKEN the token for it, likewise
|
||||||
MESH_NPM_SCOPE the scope it answers for (default: @novox)
|
MESH_NPM_SCOPE the scope it answers for (default: @novox)
|
||||||
@@ -254,7 +258,10 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
|||||||
}
|
}
|
||||||
say("build", what)
|
say("build", what)
|
||||||
|
|
||||||
npmrc, err := packagesFrom()
|
npmrc, err := packagesFrom(on)
|
||||||
|
if err == nil {
|
||||||
|
say("packages", saysRegistry(npmrc))
|
||||||
|
}
|
||||||
var built builder.Result
|
var built builder.Result
|
||||||
if request.Check != nil {
|
if request.Check != nil {
|
||||||
// **A pull request's merge check, not a build** (novox/hq to-be 45 §9): nothing is built,
|
// **A pull request's merge check, not a build** (novox/hq to-be 45 §9): nothing is built,
|
||||||
@@ -386,26 +393,71 @@ func layerOf(l *builder.Layer) *link.CheckLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// packagesFrom is where a build resolves the mesh's own published packages — the SDK above all
|
// packagesFrom is where a build resolves the mesh's own published packages — the SDK above all
|
||||||
// (novox/hq ADR 0076, issue 053).
|
// (novox/hq ADR 0076, issue 053) — on this machine.
|
||||||
//
|
//
|
||||||
// Preferably from the mesh: a package-registry binding names the endpoint the way the artifact
|
// **The binding wins, then the genesis document** (novox/hq ADR 0336 §3, §4). The build agent's
|
||||||
// store's binding does, and a sealed token file the credential the way the broker's does. The
|
// npm-package-registry requirement is optional: on genesis's node, before the gitea module is assigned,
|
||||||
// environment variables remain for a builder run by a person, and for the bootstrap, where there is
|
// nothing provides it, and the installer sealed the forge it raised to this agent as one document
|
||||||
// no registry yet — there the result is disabled and a build that needs no mesh-published dependency
|
// instead. Once the requirement binds, the binding names the endpoint the way the artifact store's
|
||||||
// builds anyway.
|
// does and a sealed token file the credential, and the genesis document is left unread. A binding
|
||||||
func packagesFrom() (builder.Npmrc, error) {
|
// file named and absent is a requirement not bound here, never a fault: the environment names the
|
||||||
|
// path whether or not the mesh wrote it.
|
||||||
|
//
|
||||||
|
// The environment variables remain for a builder run by a person. With none of the three the result
|
||||||
|
// is disabled and says this machine, so a build that needs the registry is refused naming it (§6),
|
||||||
|
// and a build that needs none builds anyway.
|
||||||
|
func packagesFrom(machine string) (builder.Npmrc, error) {
|
||||||
scope := strings.TrimSpace(os.Getenv("MESH_NPM_SCOPE"))
|
scope := strings.TrimSpace(os.Getenv("MESH_NPM_SCOPE"))
|
||||||
if scope == "" {
|
if scope == "" {
|
||||||
scope = "@novox"
|
scope = "@novox"
|
||||||
}
|
}
|
||||||
|
none := builder.Npmrc{Machine: machine}
|
||||||
|
|
||||||
registry := strings.TrimSpace(os.Getenv("MESH_NPM_REGISTRY"))
|
|
||||||
var username string
|
|
||||||
if path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")); path != "" {
|
if path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")); path != "" {
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
switch {
|
||||||
return builder.Npmrc{}, fmt.Errorf("cannot read what the mesh said about the package registry: %w", err)
|
case errors.Is(err, os.ErrNotExist):
|
||||||
|
// Not bound on this machine: the genesis document, or nothing.
|
||||||
|
case err != nil:
|
||||||
|
return none, fmt.Errorf("cannot read what the mesh said about the package registry: %w", err)
|
||||||
|
default:
|
||||||
|
return fromBinding(path, raw, scope, machine)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if path := strings.TrimSpace(os.Getenv("MESH_GENESIS_PACKAGE_REGISTRY")); path != "" {
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, os.ErrNotExist):
|
||||||
|
// Not this mesh's genesis node, or the installer has not sealed it yet.
|
||||||
|
case err != nil:
|
||||||
|
return none, fmt.Errorf("cannot read the genesis document for the package registry: %w", err)
|
||||||
|
default:
|
||||||
|
return fromGenesis(path, raw, scope, machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A builder run by a person.
|
||||||
|
registry := strings.TrimSpace(os.Getenv("MESH_NPM_REGISTRY"))
|
||||||
|
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
||||||
|
username := strings.TrimSpace(os.Getenv("MESH_NPM_USER"))
|
||||||
|
if registry == "" && secret == "" {
|
||||||
|
return none, nil
|
||||||
|
}
|
||||||
|
n := builder.Npmrc{Scope: scope, Registry: registry, From: builder.FromEnvironment, Machine: machine}
|
||||||
|
if username != "" {
|
||||||
|
n.Username, n.Password = username, secret
|
||||||
|
} else {
|
||||||
|
n.Token = secret
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromBinding composes the registry from the npm-package-registry binding the mesh wrote and the
|
||||||
|
// credential it sealed beside it. The mesh authenticates the ordinary way — a generated password the
|
||||||
|
// provider only applies (novox/hq ADR 0048) — so with a username the secret is a password (basic
|
||||||
|
// auth); without one it is a bearer token a provider minted.
|
||||||
|
func fromBinding(path string, raw []byte, scope, machine string) (builder.Npmrc, error) {
|
||||||
|
none := builder.Npmrc{Machine: machine}
|
||||||
var told struct {
|
var told struct {
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
At string `json:"at"`
|
At string `json:"at"`
|
||||||
@@ -413,10 +465,10 @@ func packagesFrom() (builder.Npmrc, error) {
|
|||||||
Serves map[string]any `json:"serves"`
|
Serves map[string]any `json:"serves"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &told); err != nil {
|
if err := json.Unmarshal(raw, &told); err != nil {
|
||||||
return builder.Npmrc{}, fmt.Errorf("%s is not a binding: %w", path, err)
|
return none, fmt.Errorf("%s is not a binding: %w", path, err)
|
||||||
}
|
}
|
||||||
if told.At == "" {
|
if told.At == "" {
|
||||||
return builder.Npmrc{}, fmt.Errorf(
|
return none, fmt.Errorf(
|
||||||
"%s says the package registry is on %q and gives no address for it", path, told.From)
|
"%s says the package registry is on %q and gives no address for it", path, told.From)
|
||||||
}
|
}
|
||||||
// Composed from what the provider serves, so nothing here knows gitea's URL shape from
|
// Composed from what the provider serves, so nothing here knows gitea's URL shape from
|
||||||
@@ -427,38 +479,85 @@ func packagesFrom() (builder.Npmrc, error) {
|
|||||||
}
|
}
|
||||||
port, ok := told.Serves["port"]
|
port, ok := told.Serves["port"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return builder.Npmrc{}, fmt.Errorf("%s says nothing about which port the package registry answers on", path)
|
return none, fmt.Errorf("%s says nothing about which port the package registry answers on", path)
|
||||||
}
|
}
|
||||||
npmPath, ok := told.Serves["npm-path"]
|
npmPath, ok := told.Serves["npm-path"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return builder.Npmrc{}, fmt.Errorf("%s says nothing about the path the package registry answers on", path)
|
return none, fmt.Errorf("%s says nothing about the path the package registry answers on", path)
|
||||||
}
|
}
|
||||||
registry = fmt.Sprintf("%s://%s:%v%v", scheme, told.At, port, npmPath)
|
|
||||||
username = told.As
|
|
||||||
}
|
|
||||||
|
|
||||||
// The credential the mesh sealed to this machine. The mesh authenticates the ordinary way — a
|
|
||||||
// generated password the provider only applies (novox/hq ADR 0048) — so with a username this is
|
|
||||||
// a password (basic auth); without one it is a bearer token a provider minted.
|
|
||||||
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
||||||
if path := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); path != "" {
|
if file := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); file != "" {
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return builder.Npmrc{}, fmt.Errorf("cannot read this builder's package-registry credential: %w", err)
|
return none, fmt.Errorf("cannot read this builder's package-registry credential: %w", err)
|
||||||
}
|
}
|
||||||
secret = strings.TrimSpace(string(raw))
|
secret = strings.TrimSpace(string(raw))
|
||||||
}
|
}
|
||||||
if u := strings.TrimSpace(os.Getenv("MESH_NPM_USER")); u != "" {
|
n := builder.Npmrc{Scope: scope, Registry: fmt.Sprintf("%s://%s:%v%v", scheme, told.At, port, npmPath),
|
||||||
username = u
|
From: builder.FromBinding, Machine: machine}
|
||||||
|
if told.As != "" {
|
||||||
|
n.Username, n.Password = told.As, secret
|
||||||
|
} else {
|
||||||
|
n.Token = secret
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if registry == "" && secret == "" {
|
// GenesisRegistry is the genesis document (novox/hq ADR 0336 §3): what the installer sealed to the
|
||||||
return builder.Npmrc{}, nil
|
// build agent on its node at genesis's packages step, as the `genesis-package-registry` own-secret —
|
||||||
|
// the forge it raised, the port this node gave the package registry, its npm path, and the account it
|
||||||
|
// made with that account's password. mesh-host writes it; the field names are the two repositories'
|
||||||
|
// agreement.
|
||||||
|
type GenesisRegistry struct {
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
At string `json:"at"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
NpmPath string `json:"npm-path"`
|
||||||
|
As string `json:"as"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
if username != "" {
|
|
||||||
return builder.Npmrc{Scope: scope, Registry: registry, Username: username, Password: secret}, nil
|
// fromGenesis composes the registry from the genesis document.
|
||||||
|
func fromGenesis(path string, raw []byte, scope, machine string) (builder.Npmrc, error) {
|
||||||
|
none := builder.Npmrc{Machine: machine}
|
||||||
|
var g GenesisRegistry
|
||||||
|
if err := json.Unmarshal(raw, &g); err != nil {
|
||||||
|
return none, fmt.Errorf("%s is not the genesis document for the package registry: %w", path, err)
|
||||||
}
|
}
|
||||||
return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil
|
var missing []string
|
||||||
|
for name, value := range map[string]string{"at": g.At, "npm-path": g.NpmPath, "as": g.As, "password": g.Password} {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
missing = append(missing, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.Port <= 0 {
|
||||||
|
missing = append(missing, "port")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
sort.Strings(missing)
|
||||||
|
return none, fmt.Errorf("%s, the genesis document for the package registry, says nothing of %s",
|
||||||
|
path, strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
scheme := g.Scheme
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
return builder.Npmrc{Scope: scope, Registry: fmt.Sprintf("%s://%s:%d%s", scheme, g.At, g.Port, g.NpmPath),
|
||||||
|
Username: g.As, Password: g.Password, From: builder.FromGenesis, Machine: machine}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saysRegistry is the line each build's log carries about the package registry (novox/hq ADR 0336
|
||||||
|
// §4): which of the binding and the genesis document it resolved from, or that it has neither here.
|
||||||
|
func saysRegistry(n builder.Npmrc) string {
|
||||||
|
if n.Enabled() {
|
||||||
|
return "the package registry is resolved from " + n.From
|
||||||
|
}
|
||||||
|
machine := n.Machine
|
||||||
|
if machine == "" {
|
||||||
|
machine = "this machine"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("no package registry: %s is unbound on %s and no genesis document is here; "+
|
||||||
|
"a build needing it is refused", builder.RegistryRequirement, machine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// forgeFrom is the git credential this builder may offer a clone, composed from the same binding
|
// forgeFrom is the git credential this builder may offer a clone, composed from the same binding
|
||||||
|
|||||||
@@ -84,10 +84,15 @@ func buildOnce(ctx context.Context, args []string) error {
|
|||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
|
|
||||||
npmrc, err := packagesFrom()
|
machine := os.Getenv("MESH_NODE")
|
||||||
|
if machine == "" {
|
||||||
|
machine, _ = os.Hostname()
|
||||||
|
}
|
||||||
|
npmrc, err := packagesFrom(machine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, " [packages] %s\n", saysRegistry(npmrc))
|
||||||
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
||||||
forgeFrom(),
|
forgeFrom(),
|
||||||
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The build agent's package registry (novox/hq ADR 0336, issue 121): its npm-package-registry
|
||||||
|
// requirement is optional, so the binding may be absent, and on genesis's node the installer seals
|
||||||
|
// the forge it raised as one genesis document instead.
|
||||||
|
|
||||||
|
// registryFiles names the three paths the build agent's environment always names, writing only those
|
||||||
|
// given.
|
||||||
|
func registryFiles(t *testing.T, binding, token, genesis string) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
for env, content := range map[string]string{
|
||||||
|
"MESH_PACKAGE_BINDING": binding,
|
||||||
|
"MESH_NPM_TOKEN_FILE": token,
|
||||||
|
"MESH_GENESIS_PACKAGE_REGISTRY": genesis,
|
||||||
|
} {
|
||||||
|
path := filepath.Join(dir, env)
|
||||||
|
t.Setenv(env, path)
|
||||||
|
if content != "" {
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, env := range []string{"MESH_NPM_REGISTRY", "MESH_NPM_TOKEN", "MESH_NPM_USER", "MESH_NPM_SCOPE"} {
|
||||||
|
t.Setenv(env, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
aBinding = `{"from":"gitea","at":"anchor.mesh","as":"anchor-agent",
|
||||||
|
"serves":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/"}}`
|
||||||
|
aGenesisDocument = `{"scheme":"http","at":"127.0.0.1","port":3100,"npm-path":"/api/packages/novox/npm/",
|
||||||
|
"as":"mesh-builder","password":"genesis-secret"}`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUntilTheRequirementBindsTheGenesisDocumentIsRead(t *testing.T) {
|
||||||
|
registryFiles(t, "", "", aGenesisDocument)
|
||||||
|
n, err := packagesFrom("anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an absent binding beside a genesis document was refused: %v", err)
|
||||||
|
}
|
||||||
|
if n.From != builder.FromGenesis || n.Registry != "http://127.0.0.1:3100/api/packages/novox/npm/" ||
|
||||||
|
n.Username != "mesh-builder" || n.Password != "genesis-secret" || n.Scope != "@novox" {
|
||||||
|
t.Fatalf("the genesis document was not what the registry came from: %+v", n)
|
||||||
|
}
|
||||||
|
file, err := n.File()
|
||||||
|
if err != nil || !strings.Contains(file, "//127.0.0.1:3100/api/packages/novox/npm/:username=mesh-builder") {
|
||||||
|
t.Fatalf("the .npmrc is not written from the genesis document: %v\n%s", err, file)
|
||||||
|
}
|
||||||
|
if got := saysRegistry(n); !strings.Contains(got, "genesis document") {
|
||||||
|
t.Errorf("the log does not say the genesis document: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARealBindingWinsOverTheGenesisDocument(t *testing.T) {
|
||||||
|
registryFiles(t, aBinding, "bound-secret\n", aGenesisDocument)
|
||||||
|
n, err := packagesFrom("anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n.From != builder.FromBinding || n.Registry != "http://anchor.mesh:3000/api/packages/novox/npm/" ||
|
||||||
|
n.Username != "anchor-agent" || n.Password != "bound-secret" {
|
||||||
|
t.Fatalf("the binding did not win over the genesis document: %+v", n)
|
||||||
|
}
|
||||||
|
if got := saysRegistry(n); !strings.Contains(got, "binding") {
|
||||||
|
t.Errorf("the log does not say the binding: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithNeitherTheAgentHasNoRegistryAndSaysWhere(t *testing.T) {
|
||||||
|
registryFiles(t, "", "", "")
|
||||||
|
n, err := packagesFrom("workstation")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an unbound optional requirement was a fault: %v", err)
|
||||||
|
}
|
||||||
|
if n.Enabled() || n.Machine != "workstation" {
|
||||||
|
t.Fatalf("a registry came from nowhere: %+v", n)
|
||||||
|
}
|
||||||
|
got := saysRegistry(n)
|
||||||
|
for _, want := range []string{"npm-package-registry", "unbound", "workstation"} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("the log does not say %q: %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAGenesisDocumentMissingAFieldIsRefusedByName(t *testing.T) {
|
||||||
|
registryFiles(t, "", "", `{"at":"127.0.0.1","port":3000,"npm-path":"/api/packages/novox/npm/","as":"mesh-builder"}`)
|
||||||
|
if _, err := packagesFrom("anchor"); err == nil || !strings.Contains(err.Error(), "password") {
|
||||||
|
t.Fatalf("a genesis document with no password was taken: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -251,6 +251,20 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
|||||||
// never resolve a mesh package — making their image non-deterministic (a needless rollout every
|
// never resolve a mesh package — making their image non-deterministic (a needless rollout every
|
||||||
// build) and leaking the credential into a build stage. Absent entirely with no registry, which
|
// build) and leaking the credential into a build stage. Absent entirely with no registry, which
|
||||||
// is the bootstrap case (novox/hq ADR 0076).
|
// is the bootstrap case (novox/hq ADR 0076).
|
||||||
|
// **A build that needs the registry and has none is refused before anything is built or
|
||||||
|
// published** (novox/hq ADR 0336 §6) — before a base is copied into the store, and never built
|
||||||
|
// without the credential or against the public registry.
|
||||||
|
if !npmrc.Enabled() {
|
||||||
|
why, err := needsRegistry(manifest, within)
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, err
|
||||||
|
}
|
||||||
|
if why != "" {
|
||||||
|
err := npmrc.unbound(manifest.Module, why)
|
||||||
|
say("packages", "REFUSED: %v", err)
|
||||||
|
return Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var npmrcPath string
|
var npmrcPath string
|
||||||
if npmrc.Enabled() && manifest.Build != nil && wantsPackages(manifest, within) {
|
if npmrc.Enabled() && manifest.Build != nil && wantsPackages(manifest, within) {
|
||||||
content, err := npmrc.File()
|
content, err := npmrc.File()
|
||||||
@@ -263,7 +277,7 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
|||||||
}
|
}
|
||||||
// Only for as long as the build: a later check's container mounts this workspace (novox/hq issue 462).
|
// Only for as long as the build: a later check's container mounts this workspace (novox/hq issue 462).
|
||||||
defer os.Remove(npmrcPath)
|
defer os.Remove(npmrcPath)
|
||||||
say("packages", "resolving %s from the mesh's package registry", npmrc.Scope)
|
say("packages", "resolving %s from the mesh's package registry, from %s", npmrc.Scope, fromOr(npmrc.From))
|
||||||
src.notPinned("it resolves packages from the mesh's registry at build time")
|
src.notPinned("it resolves packages from the mesh's registry at build time")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +715,14 @@ func against(within string, manifest catalogue.Manifest, resolved []string) []st
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fromOr is where a registry was resolved from, as a build's log says it.
|
||||||
|
func fromOr(from string) string {
|
||||||
|
if from == "" {
|
||||||
|
return "what this build was handed"
|
||||||
|
}
|
||||||
|
return from
|
||||||
|
}
|
||||||
|
|
||||||
// ManifestName is the one file a module repository must have.
|
// ManifestName is the one file a module repository must have.
|
||||||
//
|
//
|
||||||
// At the root, and named the same in every repository. A convention somebody can look for beats a
|
// At the root, and named the same in every repository. A convention somebody can look for beats a
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package builder
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Npmrc is what a build needs to resolve the mesh's own shared library — and any other package the
|
// Npmrc is what a build needs to resolve the mesh's own shared library — and any other package the
|
||||||
@@ -37,8 +44,36 @@ type Npmrc struct {
|
|||||||
// username is the consumer's mesh identity. Ignored when Token is set.
|
// username is the consumer's mesh identity. Ignored when Token is set.
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
||||||
|
// From says where the build agent resolved this registry from (novox/hq ADR 0336 §4): its
|
||||||
|
// npm-package-registry binding, the genesis document the installer sealed to it, or the
|
||||||
|
// environment of a builder run by a person. Empty when it resolved none.
|
||||||
|
From string
|
||||||
|
// Machine is the machine the build agent runs on, named when a build that needs the registry is
|
||||||
|
// refused for having none (ADR 0336 §6).
|
||||||
|
Machine string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Where a build agent resolves the mesh's package registry from (novox/hq ADR 0336 §3, §4).
|
||||||
|
const (
|
||||||
|
// FromBinding is the npm-package-registry binding the mesh wrote, with the credential it sealed.
|
||||||
|
FromBinding = "the npm-package-registry binding"
|
||||||
|
// FromGenesis is the genesis document the installer sealed at genesis's packages step, read only
|
||||||
|
// while no binding is present.
|
||||||
|
FromGenesis = "the genesis document"
|
||||||
|
// FromEnvironment is the environment of a builder run by a person.
|
||||||
|
FromEnvironment = "the environment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegistryRequirement is the build agent's optional requirement the registry is bound through, and
|
||||||
|
// RegistryProvider the catalogue module that provides it (novox/hq ADR 0336 §1, §6): named when a
|
||||||
|
// build that needs the registry is refused, so the outcome says what to assign. The controller's
|
||||||
|
// catalogue test holds the provider to the catalogue's.
|
||||||
|
const (
|
||||||
|
RegistryRequirement = "npm-package-registry"
|
||||||
|
RegistryProvider = "gitea"
|
||||||
|
)
|
||||||
|
|
||||||
// Enabled reports whether there is a registry to resolve against at all. A bootstrap build that
|
// Enabled reports whether there is a registry to resolve against at all. A bootstrap build that
|
||||||
// runs before any package registry exists has none, and must still build whatever needs no
|
// runs before any package registry exists has none, and must still build whatever needs no
|
||||||
// mesh-published dependency.
|
// mesh-published dependency.
|
||||||
@@ -90,6 +125,77 @@ func (n Npmrc) File() (string, error) {
|
|||||||
return fmt.Sprintf("%s:registry=%s\n%s", scope, reg, auth), nil
|
return fmt.Sprintf("%s:registry=%s\n%s", scope, reg, auth), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needsRegistry says why this module's build needs the mesh's package registry, or "" when it needs
|
||||||
|
// none (novox/hq ADR 0336 §5): a `package` artifact, an image whose Dockerfile names .npmrc, or a
|
||||||
|
// TypeScript artifact with an `@novox/` dependency. Every Go artifact, every image that does not name
|
||||||
|
// .npmrc, and every image pulled upstream builds without it.
|
||||||
|
func needsRegistry(manifest catalogue.Manifest, within string) (string, error) {
|
||||||
|
if manifest.Build == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
for _, a := range manifest.Build.Artifacts {
|
||||||
|
switch {
|
||||||
|
case a.Kind == catalogue.ArtifactPackage:
|
||||||
|
return fmt.Sprintf("%s is a package, published to the registry", a.Name), nil
|
||||||
|
case a.Kind == catalogue.ArtifactImage:
|
||||||
|
raw, err := os.ReadFile(filepath.Join(within, a.From))
|
||||||
|
if err == nil && strings.Contains(string(raw), ".npmrc") {
|
||||||
|
return fmt.Sprintf("%s's Dockerfile names .npmrc", a.Name), nil
|
||||||
|
}
|
||||||
|
case a.Language == "typescript":
|
||||||
|
scoped, err := scopedDependencies(within)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(scoped) > 0 {
|
||||||
|
return fmt.Sprintf("%s is TypeScript and depends on %s", a.Name, strings.Join(scoped, ", ")), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scopedDependencies is every `@novox/` package the module's package.json depends on, the SDK
|
||||||
|
// included.
|
||||||
|
func scopedDependencies(tree string) ([]string, error) {
|
||||||
|
raw, err := os.ReadFile(filepath.Join(tree, "package.json"))
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return nil, fmt.Errorf("the module's package.json is not JSON: %w", err)
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for name := range p.Dependencies {
|
||||||
|
if strings.HasPrefix(name, "@novox/") {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unbound is the refusal of a build that needs the registry on a build agent that has none (novox/hq
|
||||||
|
// ADR 0336 §6): it names the machine, the requirement left unbound there and the module that provides
|
||||||
|
// it. Nothing is built, nothing is published, and the `@novox` scope is never resolved from the
|
||||||
|
// public registry instead.
|
||||||
|
func (n Npmrc) unbound(module, why string) error {
|
||||||
|
machine := n.Machine
|
||||||
|
if machine == "" {
|
||||||
|
machine = "this machine"
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s needs the mesh's package registry (%s), and %s is unbound on %s: the build "+
|
||||||
|
"agent there has neither its binding nor the genesis document. It is provided by the %s module; "+
|
||||||
|
"once %s is assigned the requirement binds (novox/hq ADR 0336 §6)",
|
||||||
|
module, why, RegistryRequirement, machine, RegistryProvider, RegistryProvider)
|
||||||
|
}
|
||||||
|
|
||||||
// packageRecipe is how a `package` artifact is built and published: on a PUBLIC base image, never
|
// packageRecipe is how a `package` artifact is built and published: on a PUBLIC base image, never
|
||||||
// the mesh toolchain, because the toolchain is built from the package this produces (the SDK). The
|
// the mesh toolchain, because the toolchain is built from the package this produces (the SDK). The
|
||||||
// script builds the module, then publishes it to the mesh's package registry unless that exact
|
// script builds the module, then publishes it to the mesh's package registry unless that exact
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package builder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/beside"
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A build that needs the registry fails loudly while the build agent has none** (novox/hq ADR 0336
|
||||||
|
// §5, §6, issue 121). On a holder with neither the npm-package-registry binding nor the genesis
|
||||||
|
// document, a `package` artifact, an image whose Dockerfile names .npmrc and a TypeScript artifact
|
||||||
|
// with an `@novox/` dependency each fail before anything is built or published, naming the machine,
|
||||||
|
// the unbound requirement and the module that provides it. A Go artifact, and an image that does not
|
||||||
|
// name .npmrc, build there as before.
|
||||||
|
func TestABuildNeedingTheRegistryIsRefusedOnAnAgentWithoutOne(t *testing.T) {
|
||||||
|
none := Npmrc{Machine: "workstation"}
|
||||||
|
cases := map[string]struct {
|
||||||
|
manifest string
|
||||||
|
files map[string]string
|
||||||
|
held map[string]string
|
||||||
|
}{
|
||||||
|
"a package": {aPackage, map[string]string{"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`}, nil},
|
||||||
|
"an image naming .npmrc": {withBoth, map[string]string{
|
||||||
|
"Dockerfile": "FROM scratch\nCOPY .npmrc /root/.npmrc\n", "files/x": "y"}, nil},
|
||||||
|
"a TypeScript bundle depending on the SDK": {aBundle, map[string]string{
|
||||||
|
"index.ts": "console.log(1)", "package.json": `{"dependencies":{"@novox/mesh-sdk":"^0.1.0"}}`},
|
||||||
|
map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}},
|
||||||
|
}
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
r, workspace := aRepository(t, c.manifest, c.files)
|
||||||
|
var said []string
|
||||||
|
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||||
|
"https://forge.invalid/m.git", "", "", workspace, c.held, none, GitCredential{},
|
||||||
|
func(step, message string) { said = append(said, step+": "+message) })
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("built with no package registry")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"workstation", "npm-package-registry", "unbound", "gitea"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range r.ran {
|
||||||
|
if strings.HasPrefix(line, "docker build") || strings.HasPrefix(line, "docker run") ||
|
||||||
|
strings.HasPrefix(line, "docker pull") {
|
||||||
|
t.Errorf("something was built after the refusal: %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(r.images) != 0 || len(r.archives) != 0 {
|
||||||
|
t.Errorf("something was published after the refusal: %v %v", r.images, r.archives)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(said, "\n"), "REFUSED") {
|
||||||
|
t.Errorf("the build's log does not say it was refused:\n%s", strings.Join(said, "\n"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Go artifact builds on the same agent.
|
||||||
|
files := aSharedRepository("one", "1")
|
||||||
|
manifest := `{"module":"ctl","version":"1","build":{"artifacts":[
|
||||||
|
{"name":"controller","kind":"bundle","language":"go","system":"arch","from":"cmd/ctl","binary":"ctl"}]},
|
||||||
|
"resources":[{"id":"controller","type":"process","name":"ctl","artifact":"controller","run":["./ctl"]}]}`
|
||||||
|
r := &recorded{contents: map[string]string{ManifestName: manifest}}
|
||||||
|
for k, v := range files {
|
||||||
|
r.contents[k] = v
|
||||||
|
}
|
||||||
|
held := map[string]string{"mesh-tools-go/build": "registry.invalid/mesh-tools-go/build@sha256:" + strings.Repeat("b", 64)}
|
||||||
|
if _, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
||||||
|
"https://forge.invalid/ctl.git", "", "", t.TempDir(), held, none, GitCredential{}, nil); err != nil {
|
||||||
|
t.Fatalf("a Go artifact did not build on an agent without the registry: %v", err)
|
||||||
|
}
|
||||||
|
// And an image that does not name .npmrc.
|
||||||
|
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||||
|
if _, err := Build(context.Background(), r.run, r,
|
||||||
|
"https://forge.invalid/meshboard.git", "", "", workspace, nil, none, GitCredential{}, nil); err != nil {
|
||||||
|
t.Fatalf("an image not naming .npmrc did not build on an agent without the registry: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **The build's log says which registry it resolved from** (novox/hq ADR 0336 §4).
|
||||||
|
func TestABuildSaysWhichRegistryItResolvedFrom(t *testing.T) {
|
||||||
|
for _, from := range []string{FromBinding, FromGenesis} {
|
||||||
|
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||||
|
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||||
|
})
|
||||||
|
n := Npmrc{Scope: "@novox", Registry: "http://127.0.0.1:3000/api/packages/novox/npm/",
|
||||||
|
Username: "mesh-builder", Password: "p", From: from, Machine: "anchor"}
|
||||||
|
var said []string
|
||||||
|
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/mesh-sdk.git", "", "",
|
||||||
|
workspace, nil, n, GitCredential{}, func(step, message string) { said = append(said, step+": "+message) }); err != nil {
|
||||||
|
t.Fatalf("%s: %v", from, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(said, "\n"), "from "+from) {
|
||||||
|
t.Errorf("the log does not say the registry came from %s:\n%s", from, strings.Join(said, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The refusal names the module that provides the registry; it is the catalogue's provider.
|
||||||
|
func TestTheRegistryABuildIsRefusedForNamesItsProvider(t *testing.T) {
|
||||||
|
raw, err := os.ReadFile(filepath.Join(beside.Catalogue(t), RegistryProvider, "module.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
forge, err := catalogue.ParseManifest(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
provides := false
|
||||||
|
for _, p := range forge.Provides {
|
||||||
|
provides = provides || p.Name == RegistryRequirement
|
||||||
|
}
|
||||||
|
if !provides {
|
||||||
|
t.Fatalf("%s does not provide %s, which a refused build names it for", RegistryProvider, RegistryRequirement)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Genesis assigns the build agent before the package registry exists (novox/hq ADR 0336, issue 121),
|
||||||
|
// judged over the real catalogue. Genesis's step 13 was refused with "nothing provides
|
||||||
|
// \"npm-package-registry\", wanted by build-agent": the forge genesis raises at its packages step is a
|
||||||
|
// container, not the gitea module, and provides nothing.
|
||||||
|
|
||||||
|
// agentBuilt is the build agent as the mesh holds it once built: its image pinned.
|
||||||
|
func agentBuilt(t *testing.T) Manifest {
|
||||||
|
t.Helper()
|
||||||
|
agent := catalogueManifest(t, "build-agent")
|
||||||
|
built, err := agent.Resolve([]Built{{Name: "server", Kind: ArtifactImage,
|
||||||
|
Reference: "mesh-builder@sha256:" + strings.Repeat("a", 64)}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("the build agent does not resolve against its own build: %v", err)
|
||||||
|
}
|
||||||
|
return built
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenesisAssignsTheBuildAgentWithNothingProvidingTheRegistry(t *testing.T) {
|
||||||
|
agent := agentBuilt(t)
|
||||||
|
if !agent.IsOptional("npm-package-registry") {
|
||||||
|
t.Fatalf("the build agent's npm-package-registry is hard (ADR 0336 §1): %v", agent.Requires)
|
||||||
|
}
|
||||||
|
if agent.IsOptional("artifact-store") {
|
||||||
|
t.Fatal("the build agent's artifact-store is optional; it stays hard")
|
||||||
|
}
|
||||||
|
store := map[string][]Provider{"artifact-store": {
|
||||||
|
{Node: "anchor", At: "anchor.internal", Module: "distribution", Serves: map[string]any{"port": float64(5000)}}}}
|
||||||
|
books := shelf(agent, catalogueManifest(t, "gitea"), catalogueManifest(t, "distribution"))
|
||||||
|
|
||||||
|
// Genesis's node at step 13: the store answers, nothing provides the registry.
|
||||||
|
got, err := Resolve(books, []string{"build-agent"}, reachable(), World{Offered: store})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assigning the build agent with no registry was refused: %v", err)
|
||||||
|
}
|
||||||
|
if have := strings.Join(names(got), " "); have != "build-agent" {
|
||||||
|
t.Fatalf("assigning the build agent put %s on the machine; nothing is installed for the registry", have)
|
||||||
|
}
|
||||||
|
if len(got.Absent) != 1 || got.Absent[0] != (Absent{Consumer: "build-agent", Requirement: "npm-package-registry"}) {
|
||||||
|
t.Fatalf("the registry is not listed unbound for the build agent: %+v", got.Absent)
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{ArtifactStore: "anchor.internal:5000",
|
||||||
|
Needed: map[string]map[string]string{"build-agent": {"broker": "sealed-broker"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("the build agent does not compose with the registry unbound: %v", err)
|
||||||
|
}
|
||||||
|
for _, id := range []string{"build-agent." + BoundID("npm-package-registry"), "build-agent." + SecretID("npm-package-registry"),
|
||||||
|
"build-agent." + NeedID("genesis-package-registry")} {
|
||||||
|
if fileNamed(out, id) != nil {
|
||||||
|
t.Errorf("%s was delivered with the registry unbound and no genesis document given", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The genesis document, once the installer seals it at the packages step (ADR 0336 §3).
|
||||||
|
out, err = got.Declaration(Rendering{ArtifactStore: "anchor.internal:5000",
|
||||||
|
Needed: map[string]map[string]string{"build-agent": {"broker": "sealed-broker", "genesis-package-registry": "sealed-genesis"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
doc := fileNamed(out, "build-agent."+NeedID("genesis-package-registry"))
|
||||||
|
if doc == nil || doc["sealed"] != "sealed-genesis" || !strings.HasSuffix(doc["path"].(string), "/genesis-package-registry.json") {
|
||||||
|
t.Fatalf("the genesis document is not delivered where the agent reads it: %v", doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gitea module assigned: the requirement binds, with no act on the build agent (ADR 0336 §2).
|
||||||
|
registry := map[string][]Provider{"artifact-store": store["artifact-store"], "npm-package-registry": {
|
||||||
|
{Node: "anchor", At: "anchor.internal", Module: "gitea",
|
||||||
|
Serves: map[string]any{"port": float64(3000), "scheme": "http", "npm-path": "/api/packages/novox/npm/"}}}}
|
||||||
|
bound, err := Resolve(books, []string{"build-agent"}, reachable(), World{Offered: registry})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n := boundFrom(t, bound, "build-agent", "npm-package-registry"); n.Module != "gitea" {
|
||||||
|
t.Fatalf("bound to %s", n.Module)
|
||||||
|
}
|
||||||
|
if len(bound.Absent) != 0 {
|
||||||
|
t.Fatalf("the registry is still listed unbound once gitea is assigned: %+v", bound.Absent)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ The copy carries the catalogue's private details: domains, first names in module
|
|||||||
private addresses. That is acceptable while mesh-controller is private; if it ever goes public, this copy is
|
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.
|
a second place to clean besides the catalogue itself.
|
||||||
|
|
||||||
mesh-catalog b9de001833b1b61b297182b8e3bcdae1cbdeace9 modules/*/module.json, modules/nats/Dockerfile
|
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
||||||
mesh-host 7a41e120635b3f7b6cb94cca8b3fc4093e4e6110 examples/foundation-first-node.lock
|
mesh-host 7a41e120635b3f7b6cb94cca8b3fc4093e4e6110 examples/foundation-first-node.lock
|
||||||
mesh-sdk 33917f91ac3cc213b879b32f3c70e2aa70395f98 conformance/events/module-event.json
|
mesh-sdk 33917f91ac3cc213b879b32f3c70e2aa70395f98 conformance/events/module-event.json
|
||||||
|
|
||||||
|
|||||||
+28
-2
@@ -3,7 +3,9 @@
|
|||||||
"version": "1",
|
"version": "1",
|
||||||
"invokes": [
|
"invokes": [
|
||||||
"seat:mesh-controller.artifacts",
|
"seat:mesh-controller.artifacts",
|
||||||
"seat:mesh-controller.collect"
|
"seat:mesh-controller.collect",
|
||||||
|
"mesh-catalog.catalog_modules",
|
||||||
|
"mesh-catalog.catalog_module"
|
||||||
],
|
],
|
||||||
"tools": [
|
"tools": [
|
||||||
"artifact_store_repositories",
|
"artifact_store_repositories",
|
||||||
@@ -11,6 +13,29 @@
|
|||||||
"artifact_store_references",
|
"artifact_store_references",
|
||||||
"artifact_store_collect"
|
"artifact_store_collect"
|
||||||
],
|
],
|
||||||
|
"settings": {
|
||||||
|
"older-store-names": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "none",
|
||||||
|
"why": "the names the artifact store had before, as a definition may still pin an image under them: host names or host:port, separated by spaces; none when it had no other. artifact_store_references reads every definition under these as well as under the store's current name, so an image a definition pins under one is said to be named by a definition (novox/hq issue 323)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store-names",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/store-names",
|
||||||
|
"mode": "0600",
|
||||||
|
"trusted": false,
|
||||||
|
"content": "# The names the artifact store had before, from the setting older-store-names, written by the mesh.\n# artifact_store_references reads every definition under these too (novox/hq issue 323).\n# Replaced on every push; change the setting, never this file.\nolder=${setting:older-store-names}\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
"build": {
|
"build": {
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
{
|
{
|
||||||
@@ -24,7 +49,8 @@
|
|||||||
"artifact-store-tools"
|
"artifact-store-tools"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry"
|
"MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry",
|
||||||
|
"MESH_ARTIFACT_STORE_NAMES_FILE": "${dir:state}/store-names"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
{
|
{
|
||||||
"module": "betterbird",
|
"module": "betterbird",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"upgrade": {
|
||||||
|
"policy": "record",
|
||||||
|
"why": "a new build may remove the community package betterbird-bin and replaces the installation in /opt/betterbird, which a running Betterbird has open: the switch on each node and every upgrade wait for the operator, who quits Betterbird first and then pushes (novox/hq ADR 0316 §4, §5)"
|
||||||
|
},
|
||||||
"requires": [
|
"requires": [
|
||||||
"x11-display"
|
"x11-display"
|
||||||
],
|
],
|
||||||
"settings": {
|
"invokes": [
|
||||||
"prefs": {
|
"seat:mesh-controller.settings",
|
||||||
"kind": "preference",
|
"seat:mesh-controller.nodes",
|
||||||
"default": "// none",
|
"seat:mesh-controller.node",
|
||||||
"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)"
|
"betterbird.betterbird_derive"
|
||||||
}
|
],
|
||||||
},
|
|
||||||
"tools": [
|
"tools": [
|
||||||
"betterbird_status",
|
"betterbird_status",
|
||||||
"betterbird_prefs",
|
"betterbird_prefs",
|
||||||
|
"betterbird_config",
|
||||||
|
"betterbird_config_apply",
|
||||||
"betterbird_user_js",
|
"betterbird_user_js",
|
||||||
"betterbird_log",
|
"betterbird_log",
|
||||||
"betterbird_restart",
|
"betterbird_restart",
|
||||||
"betterbird_check",
|
"betterbird_check",
|
||||||
|
"betterbird_accounts",
|
||||||
|
"betterbird_identities",
|
||||||
|
"betterbird_calendars",
|
||||||
|
"betterbird_address_books",
|
||||||
|
"betterbird_filters",
|
||||||
|
"betterbird_tags",
|
||||||
|
"betterbird_derive",
|
||||||
|
"betterbird_drift",
|
||||||
"betterbird_reminder_test"
|
"betterbird_reminder_test"
|
||||||
],
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
@@ -29,12 +42,109 @@
|
|||||||
"mode": "0755"
|
"mode": "0755"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "user-js",
|
"id": "prefs",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${machine:account-home}/.config/betterbird/user.js",
|
"path": "${machine:account-home}/.config/betterbird/prefs.json",
|
||||||
"owner": "${machine:account}",
|
"owner": "${machine:account}",
|
||||||
"mode": "0644",
|
"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"
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"prefs"
|
||||||
|
],
|
||||||
|
"content": "{\n \"prefs\": {}\n}\n",
|
||||||
|
"trusted": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "accounts",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/betterbird/accounts.json",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"accounts"
|
||||||
|
],
|
||||||
|
"content": "{\n \"accounts\": {}\n}\n",
|
||||||
|
"trusted": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "configuration",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/betterbird/configuration.json",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"identities",
|
||||||
|
"calendars",
|
||||||
|
"address-books",
|
||||||
|
"filters"
|
||||||
|
],
|
||||||
|
"content": "{\n \"identities\": {},\n \"calendars\": {},\n \"address-books\": {},\n \"filters\": {}\n}\n",
|
||||||
|
"trusted": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tags",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${machine:account-home}/.config/betterbird/tags.json",
|
||||||
|
"owner": "${machine:account}",
|
||||||
|
"mode": "0644",
|
||||||
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"tags"
|
||||||
|
],
|
||||||
|
"content": "{\n \"tags\": {}\n}\n",
|
||||||
|
"trusted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "community-package",
|
||||||
|
"type": "package",
|
||||||
|
"package": "betterbird-bin",
|
||||||
|
"absent": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "installation",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/opt/betterbird",
|
||||||
|
"artifact": "archive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "owl",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/opt/betterbird/owl",
|
||||||
|
"artifact": "owl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "policies",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/opt/betterbird/distribution",
|
||||||
|
"artifact": "distribution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spelling",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/opt/betterbird/defaults/pref",
|
||||||
|
"artifact": "defaults"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "preparation",
|
||||||
|
"type": "archive",
|
||||||
|
"path": "/usr/local/lib/mesh-betterbird",
|
||||||
|
"artifact": "tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "start",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/usr/local/bin/betterbird",
|
||||||
|
"mode": "0755",
|
||||||
|
"content": "#!/bin/sh\n# Betterbird's start, written by the mesh (module betterbird, novox/hq ADR 0316). Replaced at every push.\n#\n# Before Betterbird starts, the module's tools place its user.js in the profile (the settings prefs, accounts, identities, calendars,\n# address-books and tags) and write what Betterbird rewrites itself: the account lists and each declared account's filters. They do\n# it only while no Betterbird of this account runs, because Betterbird rewrites its saved preferences when it quits; a\n# second start while one runs only hands over to it. What the preparation could not do it says here, and\n# betterbird_check says it too. Betterbird starts either way: its configuration never holds the mail back.\n/usr/local/lib/mesh-betterbird/betterbird-tools prepare || echo \"betterbird: the mesh's preparation did not complete; betterbird_check says why\" >&2\nexec /opt/betterbird/betterbird \"$@\"\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "launcher",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/usr/local/share/applications/eu.betterbird.Betterbird.desktop",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Betterbird\nGenericName=Mail Client\nComment=Send and receive mail with Betterbird\nExec=/usr/local/bin/betterbird %u\nIcon=/opt/betterbird/chrome/icons/default/default128.png\nTerminal=false\nCategories=Network;Email;\nMimeType=message/rfc822;x-scheme-handler/mailto;application/x-xpinstall;application/x-extension-ics;text/calendar;text/vcard;text/x-vcard;x-scheme-handler/webcal;x-scheme-handler/webcals;x-scheme-handler/mid;\nStartupNotify=true\nStartupWMClass=eu.betterbird.Betterbird\nKeywords=Email;E-mail;Calendar;Thunderbird;\nActions=ComposeMessage;OpenAddressBook;\n# Written by the mesh (module betterbird, novox/hq ADR 0316 §3), in place of the community package's entry:\n# it starts Betterbird through the module's start, and names the icon in the installation, with no link.\n\n[Desktop Action ComposeMessage]\nName=Write new message\nExec=/usr/local/bin/betterbird -compose\n\n[Desktop Action OpenAddressBook]\nName=Open address book\nExec=/usr/local/bin/betterbird -addressbook\n"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
@@ -49,6 +159,38 @@
|
|||||||
"loads": [
|
"loads": [
|
||||||
"betterbird-tools"
|
"betterbird-tools"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "archive",
|
||||||
|
"kind": "archive",
|
||||||
|
"url": "https://www.betterbird.eu/downloads/LinuxArchive/betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz",
|
||||||
|
"sha256": "38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2",
|
||||||
|
"version": "153.4.0esr-bb10",
|
||||||
|
"within": "betterbird",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"www.betterbird.eu": "Betterbird's project publishes its Linux release there; the build seat fetches it once, by its pinned sha256 (novox/hq ADR 0316 §2)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "owl",
|
||||||
|
"kind": "archive",
|
||||||
|
"url": "https://www.beonex.com/owl/owl.xpi",
|
||||||
|
"sha256": "a54cc3d52113b34f97942ee19190947dee23561e2217f18db3d80ac023991221",
|
||||||
|
"version": "1.5.6.1",
|
||||||
|
"file": "owl.xpi",
|
||||||
|
"names-on-purpose": {
|
||||||
|
"www.beonex.com": "Owl's vendor publishes its xpi there; the build seat fetches it once, by its pinned sha256 (novox/hq ADR 0316 §6)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "distribution",
|
||||||
|
"kind": "archive",
|
||||||
|
"from": "install/distribution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "defaults",
|
||||||
|
"kind": "archive",
|
||||||
|
"from": "install/defaults-pref"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
],
|
],
|
||||||
"requires": [
|
"requires": [
|
||||||
"artifact-store",
|
"artifact-store",
|
||||||
"npm-package-registry"
|
{
|
||||||
|
"name": "npm-package-registry",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"binds": {
|
"binds": {
|
||||||
"npm-package-registry": "${dir:mesh-state}/package-registry.json"
|
"npm-package-registry": "${dir:mesh-state}/package-registry.json"
|
||||||
@@ -23,7 +26,11 @@
|
|||||||
"npm-package-registry": "${dir:mesh-state}/package-registry.secret"
|
"npm-package-registry": "${dir:mesh-state}/package-registry.secret"
|
||||||
},
|
},
|
||||||
"own-secrets": {
|
"own-secrets": {
|
||||||
"broker": "${dir:mesh-state}/broker"
|
"broker": "${dir:mesh-state}/broker",
|
||||||
|
"genesis-package-registry": {
|
||||||
|
"path": "${dir:mesh-state}/genesis-package-registry.json",
|
||||||
|
"issued-by": "outside"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"own": [
|
"own": [
|
||||||
@@ -52,7 +59,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:mesh-state}/build-agent.env",
|
"path": "${dir:mesh-state}/build-agent.env",
|
||||||
"mode": "0600",
|
"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"
|
"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_GENESIS_PACKAGE_REGISTRY=/run/mesh/genesis-package-registry.json\nMESH_WORKSPACE=${dir:workspace}\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "server",
|
"id": "server",
|
||||||
@@ -70,7 +77,15 @@
|
|||||||
"restart-on": [
|
"restart-on": [
|
||||||
"agent-env"
|
"agent-env"
|
||||||
],
|
],
|
||||||
"network": "host"
|
"network": "host",
|
||||||
|
"health": {
|
||||||
|
"kind": "exec",
|
||||||
|
"command": "/usr/local/bin/mesh-builder ready",
|
||||||
|
"interval": "10s",
|
||||||
|
"timeout": "5s",
|
||||||
|
"looks": 3,
|
||||||
|
"grace": "60s"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"servers",
|
"servers",
|
||||||
"holdings",
|
"holdings",
|
||||||
"config",
|
"config",
|
||||||
"proposals"
|
"proposals",
|
||||||
|
"questions"
|
||||||
],
|
],
|
||||||
"reads": [
|
"reads": [
|
||||||
"claude-licence-manager.bindings"
|
"claude-licence-manager.bindings"
|
||||||
@@ -54,6 +55,8 @@
|
|||||||
"claude_code_instructions_unregister",
|
"claude_code_instructions_unregister",
|
||||||
"claude_code_instructions_propose",
|
"claude_code_instructions_propose",
|
||||||
"claude_code_proposals",
|
"claude_code_proposals",
|
||||||
|
"claude_code_ask",
|
||||||
|
"claude_code_answer",
|
||||||
"claude_code_settings_get",
|
"claude_code_settings_get",
|
||||||
"claude_code_settings_set",
|
"claude_code_settings_set",
|
||||||
"claude_code_settings_clear",
|
"claude_code_settings_clear",
|
||||||
@@ -67,7 +70,8 @@
|
|||||||
"claude_code_home_remove",
|
"claude_code_home_remove",
|
||||||
"claude_code_home_removed",
|
"claude_code_home_removed",
|
||||||
"claude_code_home_restore",
|
"claude_code_home_restore",
|
||||||
"claude_code_judge"
|
"claude_code_judge",
|
||||||
|
"claude_code_judged"
|
||||||
],
|
],
|
||||||
"data": {
|
"data": {
|
||||||
"own": [
|
"own": [
|
||||||
|
|||||||
@@ -44,6 +44,18 @@
|
|||||||
"docker exec cat /etc/resolv.conf"
|
"docker exec cat /etc/resolv.conf"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"log-max-size": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "100m",
|
||||||
|
"why": "each container's log is cut at this size and the runtime keeps log-max-file of them, so 100m and 10 hold about 1 GB per container: what the one machine with rotation set by hand has (novox/hq issue 452). A whole number from 1 with k, m or g after it, refused before daemon.json is written. The runtime reads it only at its next start, which the mesh never forces, and a container created before that keeps the options it was created with until it is created again"
|
||||||
|
},
|
||||||
|
"log-max-file": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 10,
|
||||||
|
"why": "how many of a container's log files of log-max-size the runtime keeps, the oldest dropped first; a count from 1 to 99, refused before daemon.json is written, taken at the runtime's next start and by containers created after it, like log-max-size (novox/hq issue 452)"
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "package",
|
"id": "package",
|
||||||
@@ -62,13 +74,45 @@
|
|||||||
"state": "running",
|
"state": "running",
|
||||||
"boot": "enabled"
|
"boot": "enabled"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log-rotation",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/log-rotation.conf",
|
||||||
|
"mode": "0600",
|
||||||
|
"trusted": true,
|
||||||
|
"content": "# The log rotation daemon.json takes, written by the mesh from this machine's settings log-max-size and\n# log-max-file, and judged by the step log-rotation-check before daemon.json is written (novox/hq issue 452).\n# Replaced on every push; change the settings, never this file.\nsize=${setting:log-max-size}\nfiles=${setting:log-max-file}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log-rotation-check",
|
||||||
|
"type": "process",
|
||||||
|
"name": "docker-log-rotation-check",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./docker-tools",
|
||||||
|
"check-log-rotation"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"DOCKER_LOG_ROTATION": "${dir:state}/log-rotation.conf"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"log-rotation"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "daemon",
|
"id": "daemon",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "/etc/docker/daemon.json",
|
"path": "/etc/docker/daemon.json",
|
||||||
"mode": "0644",
|
"mode": "0644",
|
||||||
"into": "json",
|
"into": "json",
|
||||||
"content": "{\"live-restore\": true, \"insecure-registries\": [\"${seat:mesh-artifact-store:reach}\"]}\n"
|
"trusted": true,
|
||||||
|
"content": "{\"live-restore\": true, \"insecure-registries\": [\"${seat:mesh-artifact-store:reach}\"], \"log-driver\": \"json-file\", \"log-opts\": {\"max-size\": \"${setting:log-max-size}\", \"max-file\": \"${setting:log-max-file}\"}}\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "runtime",
|
"id": "runtime",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"pull.closed"
|
"pull.closed"
|
||||||
],
|
],
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"mesh-controller.checked"
|
"mesh-controller.checked",
|
||||||
|
"node-build-agent.started"
|
||||||
],
|
],
|
||||||
"listens": [
|
"listens": [
|
||||||
{
|
{
|
||||||
@@ -256,6 +257,22 @@
|
|||||||
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
|
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
|
||||||
"MESH_NPM_OWNER": "novox"
|
"MESH_NPM_OWNER": "novox"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forge",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/forge",
|
||||||
|
"binary": "forge",
|
||||||
|
"loads": [
|
||||||
|
"forge"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,20 +71,36 @@
|
|||||||
"mode": "0644",
|
"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"
|
"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-datasources",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${dir:state}/datasources",
|
||||||
|
"mode": "0755",
|
||||||
|
"owner": "472:472"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "influxdb-credential",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "${dir:state}/influxdb",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "472:472"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "influxdb-secret",
|
"id": "influxdb-secret",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/influxdb-api.secret",
|
"path": "${dir:influxdb-credential}/influxdb-api",
|
||||||
"mode": "0400",
|
"mode": "0400",
|
||||||
"owner": "472:472",
|
"owner": "472:472",
|
||||||
"content": "${secret:influxdb-api}"
|
"content": "${secret:influxdb-api}",
|
||||||
|
"only-when-bound": "influxdb-api"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "influxdb-datasource",
|
"id": "influxdb-datasource",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/datasource-influxdb.yaml",
|
"path": "${dir:influxdb-datasources}/mesh-influxdb.yaml",
|
||||||
"mode": "0644",
|
"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"
|
"content": "apiVersion: 1\n# Written by the mesh from grafana's influxdb-api binding, only while it is bound (novox/hq ADR 0330):\n# without influxdb, grafana runs with no mesh data source. 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/influxdb-api}\n",
|
||||||
|
"only-when-bound": "influxdb-api"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "server",
|
"id": "server",
|
||||||
@@ -103,8 +119,8 @@
|
|||||||
"${dir:data}:/var/lib/grafana",
|
"${dir:data}:/var/lib/grafana",
|
||||||
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
||||||
"${dir:state}/oidc-client.secret:/run/secrets/oidc-client:ro",
|
"${dir:state}/oidc-client.secret:/run/secrets/oidc-client:ro",
|
||||||
"${dir:state}/influxdb-api.secret:/run/secrets/influxdb-api:ro",
|
"${dir:influxdb-credential}:/run/secrets/influxdb:ro",
|
||||||
"${dir:state}/datasource-influxdb.yaml:/etc/grafana/provisioning/datasources/mesh-influxdb.yaml:ro"
|
"${dir:influxdb-datasources}:/etc/grafana/provisioning/datasources:ro"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"GF_SECURITY_ADMIN_PASSWORD__FILE": "/run/secrets/admin"
|
"GF_SECURITY_ADMIN_PASSWORD__FILE": "/run/secrets/admin"
|
||||||
@@ -131,7 +147,10 @@
|
|||||||
"requires": [
|
"requires": [
|
||||||
"route",
|
"route",
|
||||||
"oidc-client",
|
"oidc-client",
|
||||||
"influxdb-api"
|
{
|
||||||
|
"name": "influxdb-api",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"route": {
|
"route": {
|
||||||
|
|||||||
@@ -123,11 +123,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requires": [
|
"requires": [
|
||||||
"lidarr-api",
|
{
|
||||||
"mqtt-topic",
|
"name": "lidarr-api",
|
||||||
"radarr-api",
|
"optional": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mqtt-topic",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "radarr-api",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"route",
|
"route",
|
||||||
"sonarr-api"
|
{
|
||||||
|
"name": "sonarr-api",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"mqtt-topic": {
|
"mqtt-topic": {
|
||||||
|
|||||||
@@ -162,7 +162,13 @@
|
|||||||
"path": "${dir:mesh-state}/config.json",
|
"path": "${dir:mesh-state}/config.json",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "{}\n",
|
"content": "{}\n",
|
||||||
"merge": "json"
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"url",
|
||||||
|
"user",
|
||||||
|
"realm",
|
||||||
|
"issuer"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,186 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-knowledge",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "knowledge",
|
||||||
|
"requires": [
|
||||||
|
"postgres-database"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"postgres-database": {
|
||||||
|
"name": "knowledge"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"postgres-database": "${dir:state}/database.json"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"postgres-database": "${dir:state}/database.secret"
|
||||||
|
},
|
||||||
|
"seats": [
|
||||||
|
{
|
||||||
|
"name": "knowledge-base",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"ask",
|
||||||
|
"get",
|
||||||
|
"holds",
|
||||||
|
"links",
|
||||||
|
"history",
|
||||||
|
"reserve",
|
||||||
|
"propose",
|
||||||
|
"import",
|
||||||
|
"return"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "knowledge-base",
|
||||||
|
"scope": "mesh",
|
||||||
|
"serves": [
|
||||||
|
"ask",
|
||||||
|
"get",
|
||||||
|
"holds",
|
||||||
|
"links",
|
||||||
|
"history",
|
||||||
|
"reserve",
|
||||||
|
"propose",
|
||||||
|
"import",
|
||||||
|
"return"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
"knowledge_health",
|
||||||
|
"knowledge_highest"
|
||||||
|
],
|
||||||
|
"emits": [
|
||||||
|
"reserved",
|
||||||
|
"proposed",
|
||||||
|
"reviewed",
|
||||||
|
"accepted",
|
||||||
|
"changed",
|
||||||
|
"imported",
|
||||||
|
"drafted",
|
||||||
|
"confirmed",
|
||||||
|
"returned"
|
||||||
|
],
|
||||||
|
"uses": [
|
||||||
|
"operator-channel"
|
||||||
|
],
|
||||||
|
"invokes": [
|
||||||
|
"seat:issue-tracker.reserve",
|
||||||
|
"claude-code.claude_code_judge",
|
||||||
|
"claude-code.claude_code_judged",
|
||||||
|
"records.records_status",
|
||||||
|
"records.records_list",
|
||||||
|
"records.records_read",
|
||||||
|
"seat:operator-channel.notify"
|
||||||
|
],
|
||||||
|
"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",
|
||||||
|
"trusted": false,
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/settings.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"merge": "json",
|
||||||
|
"content": "{\n \"judge-model\": \"sonnet\",\n \"drafting\": \"on\",\n \"reserved-for\": \"168h\"\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prepare",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-knowledge-prepare",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./mesh-knowledge",
|
||||||
|
"prepare"
|
||||||
|
],
|
||||||
|
"run-once": true,
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "keep",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-knowledge-keep",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./mesh-knowledge",
|
||||||
|
"keep"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "watch",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-knowledge-watch",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./mesh-knowledge",
|
||||||
|
"watch"
|
||||||
|
],
|
||||||
|
"user": "${machine:account}",
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url",
|
||||||
|
"MESH_KNOWLEDGE_STATUS": "${dir:state}/health.json"
|
||||||
|
},
|
||||||
|
"restart-on": [
|
||||||
|
"database-url"
|
||||||
|
],
|
||||||
|
"health": {
|
||||||
|
"kind": "tool",
|
||||||
|
"tool": "knowledge_health",
|
||||||
|
"interval": "60s",
|
||||||
|
"timeout": "10s",
|
||||||
|
"looks": 2,
|
||||||
|
"grace": "180s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-knowledge",
|
||||||
|
"binary": "mesh-knowledge",
|
||||||
|
"loads": [
|
||||||
|
"mesh-knowledge"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL_FILE": "${dir:state}/database.url",
|
||||||
|
"MESH_KNOWLEDGE_SETTINGS": "${dir:state}/settings.json",
|
||||||
|
"MESH_KNOWLEDGE_STATUS": "${dir:state}/health.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"module": "mesh-reads",
|
||||||
|
"version": "1",
|
||||||
|
"runs-as": "mesh-reads",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"name": "node-reads",
|
||||||
|
"scope": "node",
|
||||||
|
"serves": [
|
||||||
|
"stat",
|
||||||
|
"readlink",
|
||||||
|
"list",
|
||||||
|
"read",
|
||||||
|
"search",
|
||||||
|
"account"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:state}/broker"
|
||||||
|
},
|
||||||
|
"secrets-owner": "mesh-reads",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"type": "user",
|
||||||
|
"name": "mesh-reads",
|
||||||
|
"shell": "/usr/bin/nologin",
|
||||||
|
"home": "/var/lib/mesh-reads"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "mesh-reads",
|
||||||
|
"place": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-reads",
|
||||||
|
"binary": "mesh-reads",
|
||||||
|
"loads": [
|
||||||
|
"mesh-reads"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
"mesh-controller.condition-raised",
|
"mesh-controller.condition-raised",
|
||||||
"mesh-controller.condition-changed",
|
"mesh-controller.condition-changed",
|
||||||
"mesh-controller.condition-cleared",
|
"mesh-controller.condition-cleared",
|
||||||
|
"mesh-controller.plan-moved",
|
||||||
"intake.choice",
|
"intake.choice",
|
||||||
"intake.link",
|
"intake.link",
|
||||||
"intake.failed",
|
"intake.failed",
|
||||||
@@ -93,7 +94,8 @@
|
|||||||
"name": "asks",
|
"name": "asks",
|
||||||
"ttl-seconds": 2592000
|
"ttl-seconds": 2592000
|
||||||
},
|
},
|
||||||
"identities"
|
"identities",
|
||||||
|
"walks"
|
||||||
],
|
],
|
||||||
"own-secrets": {
|
"own-secrets": {
|
||||||
"broker": "${dir:state}/broker"
|
"broker": "${dir:state}/broker"
|
||||||
@@ -129,7 +131,7 @@
|
|||||||
"path": "${dir:state}/settings.json",
|
"path": "${dir:state}/settings.json",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"merge": "json",
|
"merge": "json",
|
||||||
"content": "{\n \"time-zone\": \"\"\n}\n",
|
"content": "{\n \"time-zone\": \"\",\n \"only-what-needs-me\": []\n}\n",
|
||||||
"owner": "messenger"
|
"owner": "messenger"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"module": "netcheck",
|
"module": "netcheck",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"emits": [
|
||||||
|
"tunnel.endpoint-changed",
|
||||||
|
"tunnel.handshake-stale",
|
||||||
|
"tunnel.handshake-fresh"
|
||||||
|
],
|
||||||
"tools": [
|
"tools": [
|
||||||
|
"netcheck_tunnel",
|
||||||
"netcheck_tcp",
|
"netcheck_tcp",
|
||||||
"netcheck_dns",
|
"netcheck_dns",
|
||||||
"netcheck_http",
|
"netcheck_http",
|
||||||
@@ -16,6 +22,10 @@
|
|||||||
"netcheck_dns": [
|
"netcheck_dns": [
|
||||||
"dig",
|
"dig",
|
||||||
"nslookup"
|
"nslookup"
|
||||||
|
],
|
||||||
|
"netcheck_tunnel": [
|
||||||
|
"wg show",
|
||||||
|
"wg show mesh0 dump"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"tools": [
|
||||||
|
"backup_health",
|
||||||
|
"backup_delete_retired",
|
||||||
|
"backup_deleted"
|
||||||
|
],
|
||||||
"own-secrets": {
|
"own-secrets": {
|
||||||
"repository": "${dir:state}/repository.secret"
|
"repository": "${dir:state}/repository.secret"
|
||||||
},
|
},
|
||||||
@@ -32,6 +37,28 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"keep-daily": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 14,
|
||||||
|
"why": "two weeks of nightly restore points undo a mistake noticed within a fortnight; a count from 1 to 9999, read by the night, which thins nothing while it does not read (novox/hq issue 416); lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone"
|
||||||
|
},
|
||||||
|
"keep-weekly": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 8,
|
||||||
|
"why": "two months of weekly restore points behind the dailies; a count from 1 to 9999, refused by the night like keep-daily; lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone"
|
||||||
|
},
|
||||||
|
"keep-monthly": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 6,
|
||||||
|
"why": "half a year of monthly restore points behind the weeklies; a count from 1 to 9999, refused by the night like keep-daily; lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone"
|
||||||
|
},
|
||||||
|
"keep-hand-taken-days": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": 7,
|
||||||
|
"why": "a restore point taken by hand (node-backup.now, or the mesh's before a module moves) is kept until a later good night of its module exists, and this many days in any case, so the next thinning never takes the one taken against a risky change (novox/hq issue 453); a count from 1 to 9999, refused by the night like keep-daily; lowering it lets go of the restore points taken by hand that are older than the new count, past a later night, at the next thinning, and that cannot be undone"
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "state",
|
"id": "state",
|
||||||
@@ -58,6 +85,14 @@
|
|||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "# The data the modules on this machine declare, composed by the mesh (novox/hq ADR 0233). Do not edit.\n${contribution:node-backup:data}"
|
"content": "# The data the modules on this machine declare, composed by the mesh (novox/hq ADR 0233). Do not edit.\n${contribution:node-backup:data}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "retention",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/retention.conf",
|
||||||
|
"mode": "0644",
|
||||||
|
"trusted": true,
|
||||||
|
"content": "# How many restore points the night keeps, written by the mesh from this machine's settings keep-daily,\n# keep-weekly, keep-monthly (novox/hq issue 416) and keep-hand-taken-days (novox/hq issue 453). Replaced on every push; change the settings, never this file.\ndaily=${setting:keep-daily}\nweekly=${setting:keep-weekly}\nmonthly=${setting:keep-monthly}\nhand=${setting:keep-hand-taken-days}\n"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "tool",
|
"id": "tool",
|
||||||
"type": "package",
|
"type": "package",
|
||||||
@@ -67,6 +102,53 @@
|
|||||||
"id": "sqlite",
|
"id": "sqlite",
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"package": "sqlite"
|
"package": "sqlite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "night",
|
||||||
|
"type": "process",
|
||||||
|
"name": "restic-night",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./restic-backups",
|
||||||
|
"night"
|
||||||
|
],
|
||||||
|
"user": "${machine:account}",
|
||||||
|
"env": {
|
||||||
|
"MESH_BACKUP_DECLARED": "${dir:state}/backups.conf",
|
||||||
|
"MESH_BACKUP_REPOSITORY": "${dir:repository}",
|
||||||
|
"MESH_BACKUP_PASSWORD_FILE": "${dir:state}/repository.secret",
|
||||||
|
"MESH_BACKUP_STATE": "${dir:state}",
|
||||||
|
"MESH_BACKUP_DATA": "${dir:state}/data.conf"
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"kind": "tool",
|
||||||
|
"tool": "backup_health",
|
||||||
|
"interval": "60s",
|
||||||
|
"timeout": "20s",
|
||||||
|
"looks": 2,
|
||||||
|
"grace": "60s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "measure",
|
||||||
|
"type": "process",
|
||||||
|
"name": "restic-measure",
|
||||||
|
"artifact": "tools",
|
||||||
|
"run": [
|
||||||
|
"./restic-backups",
|
||||||
|
"measure"
|
||||||
|
],
|
||||||
|
"user": "${machine:account}",
|
||||||
|
"env": {
|
||||||
|
"MESH_BACKUP_DECLARED": "${dir:state}/backups.conf",
|
||||||
|
"MESH_BACKUP_REPOSITORY": "${dir:repository}",
|
||||||
|
"MESH_BACKUP_PASSWORD_FILE": "${dir:state}/repository.secret",
|
||||||
|
"MESH_BACKUP_STATE": "${dir:state}",
|
||||||
|
"MESH_BACKUP_DATA": "${dir:state}/data.conf"
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -179,7 +179,15 @@
|
|||||||
"internal-trust",
|
"internal-trust",
|
||||||
"internal-acme-env"
|
"internal-acme-env"
|
||||||
],
|
],
|
||||||
"logging": "journald"
|
"logging": "journald",
|
||||||
|
"health": {
|
||||||
|
"kind": "tcp",
|
||||||
|
"endpoint": "http",
|
||||||
|
"interval": "10s",
|
||||||
|
"timeout": "5s",
|
||||||
|
"looks": 3,
|
||||||
|
"grace": "60s"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -76,6 +76,18 @@
|
|||||||
"path": "${dir:state}/settings.yml",
|
"path": "${dir:state}/settings.yml",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"merge": "json",
|
"merge": "json",
|
||||||
|
"takes-settings": [
|
||||||
|
"general",
|
||||||
|
"brand",
|
||||||
|
"search",
|
||||||
|
"server",
|
||||||
|
"ui",
|
||||||
|
"outgoing",
|
||||||
|
"engines",
|
||||||
|
"plugins",
|
||||||
|
"categories_as_tabs",
|
||||||
|
"preferences"
|
||||||
|
],
|
||||||
"content": "{\n \"use_default_settings\": true,\n \"server\": {\n \"secret_key\": \"${secret:secret}\",\n \"base_url\": false,\n \"limiter\": false,\n \"image_proxy\": false,\n \"public_instance\": false\n },\n \"search\": {\n \"formats\": [\"html\", \"json\"]\n },\n \"valkey\": {\n \"url\": \"valkey://valkey:6379/0\"\n }\n}\n"
|
"content": "{\n \"use_default_settings\": true,\n \"server\": {\n \"secret_key\": \"${secret:secret}\",\n \"base_url\": false,\n \"limiter\": false,\n \"image_proxy\": false,\n \"public_instance\": false\n },\n \"search\": {\n \"formats\": [\"html\", \"json\"]\n },\n \"valkey\": {\n \"url\": \"valkey://valkey:6379/0\"\n }\n}\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user