105 lines
3.6 KiB
Go
105 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The node-engine's narrow local write verb (novox/hq ADR 0349 clause 4, design 57 P2; mesh-host internal/filling):
|
|
// one unix socket, one JSON request a line — a filled file's path and its content — and one answer. **The client
|
|
// fills; the node-engine writes**, as the declared file, with its owner, mode and who owns it at runtime: an owned
|
|
// file whole and written back whenever it differs, a seed only when absent, a managed file's own keys or block
|
|
// only. And it makes what reads the file take the change (restart-on, reload-on, a container that reads it). So
|
|
// the client never writes a machine's file itself and needs no right to.
|
|
//
|
|
// The request and the answer are copied here, field for field, from mesh-host internal/filling: the client is
|
|
// the one other reader of that format, and the node-engine's decoder refuses a field it does not know.
|
|
|
|
// DefaultSocket is where the node-engine serves the write.
|
|
const DefaultSocket = "/run/nox-mesh-host/fill.sock"
|
|
|
|
// FillRequest is one write.
|
|
type FillRequest struct {
|
|
Path string `json:"path"`
|
|
Content []byte `json:"content"`
|
|
}
|
|
|
|
// FillAnswer is what came of it: Refused is a write not attempted, Failed one attempted that did not complete,
|
|
// each with why; otherwise Action is what the write did (created, updated, corrected, unchanged, kept) and Digest
|
|
// what the file holds now.
|
|
type FillAnswer struct {
|
|
Path string `json:"path,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
Digest string `json:"digest,omitempty"`
|
|
Refused string `json:"refused,omitempty"`
|
|
Failed string `json:"failed,omitempty"`
|
|
}
|
|
|
|
// Filler has the node-engine write a filled file.
|
|
type Filler interface {
|
|
Fill(ctx context.Context, path string, content []byte) (FillAnswer, error)
|
|
}
|
|
|
|
// SocketFiller is the node-engine's socket.
|
|
type SocketFiller struct {
|
|
Socket string
|
|
// Busy is how long a write the node-engine says it is busy for waits before it is asked again, and Tries how
|
|
// many times it is asked.
|
|
Busy time.Duration
|
|
Tries int
|
|
}
|
|
|
|
// Fill asks the node-engine to write one file, again while it says it is busy.
|
|
func (s SocketFiller) Fill(ctx context.Context, path string, content []byte) (FillAnswer, error) {
|
|
tries := max(s.Tries, 1)
|
|
var a FillAnswer
|
|
var err error
|
|
for i := 0; i < tries; i++ {
|
|
if a, err = s.once(ctx, path, content); err != nil || !strings.HasPrefix(a.Refused, "busy") {
|
|
return a, err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return a, ctx.Err()
|
|
case <-time.After(s.Busy):
|
|
}
|
|
}
|
|
return a, err
|
|
}
|
|
|
|
func (s SocketFiller) once(ctx context.Context, path string, content []byte) (FillAnswer, error) {
|
|
var d net.Dialer
|
|
conn, err := d.DialContext(ctx, "unix", s.Socket)
|
|
if err != nil {
|
|
return FillAnswer{}, fmt.Errorf("the node-engine's write socket %s: %w", s.Socket, err)
|
|
}
|
|
defer conn.Close()
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = conn.SetDeadline(deadline)
|
|
}
|
|
if err := json.NewEncoder(conn).Encode(FillRequest{Path: path, Content: content}); err != nil {
|
|
return FillAnswer{}, fmt.Errorf("asking the node-engine to write %s: %w", path, err)
|
|
}
|
|
var a FillAnswer
|
|
if err := json.NewDecoder(conn).Decode(&a); err != nil {
|
|
return FillAnswer{}, fmt.Errorf("the node-engine's answer for %s: %w", path, err)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// errFill is a write the node-engine refused or failed, with its word.
|
|
func errFill(a FillAnswer) error {
|
|
switch {
|
|
case a.Refused != "":
|
|
return errors.New("the node-engine refused it: " + a.Refused)
|
|
case a.Failed != "":
|
|
return errors.New("the node-engine's write failed: " + a.Failed)
|
|
}
|
|
return nil
|
|
}
|