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 }