Files
mesh-catalog/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/fill.go
T

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
}