mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
afb30d3f9d
Brace yourselves! For there are many touched files here. No changes in semantics however. Spent a long time trying out the various optional rules gocritic provides, and settled for a few of them. There are more I really like, but that would take many hours to address across the codebase. Perhaps others find gocritic too pedantic? If so, we can merge the fixes without enabling the rule. Signed-off-by: Anders Eknert <anders@styra.com>
494 lines
13 KiB
Go
494 lines
13 KiB
Go
package test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/internal/file/archive"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/compile"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// MockBundle sets a bundle named file on the test server containing the given
|
|
// policies.
|
|
func MockBundle(file string, policies map[string]string) func(*Server) error {
|
|
return func(s *Server) error {
|
|
if !strings.HasPrefix(file, "/bundles/") {
|
|
return fmt.Errorf("mock bundle filename must be prefixed with '/bundles/ but got %q", file)
|
|
}
|
|
s.bundles[file] = policies
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MockOCIBundle prepares the server to allow serving "/v2" OCI responses from the supplied policies
|
|
// Ref parameter must be in the form of <registry>/<org>/<repo>:<tag> that will be used in detecting future calls
|
|
func MockOCIBundle(ref string, policies map[string]string) func(*Server) error {
|
|
return func(s *Server) error {
|
|
if !strings.Contains(ref, "/") {
|
|
return fmt.Errorf("mock oci bundle ref must contain 'org/repo' but got %q", ref)
|
|
}
|
|
return s.buildBundles(ref, policies)
|
|
}
|
|
}
|
|
|
|
// Ready provides a channel that the server will use to gate readiness. The
|
|
// caller can provide this channel to prevent the server from becoming ready.
|
|
// The server will response with HTTP 500 responses until ready. The caller
|
|
// should close the channel to indicate readiness.
|
|
func Ready(ch chan struct{}) func(*Server) error {
|
|
return func(s *Server) error {
|
|
s.ready = ch
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ParserOptions sets the ast.ParserOptions to use when parsing modules when preparing bundles.
|
|
func ParserOptions(popts ast.ParserOptions) func(*Server) error {
|
|
return func(s *Server) error {
|
|
s.parserOptions = popts
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Server provides a mock HTTP server for testing the SDK and integrations.
|
|
type Server struct {
|
|
server *httptest.Server
|
|
ready chan struct{}
|
|
bundles map[string]map[string]string
|
|
rawBundles bool
|
|
parserOptions ast.ParserOptions
|
|
}
|
|
|
|
// MustNewServer returns a new Server for test purposes or panics if an error occurs.
|
|
func MustNewServer(opts ...func(*Server) error) *Server {
|
|
s, err := NewServer(opts...)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// NewServer returns a new Server for test purposes.
|
|
func NewServer(opts ...func(*Server) error) (*Server, error) {
|
|
s := &Server{
|
|
bundles: map[string]map[string]string{},
|
|
}
|
|
for _, opt := range opts {
|
|
if err := opt(s); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.ready == nil {
|
|
s.ready = make(chan struct{})
|
|
close(s.ready)
|
|
}
|
|
s.server = httptest.NewServer(http.HandlerFunc(s.handle))
|
|
return s, nil
|
|
}
|
|
|
|
func RawBundles(raw bool) func(*Server) error {
|
|
return func(s *Server) error {
|
|
s.rawBundles = raw
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *Server) ParserOptions() ast.ParserOptions {
|
|
return s.parserOptions
|
|
}
|
|
|
|
// WithTestBundle adds a bundle to the server at the specified endpoint.
|
|
func (s *Server) WithTestBundle(endpoint string, policies map[string]string) *Server {
|
|
s.bundles[endpoint] = policies
|
|
return s
|
|
}
|
|
|
|
// Stop stops the test server.
|
|
func (s *Server) Stop() {
|
|
s.server.Close()
|
|
}
|
|
|
|
// URL returns the base URL of the server.
|
|
func (s *Server) URL() string {
|
|
return s.server.URL
|
|
}
|
|
|
|
// Builds the tarball from the supplied policies and prepares the layers in a temporary directory
|
|
func (s *Server) buildBundles(ref string, policies map[string]string) error {
|
|
// Prepare the modules to include in the bundle. Sort them so bundles are deterministic.
|
|
modules := make([]bundle.ModuleFile, 0, len(policies))
|
|
for url, str := range policies {
|
|
module, err := ast.ParseModuleWithOpts(url, str, s.parserOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse module: %v", err)
|
|
}
|
|
modules = append(modules, bundle.ModuleFile{
|
|
URL: url,
|
|
Parsed: module,
|
|
})
|
|
}
|
|
sort.Slice(modules, func(i, j int) bool {
|
|
return modules[i].URL < modules[j].URL
|
|
})
|
|
|
|
// Compile the bundle out into a buffer
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
// We need to explicitly set the global bundle rego-version, as an unassigned version will be
|
|
// interpreted as v0 on the receiving end, which will cause problems if modules are parsed/compiled
|
|
// as v1 on this end, which will drop 'rego.v1' and 'future.keywords' imports.
|
|
bundleManifest := bundle.Manifest{}
|
|
bundleManifest.SetRegoVersion(ast.DefaultRegoVersion)
|
|
bundleManifest.Init()
|
|
|
|
err := compile.New().WithOutput(buf).WithBundle(&bundle.Bundle{
|
|
Data: map[string]interface{}{},
|
|
Modules: modules,
|
|
Manifest: bundleManifest,
|
|
}).Build(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
directoryName, err := os.MkdirTemp("", "oci-test-temp")
|
|
fmt.Println("Testing OCI temporary directory:", directoryName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Write buf tarball to layer
|
|
tarLayer := filepath.Join(directoryName, "tar.layer")
|
|
err = os.WriteFile(tarLayer, buf.Bytes(), 0655)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Write empty config layer
|
|
configLayer := filepath.Join(directoryName, "config.layer")
|
|
err = os.WriteFile(configLayer, []byte("{}"), 0655)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Calculate SHA and size and prepare manifest layer
|
|
tarSHA, err := getFileSHA(tarLayer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configSHA, err := getFileSHA(configLayer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var manifest ocispec.Manifest
|
|
manifest.SchemaVersion = 2
|
|
manifest.Config = ocispec.Descriptor{
|
|
MediaType: ocispec.MediaTypeImageConfig,
|
|
Digest: digest.Digest(fmt.Sprintf("sha256:%x", configSHA)),
|
|
Size: int64(2), // config size is set to 2 as an empty config is used
|
|
}
|
|
manifest.Layers = []ocispec.Descriptor{
|
|
{
|
|
MediaType: ocispec.MediaTypeImageLayerGzip,
|
|
Digest: digest.Digest(fmt.Sprintf("sha256:%x", tarSHA)),
|
|
Size: int64(buf.Len()),
|
|
Annotations: map[string]string{
|
|
ocispec.AnnotationTitle: ref,
|
|
ocispec.AnnotationCreated: time.Now().Format(time.RFC3339),
|
|
},
|
|
},
|
|
}
|
|
|
|
manifestData, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
manifestLayer := filepath.Join(directoryName, "manifest.layer")
|
|
err = os.WriteFile(manifestLayer, manifestData, 0655)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set ref layer paths to server bundles
|
|
s.bundles[ref] = map[string]string{
|
|
"manifest": manifestLayer,
|
|
"config": configLayer,
|
|
"tar": tarLayer,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getFileSHA(filePath string) ([]byte, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, f); err != nil {
|
|
return nil, err
|
|
}
|
|
return hash.Sum(nil), nil
|
|
}
|
|
|
|
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
select {
|
|
case <-s.ready:
|
|
default:
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/v2") {
|
|
s.handleOCIBundles(w, r)
|
|
return
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/bundles") {
|
|
if s.rawBundles {
|
|
s.handleRawBundles(w, r)
|
|
} else {
|
|
s.handleBundles(w, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
|
|
func (s *Server) handleOCIBundles(w http.ResponseWriter, r *http.Request) {
|
|
ref := "" // key used to detect layers from s.bundles
|
|
tag := "" // image tag used in request path verification
|
|
repo := "" // image repo used in request path verification
|
|
var buf bytes.Buffer
|
|
// get first key that matches request url pattern
|
|
for key := range s.bundles {
|
|
// extract tag
|
|
parsedRef := strings.Split(key, ":")
|
|
checkRef := strings.Split(parsedRef[0], "/")
|
|
// check if request path contains org and repository
|
|
if strings.Contains(r.URL.Path, checkRef[1]) && strings.Contains(r.URL.Path, checkRef[2]) {
|
|
ref = key
|
|
tag = parsedRef[1]
|
|
repo = checkRef[1] + "/" + checkRef[2]
|
|
break
|
|
}
|
|
}
|
|
if ref == "" || tag == "" || repo == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
layers := s.bundles[ref]
|
|
fi, err := os.Stat(layers["manifest"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
manifestSize := fi.Size()
|
|
manifestSHA, err := getFileSHA(layers["manifest"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
fi, err = os.Stat(layers["config"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
configSize := fi.Size()
|
|
configSHA, err := getFileSHA(layers["config"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
fi, err = os.Stat(layers["tar"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
// get the size
|
|
tarSize := fi.Size()
|
|
tarSHA, err := getFileSHA(layers["tar"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == fmt.Sprintf("/v2/%s/manifests/%s", repo, tag) {
|
|
w.Header().Add("Content-Length", strconv.FormatInt(manifestSize, 10))
|
|
w.Header().Add("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
w.Header().Add("Docker-Content-Digest", fmt.Sprintf("sha256:%x", manifestSHA))
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
if r.URL.Path == fmt.Sprintf("/v2/%s/manifests/sha256:%x", repo, manifestSHA) {
|
|
w.Header().Add("Content-Length", strconv.FormatInt(manifestSize, 10))
|
|
w.Header().Add("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
w.Header().Add("Docker-Content-Digest", fmt.Sprintf("sha256:%x", manifestSHA))
|
|
w.WriteHeader(200)
|
|
bs, err := os.ReadFile(layers["manifest"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
buf.Write(bs)
|
|
_, err = w.Write(buf.Bytes())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
if r.URL.Path == fmt.Sprintf("/v2/%s/blobs/sha256:%x", repo, configSHA) {
|
|
w.Header().Add("Content-Length", strconv.FormatInt(configSize, 10))
|
|
w.Header().Add("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
w.Header().Add("Docker-Content-Digest", fmt.Sprintf("sha256:%x", configSHA))
|
|
w.WriteHeader(200)
|
|
bs, err := os.ReadFile(layers["config"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
buf.Write(bs)
|
|
_, err = w.Write(buf.Bytes())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
if r.URL.Path == fmt.Sprintf("/v2/%s/blobs/sha256:%x", repo, tarSHA) {
|
|
w.Header().Add("Content-Length", strconv.FormatInt(tarSize, 10))
|
|
w.Header().Add("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
w.Header().Add("Docker-Content-Digest", fmt.Sprintf("sha256:%x", tarSHA))
|
|
w.WriteHeader(200)
|
|
bs, err := os.ReadFile(layers["tar"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
buf.Write(bs)
|
|
_, err = w.Write(buf.Bytes())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleBundles(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Return 404 if bundle path does not exist.
|
|
b, ok := s.bundles[r.URL.Path]
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Prepare a mapping to store bundle data
|
|
data := map[string]interface{}{}
|
|
|
|
// Prepare a manifest for use if a .manifest file exists.
|
|
var manifest bundle.Manifest
|
|
|
|
// Prepare the modules to include in the bundle. Sort them so bundles are deterministic.
|
|
modules := make([]bundle.ModuleFile, 0, len(b))
|
|
for url, str := range b {
|
|
switch {
|
|
case url == ".manifest":
|
|
err := json.Unmarshal([]byte(str), &manifest)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "error unmarshaling .manifest file: %v", err)
|
|
return
|
|
}
|
|
case strings.HasSuffix(url, ".rego"):
|
|
module, err := ast.ParseModuleWithOpts(url, str, s.parserOptions)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
modules = append(modules, bundle.ModuleFile{
|
|
URL: url,
|
|
Parsed: module,
|
|
})
|
|
case strings.HasSuffix(url, ".json"):
|
|
if strings.Contains(url, "/") {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "nested data documents are not implemented in the dummy server: %s", url)
|
|
return
|
|
}
|
|
|
|
var d map[string]interface{}
|
|
|
|
err := json.Unmarshal([]byte(str), &d)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "error unmarshaling json file: %v", err)
|
|
return
|
|
}
|
|
|
|
for k, v := range d {
|
|
data[k] = v
|
|
}
|
|
default:
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "unexpected file in dummy bundle: %s", url)
|
|
return
|
|
}
|
|
}
|
|
sort.Slice(modules, func(i, j int) bool {
|
|
return modules[i].URL < modules[j].URL
|
|
})
|
|
|
|
// Compile the bundle out into a buffer
|
|
buf := bytes.NewBuffer(nil)
|
|
err := compile.New().WithOutput(buf).WithBundle(&bundle.Bundle{
|
|
Data: data,
|
|
Modules: modules,
|
|
Manifest: manifest,
|
|
}).Build(r.Context())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Write out the bundle
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = io.Copy(w, buf)
|
|
}
|
|
|
|
func (s *Server) handleRawBundles(w http.ResponseWriter, r *http.Request) {
|
|
// Return 404 if bundle path does not exist.
|
|
b, ok := s.bundles[r.URL.Path]
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
files := make([][2]string, 0, len(b))
|
|
for url, str := range b {
|
|
files = append(files, [2]string{url, str})
|
|
}
|
|
buf := archive.MustWriteTarGz(files)
|
|
|
|
// Write out the bundle
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = io.Copy(w, buf)
|
|
}
|