96 lines
3.3 KiB
Go
96 lines
3.3 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"unicode/utf8"
|
|
|
|
"encoding/base64"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// FetchObject reads the object of the declaration-files bucket named by name, no larger than limit bytes (novox/hq ADR
|
|
// 0486 §1, §2). Nil is a node-engine with no link to the bus: a file by reference is then only the file already there.
|
|
type FetchObject func(ctx context.Context, name string, limit int64) ([]byte, error)
|
|
|
|
// ResolveFileReferences puts the content of every file given by digest and size into the declaration, so what is
|
|
// applied afterwards is the declaration as it was before references existed (novox/hq ADR 0486 §1, issue 1695).
|
|
//
|
|
// **Nothing of the declaration is applied before every reference is resolved**: a missing object, one whose size or
|
|
// sha256 differs from what the signed declaration names, or a fetch that fails, refuses the declaration whole. The
|
|
// content is checked with File.Verify before it is kept, and is only ever written by the apply that follows.
|
|
//
|
|
// A file already on the machine whose content verifies is used as it is and fetches nothing: the reconcile of an
|
|
// unchanged declaration, and a node that is asleep and has no bus, apply as they always did. One object is fetched
|
|
// once however many files name its digest.
|
|
func ResolveFileReferences(ctx context.Context, d *declaration.Declaration, fetch FetchObject) error {
|
|
fetched := map[string][]byte{}
|
|
for i, r := range d.Resources {
|
|
f, ok := r.(*declaration.File)
|
|
if !ok || !f.IsReference() {
|
|
continue
|
|
}
|
|
body, err := referencedContent(ctx, f, fetch, fetched)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inline := *f
|
|
inline.Digest, inline.Size = "", 0
|
|
if utf8.Valid(body) {
|
|
inline.Content = string(body)
|
|
} else {
|
|
inline.Bytes = base64.StdEncoding.EncodeToString(body)
|
|
}
|
|
d.Resources[i] = &inline
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func referencedContent(ctx context.Context, f *declaration.File, fetch FetchObject,
|
|
fetched map[string][]byte) ([]byte, error) {
|
|
if body, ok := fetched[f.Digest]; ok {
|
|
return body, nil
|
|
}
|
|
if body, ok := alreadyThere(f); ok {
|
|
fetched[f.Digest] = body
|
|
return body, nil
|
|
}
|
|
name, err := declaration.ObjectName(f.Digest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("file %s (%s): %w", f.ID, f.Path, err)
|
|
}
|
|
if fetch == nil {
|
|
return nil, fmt.Errorf("file %s (%s) is given by digest %s and is not on this machine, and this node-engine "+
|
|
"has no link to the bus to fetch it from", f.ID, f.Path, f.Digest)
|
|
}
|
|
body, err := fetch(ctx, name, f.Size)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("file %s (%s): fetching the object %s of the bucket %s: %w",
|
|
f.ID, f.Path, name, declaration.FileBucket, err)
|
|
}
|
|
if err := f.Verify(body); err != nil {
|
|
return nil, fmt.Errorf("%s (%s): %w; nothing was applied", f.ID, f.Path, err)
|
|
}
|
|
fetched[f.Digest] = body
|
|
return body, nil
|
|
}
|
|
|
|
// alreadyThere is the content of the file on the machine when it is what the reference names.
|
|
func alreadyThere(f *declaration.File) ([]byte, bool) {
|
|
if !filepath.IsAbs(f.Path) {
|
|
return nil, false
|
|
}
|
|
info, err := os.Lstat(f.Path)
|
|
if err != nil || !info.Mode().IsRegular() || info.Size() != f.Size {
|
|
return nil, false
|
|
}
|
|
body, err := os.ReadFile(f.Path)
|
|
if err != nil || f.Verify(body) != nil {
|
|
return nil, false
|
|
}
|
|
return body, true
|
|
}
|