Files
releases/bundle/bundle.go
T
Torin Sandall 43ce86f55b Add manifest to bundle files
These chanages introduce a manifest to the bundle files. This way bundle
services can include metadata about the bundle in the file. The initial
use case for bundle data is keeping track of bundle revisions.

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
2018-04-09 11:05:06 -07:00

234 lines
5.0 KiB
Go

// Copyright 2018 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 implements bundle loading.
package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/util"
"github.com/pkg/errors"
)
// Common file extensions and file names.
const (
RegoExt = ".rego"
JSONExt = ".json"
ManifestExt = ".manifest"
DataFileExt = "/data.json"
)
// Bundle represents a loaded bundle. The bundle can contain data and policies.
type Bundle struct {
Manifest Manifest
Data map[string]interface{}
Modules []ModuleFile
}
// Manifest represents the manifest from a bundle. The manifest may contain
// metadata such as the bundle revision.
type Manifest struct {
Revision string `json:"revision"`
}
// ModuleFile represents a single module contained a bundle.
type ModuleFile struct {
Path string
Raw []byte
Parsed *ast.Module
}
// Write serializes the Bundle and writes it to w.
func Write(w io.Writer, bundle Bundle) error {
gw := gzip.NewWriter(w)
tw := tar.NewWriter(gw)
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bundle.Data); err != nil {
return err
}
if err := writeFile(tw, "data.json", buf.Bytes()); err != nil {
return err
}
for _, module := range bundle.Modules {
if err := writeFile(tw, module.Path, module.Raw); err != nil {
return err
}
}
if err := writeManifest(tw, bundle); err != nil {
return err
}
if err := tw.Close(); err != nil {
return err
}
return gw.Close()
}
func writeManifest(tw *tar.Writer, bundle Bundle) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bundle.Manifest); err != nil {
return err
}
return writeFile(tw, ManifestExt, buf.Bytes())
}
// Read returns a new Bundle loaded from the reader.
func Read(r io.Reader) (Bundle, error) {
var bundle Bundle
bundle.Data = map[string]interface{}{}
gr, err := gzip.NewReader(r)
if err != nil {
return bundle, errors.Wrap(err, "bundle read failed")
}
tr := tar.NewReader(gr)
for {
header, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return bundle, errors.Wrap(err, "bundle read failed")
}
if header.Typeflag != tar.TypeReg {
continue
}
var buf bytes.Buffer
io.Copy(&buf, tr)
path := header.Name
if strings.HasSuffix(path, RegoExt) {
module, err := ast.ParseModule(path, buf.String())
if err != nil {
return bundle, errors.Wrap(err, "bundle load failed")
}
file := ModuleFile{
Path: path,
Raw: buf.Bytes(),
Parsed: module,
}
bundle.Modules = append(bundle.Modules, file)
} else if strings.HasSuffix(path, DataFileExt) {
var value interface{}
if err := util.NewJSONDecoder(&buf).Decode(&value); err != nil {
return bundle, errors.Wrapf(err, "bundle load failed on %v", path)
}
dirpath := strings.Trim(strings.TrimSuffix(path, DataFileExt), "/")
var key []string
if dirpath != "" {
key = strings.Split(dirpath, "/")
}
if err := bundle.insert(key, value); err != nil {
return bundle, errors.Wrapf(err, "bundle load failed on %v", path)
}
} else if strings.HasSuffix(path, ManifestExt) {
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
return bundle, errors.Wrapf(err, "bundle load failed on manifest")
}
}
}
return bundle, nil
}
// Equal returns true if this bundle's contents equal the other bundle's
// contents.
func (b Bundle) Equal(other Bundle) bool {
if !reflect.DeepEqual(b.Data, other.Data) {
return false
}
if len(b.Modules) != len(other.Modules) {
return false
}
for i := range b.Modules {
if b.Modules[i].Path != other.Modules[i].Path {
return false
}
if !b.Modules[i].Parsed.Equal(other.Modules[i].Parsed) {
return false
}
if !bytes.Equal(b.Modules[i].Raw, other.Modules[i].Raw) {
return false
}
}
return true
}
func (b *Bundle) insert(key []string, value interface{}) error {
if len(key) == 0 {
obj, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("root value must be object")
}
b.Data = obj
return nil
}
obj, err := b.mkdir(key[:len(key)-1])
if err != nil {
return err
}
obj[key[len(key)-1]] = value
return nil
}
func (b *Bundle) mkdir(key []string) (map[string]interface{}, error) {
obj := b.Data
for i := 0; i < len(key); i++ {
node, ok := obj[key[i]]
if !ok {
node = map[string]interface{}{}
obj[key[i]] = node
}
obj, ok = node.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("non-leaf value must be object")
}
}
return obj, nil
}
func writeFile(tw *tar.Writer, path string, bs []byte) error {
hdr := &tar.Header{
Name: "/" + strings.TrimLeft(path, "/"),
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len(bs)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err := tw.Write(bs)
return err
}