Files
releases/bundle/store.go
T
Ashutosh Narkar 0e69dbba20 Extend type checking for authz policies
The schema of the input document for the authorization
policy is known to OPA. This feature leverages that
to perform automatic type checking on the authorization policy.
The checks happen on policies provided to OPA on start-up and
also those provided via bundles. This check is enabled by default
and can be disabled using the `--skip-known-schema-check` flag
on `opa run`. This feature will help catch errors such as
typos, mismatch types etc. in these policies and provide precise
feedback to the policy author.

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
2023-09-11 15:34:10 -07:00

1002 lines
28 KiB
Go

// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package bundle
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/open-policy-agent/opa/ast"
iCompiler "github.com/open-policy-agent/opa/internal/compiler"
"github.com/open-policy-agent/opa/internal/json/patch"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/util"
)
// BundlesBasePath is the storage path used for storing bundle metadata
var BundlesBasePath = storage.MustParsePath("/system/bundles")
// Note: As needed these helpers could be memoized.
// ManifestStoragePath is the storage path used for the given named bundle manifest.
func ManifestStoragePath(name string) storage.Path {
return append(BundlesBasePath, name, "manifest")
}
// EtagStoragePath is the storage path used for the given named bundle etag.
func EtagStoragePath(name string) storage.Path {
return append(BundlesBasePath, name, "etag")
}
func namedBundlePath(name string) storage.Path {
return append(BundlesBasePath, name)
}
func rootsPath(name string) storage.Path {
return append(BundlesBasePath, name, "manifest", "roots")
}
func revisionPath(name string) storage.Path {
return append(BundlesBasePath, name, "manifest", "revision")
}
func wasmModulePath(name string) storage.Path {
return append(BundlesBasePath, name, "wasm")
}
func wasmEntrypointsPath(name string) storage.Path {
return append(BundlesBasePath, name, "manifest", "wasm")
}
func metadataPath(name string) storage.Path {
return append(BundlesBasePath, name, "manifest", "metadata")
}
// ReadBundleNamesFromStore will return a list of bundle names which have had their metadata stored.
func ReadBundleNamesFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) ([]string, error) {
value, err := store.Read(ctx, txn, BundlesBasePath)
if err != nil {
return nil, err
}
bundleMap, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("corrupt manifest roots")
}
bundles := make([]string, len(bundleMap))
idx := 0
for name := range bundleMap {
bundles[idx] = name
idx++
}
return bundles, nil
}
// WriteManifestToStore will write the manifest into the storage. This function is called when
// the bundle is activated.
func WriteManifestToStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string, manifest Manifest) error {
return write(ctx, store, txn, ManifestStoragePath(name), manifest)
}
// WriteEtagToStore will write the bundle etag into the storage. This function is called when the bundle is activated.
func WriteEtagToStore(ctx context.Context, store storage.Store, txn storage.Transaction, name, etag string) error {
return write(ctx, store, txn, EtagStoragePath(name), etag)
}
func write(ctx context.Context, store storage.Store, txn storage.Transaction, path storage.Path, value interface{}) error {
if err := util.RoundTrip(&value); err != nil {
return err
}
var dir []string
if len(path) > 1 {
dir = path[:len(path)-1]
}
if err := storage.MakeDir(ctx, store, txn, dir); err != nil {
return err
}
return store.Write(ctx, txn, storage.AddOp, path, value)
}
// EraseManifestFromStore will remove the manifest from storage. This function is called
// when the bundle is deactivated.
func EraseManifestFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) error {
path := namedBundlePath(name)
err := store.Write(ctx, txn, storage.RemoveOp, path, nil)
return suppressNotFound(err)
}
// eraseBundleEtagFromStore will remove the bundle etag from storage. This function is called
// when the bundle is deactivated.
func eraseBundleEtagFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) error {
path := EtagStoragePath(name)
err := store.Write(ctx, txn, storage.RemoveOp, path, nil)
return suppressNotFound(err)
}
func suppressNotFound(err error) error {
if err == nil || storage.IsNotFound(err) {
return nil
}
return err
}
func writeWasmModulesToStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string, b *Bundle) error {
basePath := wasmModulePath(name)
for _, wm := range b.WasmModules {
path := append(basePath, wm.Path)
err := write(ctx, store, txn, path, base64.StdEncoding.EncodeToString(wm.Raw))
if err != nil {
return err
}
}
return nil
}
func eraseWasmModulesFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) error {
path := wasmModulePath(name)
err := store.Write(ctx, txn, storage.RemoveOp, path, nil)
return suppressNotFound(err)
}
// ReadWasmMetadataFromStore will read Wasm module resolver metadata from the store.
func ReadWasmMetadataFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) ([]WasmResolver, error) {
path := wasmEntrypointsPath(name)
value, err := store.Read(ctx, txn, path)
if err != nil {
return nil, err
}
bs, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("corrupt wasm manifest data")
}
var wasmMetadata []WasmResolver
err = util.UnmarshalJSON(bs, &wasmMetadata)
if err != nil {
return nil, fmt.Errorf("corrupt wasm manifest data")
}
return wasmMetadata, nil
}
// ReadWasmModulesFromStore will write Wasm module resolver metadata from the store.
func ReadWasmModulesFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (map[string][]byte, error) {
path := wasmModulePath(name)
value, err := store.Read(ctx, txn, path)
if err != nil {
return nil, err
}
encodedModules, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("corrupt wasm modules")
}
rawModules := map[string][]byte{}
for path, enc := range encodedModules {
encStr, ok := enc.(string)
if !ok {
return nil, fmt.Errorf("corrupt wasm modules")
}
bs, err := base64.StdEncoding.DecodeString(encStr)
if err != nil {
return nil, err
}
rawModules[path] = bs
}
return rawModules, nil
}
// ReadBundleRootsFromStore returns the roots in the specified bundle.
// If the bundle is not activated, this function will return
// storage NotFound error.
func ReadBundleRootsFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) ([]string, error) {
value, err := store.Read(ctx, txn, rootsPath(name))
if err != nil {
return nil, err
}
sl, ok := value.([]interface{})
if !ok {
return nil, fmt.Errorf("corrupt manifest roots")
}
roots := make([]string, len(sl))
for i := range sl {
roots[i], ok = sl[i].(string)
if !ok {
return nil, fmt.Errorf("corrupt manifest root")
}
}
return roots, nil
}
// ReadBundleRevisionFromStore returns the revision in the specified bundle.
// If the bundle is not activated, this function will return
// storage NotFound error.
func ReadBundleRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (string, error) {
return readRevisionFromStore(ctx, store, txn, revisionPath(name))
}
func readRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, path storage.Path) (string, error) {
value, err := store.Read(ctx, txn, path)
if err != nil {
return "", err
}
str, ok := value.(string)
if !ok {
return "", fmt.Errorf("corrupt manifest revision")
}
return str, nil
}
// ReadBundleMetadataFromStore returns the metadata in the specified bundle.
// If the bundle is not activated, this function will return
// storage NotFound error.
func ReadBundleMetadataFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (map[string]interface{}, error) {
return readMetadataFromStore(ctx, store, txn, metadataPath(name))
}
func readMetadataFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, path storage.Path) (map[string]interface{}, error) {
value, err := store.Read(ctx, txn, path)
if err != nil {
return nil, suppressNotFound(err)
}
data, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("corrupt manifest metadata")
}
return data, nil
}
// ReadBundleEtagFromStore returns the etag for the specified bundle.
// If the bundle is not activated, this function will return
// storage NotFound error.
func ReadBundleEtagFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (string, error) {
return readEtagFromStore(ctx, store, txn, EtagStoragePath(name))
}
func readEtagFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, path storage.Path) (string, error) {
value, err := store.Read(ctx, txn, path)
if err != nil {
return "", err
}
str, ok := value.(string)
if !ok {
return "", fmt.Errorf("corrupt bundle etag")
}
return str, nil
}
// ActivateOpts defines options for the Activate API call.
type ActivateOpts struct {
Ctx context.Context
Store storage.Store
Txn storage.Transaction
TxnCtx *storage.Context
Compiler *ast.Compiler
Metrics metrics.Metrics
Bundles map[string]*Bundle // Optional
ExtraModules map[string]*ast.Module // Optional
AuthorizationDecisionRef ast.Ref
legacy bool
}
// Activate the bundle(s) by loading into the given Store. This will load policies, data, and record
// the manifest in storage. The compiler provided will have had the polices compiled on it.
func Activate(opts *ActivateOpts) error {
opts.legacy = false
return activateBundles(opts)
}
// DeactivateOpts defines options for the Deactivate API call
type DeactivateOpts struct {
Ctx context.Context
Store storage.Store
Txn storage.Transaction
BundleNames map[string]struct{}
}
// Deactivate the bundle(s). This will erase associated data, policies, and the manifest entry from the store.
func Deactivate(opts *DeactivateOpts) error {
erase := map[string]struct{}{}
for name := range opts.BundleNames {
roots, err := ReadBundleRootsFromStore(opts.Ctx, opts.Store, opts.Txn, name)
if suppressNotFound(err) != nil {
return err
}
for _, root := range roots {
erase[root] = struct{}{}
}
}
_, err := eraseBundles(opts.Ctx, opts.Store, opts.Txn, opts.BundleNames, erase)
return err
}
func activateBundles(opts *ActivateOpts) error {
// Build collections of bundle names, modules, and roots to erase
erase := map[string]struct{}{}
names := map[string]struct{}{}
deltaBundles := map[string]*Bundle{}
snapshotBundles := map[string]*Bundle{}
for name, b := range opts.Bundles {
if b.Type() == DeltaBundleType {
deltaBundles[name] = b
} else {
snapshotBundles[name] = b
names[name] = struct{}{}
roots, err := ReadBundleRootsFromStore(opts.Ctx, opts.Store, opts.Txn, name)
if suppressNotFound(err) != nil {
return err
}
for _, root := range roots {
erase[root] = struct{}{}
}
// Erase data at new roots to prepare for writing the new data
for _, root := range *b.Manifest.Roots {
erase[root] = struct{}{}
}
}
}
// Before changing anything make sure the roots don't collide with any
// other bundles that already are activated or other bundles being activated.
err := hasRootsOverlap(opts.Ctx, opts.Store, opts.Txn, opts.Bundles)
if err != nil {
return err
}
if len(deltaBundles) != 0 {
err := activateDeltaBundles(opts, deltaBundles)
if err != nil {
return err
}
}
// Erase data and policies at new + old roots, and remove the old
// manifests before activating a new snapshot bundle.
remaining, err := eraseBundles(opts.Ctx, opts.Store, opts.Txn, names, erase)
if err != nil {
return err
}
// Validate data in bundle does not contain paths outside the bundle's roots.
for _, b := range snapshotBundles {
if b.lazyLoadingMode {
for _, item := range b.Raw {
path := filepath.ToSlash(item.Path)
if filepath.Base(path) == dataFile || filepath.Base(path) == yamlDataFile {
var val map[string]json.RawMessage
err = util.Unmarshal(item.Value, &val)
if err == nil {
err = doDFS(val, filepath.Dir(strings.Trim(path, "/")), *b.Manifest.Roots)
if err != nil {
return err
}
} else {
// Build an object for the value
p := getNormalizedPath(path)
if len(p) == 0 {
return fmt.Errorf("root value must be object")
}
// verify valid YAML or JSON value
var x interface{}
err := util.Unmarshal(item.Value, &x)
if err != nil {
return err
}
value := item.Value
dir := map[string]json.RawMessage{}
for i := len(p) - 1; i > 0; i-- {
dir[p[i]] = value
bs, err := json.Marshal(dir)
if err != nil {
return err
}
value = bs
dir = map[string]json.RawMessage{}
}
dir[p[0]] = value
err = doDFS(dir, filepath.Dir(strings.Trim(path, "/")), *b.Manifest.Roots)
if err != nil {
return err
}
}
}
}
}
}
// Compile the modules all at once to avoid having to re-do work.
remainingAndExtra := make(map[string]*ast.Module)
for name, mod := range remaining {
remainingAndExtra[name] = mod
}
for name, mod := range opts.ExtraModules {
remainingAndExtra[name] = mod
}
err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef)
if err != nil {
return err
}
if err := writeDataAndModules(opts.Ctx, opts.Store, opts.Txn, opts.TxnCtx, snapshotBundles, opts.legacy); err != nil {
return err
}
if err := ast.CheckPathConflicts(opts.Compiler, storage.NonEmpty(opts.Ctx, opts.Store, opts.Txn)); len(err) > 0 {
return err
}
for name, b := range snapshotBundles {
if err := writeManifestToStore(opts, name, b.Manifest); err != nil {
return err
}
if err := writeEtagToStore(opts, name, b.Etag); err != nil {
return err
}
if err := writeWasmModulesToStore(opts.Ctx, opts.Store, opts.Txn, name, b); err != nil {
return err
}
}
return nil
}
func doDFS(obj map[string]json.RawMessage, path string, roots []string) error {
if len(roots) == 1 && roots[0] == "" {
return nil
}
for key := range obj {
newPath := filepath.Join(strings.Trim(path, "/"), key)
// Note: filepath.Join can return paths with '\' separators, always use
// filepath.ToSlash to keep them normalized.
newPath = strings.TrimLeft(normalizePath(newPath), "/.")
contains := false
prefix := false
if RootPathsContain(roots, newPath) {
contains = true
} else {
for i := range roots {
if strings.HasPrefix(strings.Trim(roots[i], "/"), newPath) {
prefix = true
break
}
}
}
if !contains && !prefix {
return fmt.Errorf("manifest roots %v do not permit data at path '/%s' (hint: check bundle directory structure)", roots, newPath)
}
if contains {
continue
}
var next map[string]json.RawMessage
err := util.Unmarshal(obj[key], &next)
if err != nil {
return fmt.Errorf("manifest roots %v do not permit data at path '/%s' (hint: check bundle directory structure)", roots, newPath)
}
if err := doDFS(next, newPath, roots); err != nil {
return err
}
}
return nil
}
func activateDeltaBundles(opts *ActivateOpts, bundles map[string]*Bundle) error {
// Check that the manifest roots and wasm resolvers in the delta bundle
// match with those currently in the store
for name, b := range bundles {
value, err := opts.Store.Read(opts.Ctx, opts.Txn, ManifestStoragePath(name))
if err != nil {
if storage.IsNotFound(err) {
continue
}
return err
}
bs, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("corrupt manifest data: %w", err)
}
var manifest Manifest
err = util.UnmarshalJSON(bs, &manifest)
if err != nil {
return fmt.Errorf("corrupt manifest data: %w", err)
}
if !b.Manifest.equalWasmResolversAndRoots(manifest) {
return fmt.Errorf("delta bundle '%s' has wasm resolvers or manifest roots that are different from those in the store", name)
}
}
for _, b := range bundles {
err := applyPatches(opts.Ctx, opts.Store, opts.Txn, b.Patch.Data)
if err != nil {
return err
}
}
if err := ast.CheckPathConflicts(opts.Compiler, storage.NonEmpty(opts.Ctx, opts.Store, opts.Txn)); len(err) > 0 {
return err
}
for name, b := range bundles {
if err := writeManifestToStore(opts, name, b.Manifest); err != nil {
return err
}
if err := writeEtagToStore(opts, name, b.Etag); err != nil {
return err
}
}
return nil
}
// erase bundles by name and roots. This will clear all policies and data at its roots and remove its
// manifest from storage.
func eraseBundles(ctx context.Context, store storage.Store, txn storage.Transaction, names map[string]struct{}, roots map[string]struct{}) (map[string]*ast.Module, error) {
if err := eraseData(ctx, store, txn, roots); err != nil {
return nil, err
}
remaining, err := erasePolicies(ctx, store, txn, roots)
if err != nil {
return nil, err
}
for name := range names {
if err := EraseManifestFromStore(ctx, store, txn, name); suppressNotFound(err) != nil {
return nil, err
}
if err := LegacyEraseManifestFromStore(ctx, store, txn); suppressNotFound(err) != nil {
return nil, err
}
if err := eraseBundleEtagFromStore(ctx, store, txn, name); suppressNotFound(err) != nil {
return nil, err
}
if err := eraseWasmModulesFromStore(ctx, store, txn, name); suppressNotFound(err) != nil {
return nil, err
}
}
return remaining, nil
}
func eraseData(ctx context.Context, store storage.Store, txn storage.Transaction, roots map[string]struct{}) error {
for root := range roots {
path, ok := storage.ParsePathEscaped("/" + root)
if !ok {
return fmt.Errorf("manifest root path invalid: %v", root)
}
if len(path) > 0 {
if err := store.Write(ctx, txn, storage.RemoveOp, path, nil); suppressNotFound(err) != nil {
return err
}
}
}
return nil
}
func erasePolicies(ctx context.Context, store storage.Store, txn storage.Transaction, roots map[string]struct{}) (map[string]*ast.Module, error) {
ids, err := store.ListPolicies(ctx, txn)
if err != nil {
return nil, err
}
remaining := map[string]*ast.Module{}
for _, id := range ids {
bs, err := store.GetPolicy(ctx, txn, id)
if err != nil {
return nil, err
}
module, err := ast.ParseModule(id, string(bs))
if err != nil {
return nil, err
}
path, err := module.Package.Path.Ptr()
if err != nil {
return nil, err
}
deleted := false
for root := range roots {
if RootPathsContain([]string{root}, path) {
if err := store.DeletePolicy(ctx, txn, id); err != nil {
return nil, err
}
deleted = true
break
}
}
if !deleted {
remaining[id] = module
}
}
return remaining, nil
}
func writeManifestToStore(opts *ActivateOpts, name string, manifest Manifest) error {
// Always write manifests to the named location. If the plugin is in the older style config
// then also write to the old legacy unnamed location.
if err := WriteManifestToStore(opts.Ctx, opts.Store, opts.Txn, name, manifest); err != nil {
return err
}
if opts.legacy {
if err := LegacyWriteManifestToStore(opts.Ctx, opts.Store, opts.Txn, manifest); err != nil {
return err
}
}
return nil
}
func writeEtagToStore(opts *ActivateOpts, name, etag string) error {
if err := WriteEtagToStore(opts.Ctx, opts.Store, opts.Txn, name, etag); err != nil {
return err
}
return nil
}
func writeDataAndModules(ctx context.Context, store storage.Store, txn storage.Transaction, txnCtx *storage.Context, bundles map[string]*Bundle, legacy bool) error {
params := storage.WriteParams
params.Context = txnCtx
for name, b := range bundles {
if len(b.Raw) == 0 {
// Write data from each new bundle into the store. Only write under the
// roots contained in their manifest.
if err := writeData(ctx, store, txn, *b.Manifest.Roots, b.Data); err != nil {
return err
}
for _, mf := range b.Modules {
var path string
// For backwards compatibility, in legacy mode, upsert policies to
// the unprefixed path.
if legacy {
path = mf.Path
} else {
path = modulePathWithPrefix(name, mf.Path)
}
if err := store.UpsertPolicy(ctx, txn, path, mf.Raw); err != nil {
return err
}
}
} else {
params.BasePaths = *b.Manifest.Roots
err := store.Truncate(ctx, txn, params, NewIterator(b.Raw))
if err != nil {
return fmt.Errorf("store truncate failed for bundle '%s': %v", name, err)
}
}
}
return nil
}
func writeData(ctx context.Context, store storage.Store, txn storage.Transaction, roots []string, data map[string]interface{}) error {
for _, root := range roots {
path, ok := storage.ParsePathEscaped("/" + root)
if !ok {
return fmt.Errorf("manifest root path invalid: %v", root)
}
if value, ok := lookup(path, data); ok {
if len(path) > 0 {
if err := storage.MakeDir(ctx, store, txn, path[:len(path)-1]); err != nil {
return err
}
}
if err := store.Write(ctx, txn, storage.AddOp, path, value); err != nil {
return err
}
}
}
return nil
}
func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref) error {
m.Timer(metrics.RegoModuleCompile).Start()
defer m.Timer(metrics.RegoModuleCompile).Stop()
modules := map[string]*ast.Module{}
// preserve any modules already on the compiler
for name, module := range compiler.Modules {
modules[name] = module
}
// preserve any modules passed in from the store
for name, module := range extraModules {
modules[name] = module
}
// include all the new bundle modules
for bundleName, b := range bundles {
if legacy {
for _, mf := range b.Modules {
modules[mf.Path] = mf.Parsed
}
} else {
for name, module := range b.ParsedModules(bundleName) {
modules[name] = module
}
}
}
if compiler.Compile(modules); compiler.Failed() {
return compiler.Errors
}
if authorizationDecisionRef.Equal(ast.EmptyRef()) {
return nil
}
return iCompiler.VerifyAuthorizationPolicySchema(compiler, authorizationDecisionRef)
}
func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool) error {
m.Timer(metrics.RegoModuleCompile).Start()
defer m.Timer(metrics.RegoModuleCompile).Stop()
modules := map[string]*ast.Module{}
// preserve any modules already on the compiler
for name, module := range compiler.Modules {
modules[name] = module
}
// preserve any modules passed in from the store
for name, module := range extraModules {
modules[name] = module
}
// include all the new bundle modules
for bundleName, b := range bundles {
if legacy {
for _, mf := range b.Modules {
modules[mf.Path] = mf.Parsed
}
} else {
for name, module := range b.ParsedModules(bundleName) {
modules[name] = module
}
}
}
if compiler.Compile(modules); compiler.Failed() {
return compiler.Errors
}
for bundleName, b := range bundles {
for _, mf := range b.Modules {
var path string
// For backwards compatibility, in legacy mode, upsert policies to
// the unprefixed path.
if legacy {
path = mf.Path
} else {
path = modulePathWithPrefix(bundleName, mf.Path)
}
if err := store.UpsertPolicy(ctx, txn, path, mf.Raw); err != nil {
return err
}
}
}
return nil
}
func lookup(path storage.Path, data map[string]interface{}) (interface{}, bool) {
if len(path) == 0 {
return data, true
}
for i := 0; i < len(path)-1; i++ {
value, ok := data[path[i]]
if !ok {
return nil, false
}
obj, ok := value.(map[string]interface{})
if !ok {
return nil, false
}
data = obj
}
value, ok := data[path[len(path)-1]]
return value, ok
}
func hasRootsOverlap(ctx context.Context, store storage.Store, txn storage.Transaction, bundles map[string]*Bundle) error {
collisions := map[string][]string{}
allBundles, err := ReadBundleNamesFromStore(ctx, store, txn)
if suppressNotFound(err) != nil {
return err
}
allRoots := map[string][]string{}
// Build a map of roots for existing bundles already in the system
for _, name := range allBundles {
roots, err := ReadBundleRootsFromStore(ctx, store, txn, name)
if suppressNotFound(err) != nil {
return err
}
allRoots[name] = roots
}
// Add in any bundles that are being activated, overwrite existing roots
// with new ones where bundles are in both groups.
for name, bundle := range bundles {
allRoots[name] = *bundle.Manifest.Roots
}
// Now check for each new bundle if it conflicts with any of the others
for name, bundle := range bundles {
for otherBundle, otherRoots := range allRoots {
if name == otherBundle {
// Skip the current bundle being checked
continue
}
// Compare the "new" roots with other existing (or a different bundles new roots)
for _, newRoot := range *bundle.Manifest.Roots {
for _, otherRoot := range otherRoots {
if RootPathsOverlap(newRoot, otherRoot) {
collisions[otherBundle] = append(collisions[otherBundle], newRoot)
}
}
}
}
}
if len(collisions) > 0 {
var bundleNames []string
for name := range collisions {
bundleNames = append(bundleNames, name)
}
return fmt.Errorf("detected overlapping roots in bundle manifest with: %s", bundleNames)
}
return nil
}
func applyPatches(ctx context.Context, store storage.Store, txn storage.Transaction, patches []PatchOperation) error {
for _, pat := range patches {
// construct patch path
path, ok := patch.ParsePatchPathEscaped("/" + strings.Trim(pat.Path, "/"))
if !ok {
return fmt.Errorf("error parsing patch path")
}
var op storage.PatchOp
switch pat.Op {
case "upsert":
op = storage.AddOp
_, err := store.Read(ctx, txn, path[:len(path)-1])
if err != nil {
if !storage.IsNotFound(err) {
return err
}
if err := storage.MakeDir(ctx, store, txn, path[:len(path)-1]); err != nil {
return err
}
}
case "remove":
op = storage.RemoveOp
case "replace":
op = storage.ReplaceOp
default:
return fmt.Errorf("bad patch operation: %v", pat.Op)
}
// apply the patch
if err := store.Write(ctx, txn, op, path, pat.Value); err != nil {
return err
}
}
return nil
}
// Helpers for the older single (unnamed) bundle style manifest storage.
// LegacyManifestStoragePath is the older unnamed bundle path for manifests to be stored.
// Deprecated: Use ManifestStoragePath and named bundles instead.
var legacyManifestStoragePath = storage.MustParsePath("/system/bundle/manifest")
var legacyRevisionStoragePath = append(legacyManifestStoragePath, "revision")
// LegacyWriteManifestToStore will write the bundle manifest to the older single (unnamed) bundle manifest location.
// Deprecated: Use WriteManifestToStore and named bundles instead.
func LegacyWriteManifestToStore(ctx context.Context, store storage.Store, txn storage.Transaction, manifest Manifest) error {
return write(ctx, store, txn, legacyManifestStoragePath, manifest)
}
// LegacyEraseManifestFromStore will erase the bundle manifest from the older single (unnamed) bundle manifest location.
// Deprecated: Use WriteManifestToStore and named bundles instead.
func LegacyEraseManifestFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) error {
err := store.Write(ctx, txn, storage.RemoveOp, legacyManifestStoragePath, nil)
if err != nil {
return err
}
return nil
}
// LegacyReadRevisionFromStore will read the bundle manifest revision from the older single (unnamed) bundle manifest location.
// Deprecated: Use ReadBundleRevisionFromStore and named bundles instead.
func LegacyReadRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) (string, error) {
return readRevisionFromStore(ctx, store, txn, legacyRevisionStoragePath)
}
// ActivateLegacy calls Activate for the bundles but will also write their manifest to the older unnamed store location.
// Deprecated: Use Activate with named bundles instead.
func ActivateLegacy(opts *ActivateOpts) error {
opts.legacy = true
return activateBundles(opts)
}