195 lines
5.5 KiB
Go
195 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// A module's templates, fetched by digest (novox/hq ADR 0349 clause 2, design 57 P5): the archive its build packed
|
|
// its templates directory into, fetched from where the plan says and used only when it hashes to the plan's
|
|
// digest — the bytes come from a network the mesh does not control, and only the digest makes them the ones the
|
|
// mesh built. Kept in the client's state by digest, so a node that cannot reach the artifact store still fills
|
|
// from what it fetched.
|
|
|
|
// maxTemplates bounds a templates archive: configuration text, never a theme.
|
|
const maxTemplates = 32 << 20
|
|
|
|
// Fetcher fetches an archive's bytes from its source.
|
|
type Fetcher func(ctx context.Context, source string) ([]byte, error)
|
|
|
|
// HTTPFetch is the artifact store, over HTTP.
|
|
func HTTPFetch(ctx context.Context, source string) ([]byte, error) {
|
|
if !strings.HasPrefix(source, "https://") && !strings.HasPrefix(source, "http://") {
|
|
return nil, fmt.Errorf("%s is not an address the templates can be fetched from", source)
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot fetch %s: %w", source, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("fetching %s: %s", source, resp.Status)
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, maxTemplates+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(body) > maxTemplates {
|
|
return nil, fmt.Errorf("%s is over %d bytes, more than a module's templates", source, maxTemplates)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
// templateStore keeps fetched archives by digest under dir, and their texts in memory.
|
|
type templateStore struct {
|
|
dir string
|
|
fetch Fetcher
|
|
texts map[string]map[string]string
|
|
}
|
|
|
|
// load is every template's text of the archive the plan names, by its path inside the archive.
|
|
func (s *templateStore) load(ctx context.Context, t Templates) (map[string]string, error) {
|
|
if texts, ok := s.texts[t.Digest]; ok {
|
|
return texts, nil
|
|
}
|
|
kept := filepath.Join(s.dir, strings.TrimPrefix(t.Digest, "sha256:")+".tar.gz")
|
|
body, err := os.ReadFile(kept)
|
|
if err != nil || digestOf(body) != t.Digest {
|
|
if body, err = s.fetch(ctx, t.Source); err != nil {
|
|
return nil, err
|
|
}
|
|
if got := digestOf(body); got != t.Digest {
|
|
return nil, fmt.Errorf("the templates at %s are %s, and the plan names %s: nothing was filled from them",
|
|
t.Source, got, t.Digest)
|
|
}
|
|
if err := writeKept(kept, body); err != nil {
|
|
// Said and gone on: the archive is in hand, and is fetched again next start.
|
|
logf("[provisioner-client] the templates %s could not be kept at %s: %v", t.Digest, kept, err)
|
|
}
|
|
}
|
|
texts, err := unpackTexts(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the templates %s cannot be read: %w", t.Digest, err)
|
|
}
|
|
if s.texts == nil {
|
|
s.texts = map[string]map[string]string{}
|
|
}
|
|
s.texts[t.Digest] = texts
|
|
return texts, nil
|
|
}
|
|
|
|
func digestOf(b []byte) string {
|
|
sum := sha256.Sum256(b)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func writeKept(at string, body []byte) error {
|
|
if err := os.MkdirAll(filepath.Dir(at), 0o700); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(at), ".templates-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if _, err := tmp.Write(body); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), at)
|
|
}
|
|
|
|
// unpackTexts is every regular file of a gzipped tar, by its clean path inside it.
|
|
func unpackTexts(body []byte) (map[string]string, error) {
|
|
zipped, err := gzip.NewReader(bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zipped.Close()
|
|
r := tar.NewReader(zipped)
|
|
out := map[string]string{}
|
|
for {
|
|
h, err := r.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
return out, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if h.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
b, err := io.ReadAll(io.LimitReader(r, maxTemplates))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[strings.TrimPrefix(path.Clean(h.Name), "./")] = string(b)
|
|
}
|
|
}
|
|
|
|
// placeholder is `${<kind>:<name>}`, as ADR 0349 clause 2 writes one.
|
|
var placeholder = regexp.MustCompile(`\$\{([a-z][a-z0-9-]*):([^}]+)\}`)
|
|
|
|
// Need is one value a template names.
|
|
type Need struct {
|
|
Kind string
|
|
Name string
|
|
}
|
|
|
|
func (n Need) String() string { return n.Kind + ":" + n.Name }
|
|
|
|
// needsOf is every placeholder a text names, in the order each first appears.
|
|
func needsOf(text string) []Need {
|
|
var out []Need
|
|
seen := map[Need]bool{}
|
|
for _, m := range placeholder.FindAllStringSubmatch(text, -1) {
|
|
n := Need{Kind: m[1], Name: m[2]}
|
|
if !seen[n] {
|
|
seen[n] = true
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// render fills every placeholder of text from values, which holds each.
|
|
func render(text string, values map[Need]string) string {
|
|
return placeholder.ReplaceAllStringFunc(text, func(ref string) string {
|
|
m := placeholder.FindStringSubmatch(ref)
|
|
return values[Need{Kind: m[1], Name: m[2]}]
|
|
})
|
|
}
|
|
|
|
// configText is a config value as a file takes it: a string as its text, anything else as its JSON.
|
|
func configText(raw json.RawMessage) string {
|
|
var s string
|
|
if json.Unmarshal(raw, &s) == nil {
|
|
return s
|
|
}
|
|
return string(raw)
|
|
}
|