localmint.go

v0.4.0
Doc Versions Source
1
package steps
2
3
import (
4
	"context"
5
	"errors"
6
	"io/fs"
7
	"os"
8
	"path/filepath"
9
10
	"go.bigb.es/auxilia/culpa"
11
	"go.bigb.es/auxilia/legatus"
12
)
13
14
// LocalMint mints a LOCAL file once from a remote command, then reuses it. Every
15
// other step in this package manages a REMOTE file; LocalMint is the inverse — its
16
// target lives on the machine running the Flow. It models "derive a local artifact
17
// from something only the remote host can produce" (a generated key, a fetched
18
// certificate, an exported config): expensive to produce, cheap to keep.
19
//
20
//   - Check is a local stat: apply is needed when Path is missing, or when a Stale
21
//     predicate flags the existing bytes (e.g. an always-true predicate for an
22
//     artifact that must be regenerated every run, or a content test for a back-fill).
23
//   - Apply reads any existing bytes, hands them to Produce (so a back-fill producer
24
//     can preserve them), runs the producer over the Conn, and writes the result with
25
//     an atomic temp+rename at Mode. A Produce error aborts with nothing written.
26
//
27
// Path, Mode, Stale, and Produce are all caller-supplied; Mode must be set (a zero
28
// Mode writes a 0000 file). LocalMint itself is transport- and content-agnostic.
29
type LocalMint struct {
30
	Path    string
31
	Mode    legatus.FileMode
32
	Stale   func(existing []byte) bool
33
	Produce func(ctx context.Context, conn legatus.Conn, existing []byte) ([]byte, error)
34
}
35
36
// Name returns the step name (the local path it owns).
37
func (m LocalMint) Name() string { return "mint:" + m.Path }
38
39
// Check reports apply-needed when Path is missing or Stale flags the existing
40
// bytes. A read error other than not-exist is a hard failure (no regenerate over an
41
// unreadable file).
42
func (m LocalMint) Check(*legatus.FlowCtx) (bool, error) {
43
	b, err := os.ReadFile(m.Path)
44
	if errors.Is(err, fs.ErrNotExist) {
45
		return true, nil
46
	}
47
	if err != nil {
48
		return false, culpa.WithCode(culpa.Wrapf(err, "mint: read %s", m.Path), "LOCAL_MINT")
49
	}
50
	if m.Stale != nil && m.Stale(b) {
51
		return true, nil
52
	}
53
	return false, nil
54
}
55
56
// Apply reads any existing bytes, runs Produce over the Conn, and writes the result
57
// atomically at Mode.
58
func (m LocalMint) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
59
	if m.Produce == nil {
60
		return legatus.Outcome{}, culpa.WithCode(
61
			culpa.Errorf("mint %s: no Produce set", m.Path), "LOCAL_MINT")
62
	}
63
64
	var existing []byte
65
	if b, err := os.ReadFile(m.Path); err == nil {
66
		existing = b
67
	} else if !errors.Is(err, fs.ErrNotExist) {
68
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "mint: read %s", m.Path), "LOCAL_MINT")
69
	}
70
71
	out, err := m.Produce(fc.Ctx(), fc.Conn(), existing)
72
	if err != nil {
73
		return legatus.Outcome{}, err
74
	}
75
	if err := mintWrite(m.Path, out, m.Mode); err != nil {
76
		return legatus.Outcome{}, err
77
	}
78
	return legatus.Outcome{Changed: true, Message: "minted " + filepath.Base(m.Path)}, nil
79
}
80
81
// mintWrite writes data to path via a temp file in the same dir + rename, so a
82
// crash mid-write never leaves a half-written artifact. Mirrors legatus/local's
83
// atomicWrite (CreateTemp → write → close → chmod-by-path → rename).
84
func mintWrite(path string, data []byte, mode legatus.FileMode) error {
85
	dir := filepath.Dir(path)
86
	if err := os.MkdirAll(dir, 0o755); err != nil {
87
		return culpa.WithCode(culpa.Wrapf(err, "mint: mkdir %s", dir), "LOCAL_MINT")
88
	}
89
	tmp, err := os.CreateTemp(dir, ".legatus-mint-*")
90
	if err != nil {
91
		return culpa.WithCode(culpa.Wrapf(err, "mint: temp in %s", dir), "LOCAL_MINT")
92
	}
93
	tmpName := tmp.Name()
94
	if _, err := tmp.Write(data); err != nil {
95
		_ = tmp.Close()
96
		_ = os.Remove(tmpName)
97
		return culpa.WithCode(culpa.Wrapf(err, "mint: write %s", tmpName), "LOCAL_MINT")
98
	}
99
	if err := tmp.Close(); err != nil {
100
		_ = os.Remove(tmpName)
101
		return culpa.WithCode(culpa.Wrapf(err, "mint: close %s", tmpName), "LOCAL_MINT")
102
	}
103
	if err := os.Chmod(tmpName, mode); err != nil {
104
		_ = os.Remove(tmpName)
105
		return culpa.WithCode(culpa.Wrapf(err, "mint: chmod %s", tmpName), "LOCAL_MINT")
106
	}
107
	if err := os.Rename(tmpName, path); err != nil {
108
		_ = os.Remove(tmpName)
109
		return culpa.WithCode(culpa.Wrapf(err, "mint: rename into %s", path), "LOCAL_MINT")
110
	}
111
	return nil
112
}
113

Source Files