Complete adapter tests. Check in a sample config.
diff --git a/Makefile b/Makefile
index 364ac8c..ef8654f 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,13 @@
test:
bazel test ... --test_output=all
+clean:
+ bazel clean
+
checkfmt:
(cd adapter; ../tools/checkfmt.sh)
(cd common; ../tools/checkfmt.sh)
+ (cd cmd/mockserver; ../../tools/checkfmt.sh)
(cd mock; ../tools/checkfmt.sh)
presubmit: checkfmt test
diff --git a/README.md b/README.md
index 86cfed4..851eb64 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,46 @@
# Istio Apigee Adapter
-This workspace holds an apigee adapter for Istio.
+This workspace holds an apigee adapter for Istio. It can be tested by itself, but in order
+to really use it you need a build of the Istio mixer that pulls it in. Instructions for that
+are forthcoming.
-## Installation
+## Building
-In order to use this, right now, you need a build of Istio
\ No newline at end of file
+You need Bazel, just like with Istio.
+
+ make
+
+Builds everything, and
+
+ make test
+
+Runs the tests.
+
+## Launching with the mixer
+
+Assuming that you have built a Mixer that includes it, you can launch the mixer as follows:
+
+First, edit testdata/global/adapters.yml to specify the organization and environment name for
+your Apigee adapter to use. If you deployed "edgemicro-auth" to a production org, then
+that is all you need. Otherwise, you can use "validationURL" to specify the base path
+to another proxy.
+
+Now, launch the Mixer binary:
+
+ $MIXER_HOME/bazel-bin/cmd/server/mixs --logtostderr --configStoreURL fs://$THIS_ROOT/testdata/configroot
+
+(In other words the "config store URL" needs to be the absolute path of the "testdata/configroot"
+directory of this repo. If you get this wrong then the mixer will silently do nothing
+and always return "OK!")
+
+Once that's running, here's a sample command:
+
+ $MIXER_HOME/bazel-bin/cmd/client/mixc check -a target.serivce=fault.svc.cluster.local \
+ --stringmap_attributes request.headers=apikey:SOME_VALID_API_KEY
+
+That will send a "check" RPC to the mixer, which will respond "OK" if and only if the
+API key is valid.
+
+You can also run "bazel-bin/cmd/mockserver," which will implement the same API as
+edgemicro-auth, but it will do it locally so that you can test easily. With the mock
+server, "12345" is a valid API key.
diff --git a/adapter/BUILD b/adapter/BUILD
index 0379f87..9e57dc2 100644
--- a/adapter/BUILD
+++ b/adapter/BUILD
@@ -11,15 +11,18 @@
],
deps = [
"//adapter/config:go_default_library",
+ "//common:go_default_library",
"//external:mixer_adapter",
"@com_github_hashicorp_go_multierror//:go_default_library",
],
)
go_test(
- name = "small_tests",
- size = "small",
+ name = "tests",
+ size = "medium",
srcs = ["apigeeKeyChecker_test.go"],
library = ":go_default_library",
- deps = [],
+ deps = [
+ "//mock:go_default_library",
+ ],
)
diff --git a/adapter/apigee.go b/adapter/apigee.go
index 8bf7f7d..c11818e 100644
--- a/adapter/apigee.go
+++ b/adapter/apigee.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package apigee
+package adapter
import (
"istio.io/mixer/pkg/adapter"
diff --git a/adapter/apigeeKeyChecker.go b/adapter/apigeeKeyChecker.go
index 52d5010..6785f6f 100644
--- a/adapter/apigeeKeyChecker.go
+++ b/adapter/apigeeKeyChecker.go
@@ -14,21 +14,24 @@
limitations under the License.
*/
-package apigee
+package adapter
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
+ "net/url"
"github.com/apid/istioApigeeAdapter/adapter/config"
+ "github.com/apid/istioApigeeAdapter/common"
"istio.io/mixer/pkg/adapter"
)
const (
- checkName = "apigeeKeyChecker"
- checkDesc = "Verify an API key from a parameter"
+ checkName = "apigeeKeyChecker"
+ checkDesc = "Verify an API key from a parameter"
+ verifyKeyPath = "/verifyApiKey"
)
var checkConf = &config.VerifyKeyParams{}
@@ -38,31 +41,51 @@
}
type keyChecker struct {
- keyParam string
- organization string
- environment string
+ env adapter.Env
+ checkURL string
}
-type APIKeyBody struct {
- APIKey string `json:"apiKey"`
-}
-
-func newKeyCheckBuilder() keyCheckBuilder {
+func newKeyCheckBuilder() adapter.ListsBuilder {
return keyCheckBuilder{
adapter.NewDefaultBuilder(checkName, checkDesc, checkConf),
}
}
-func (keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
- return newKeyChecker(c.(*config.VerifyKeyParams))
+func (b keyCheckBuilder) ValidateConfig(c adapter.Config) (ce *adapter.ConfigErrors) {
+ cfg := c.(*config.VerifyKeyParams)
+ if cfg.Organization == "" {
+ ce = ce.Appendf("organization", "Organization parameter must be specified")
+ }
+ if cfg.Environment == "" {
+ ce = ce.Appendf("environment", "Environment parameter must be specified")
+ }
+ if cfg.VerificationURL != "" {
+ _, err := url.Parse(cfg.VerificationURL)
+ if err != nil {
+ ce = ce.Appendf("verificationURL", "Invalid verification URL: %s", err)
+ }
+ }
+ return
}
-func newKeyChecker(c *config.VerifyKeyParams) (*keyChecker, error) {
- return &keyChecker{
- keyParam: c.KeyParameter,
- organization: c.Organization,
- environment: c.Environment,
- }, nil
+func (b keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
+ cfg := c.(*config.VerifyKeyParams)
+ var basePath string
+
+ if cfg.VerificationURL == "" {
+ basePath = fmt.Sprintf("https://%s-%s.apigee.net/edgemicro-auth",
+ cfg.Organization, cfg.Environment)
+ } else {
+ basePath = cfg.VerificationURL
+ }
+
+ kc := &keyChecker{
+ env: env,
+ checkURL: basePath + verifyKeyPath,
+ }
+ env.Logger().Infof("Created Apigee Key Checker to invoke \"%s\"", kc.checkURL)
+
+ return kc, nil
}
func (l *keyChecker) Close() error {
@@ -75,28 +98,22 @@
// Test command ./bazel-bin/cmd/client/mixc check -a target.service=f.default.svc.cluster.local --stringmap_attributes request.headers=x-api-key:1tu9pl04Srua2MtsAGtu6ViPxSYSSX2I
func (l *keyChecker) CheckList(symbol string) (bool, error) {
- fmt.Printf("*** Going to check \"%s\" against \"%s\"\n", symbol, l.organization)
-
- edge_url := "https://" + l.organization + "-" + l.environment + ".apigee.net/edgemicro-auth/verifyApiKey"
-
- fmt.Printf("*** edge_url \"%s\"\n", edge_url)
-
- return verifyApiKey(symbol, edge_url), nil
-}
-
-func verifyApiKey(apiKey string, uri string) bool {
- apiKeyBody := APIKeyBody{
- APIKey: apiKey,
+ apiKeyBody := common.VerifyAPIKeyRequest{
+ Key: symbol,
}
- serializedBody, _ := json.Marshal(&apiKeyBody)
- req, _ := http.NewRequest("POST", uri, bytes.NewBuffer(serializedBody))
- req.Header.Add("x-dna-api-key", apiKey)
- req.Header.Add("content-type", "application/json")
- client := &http.Client{}
- resp, _ := client.Do(req)
+ requestBody, _ := json.Marshal(&apiKeyBody)
+
+ resp, err := http.DefaultClient.Post(l.checkURL, "application/json",
+ bytes.NewBuffer(requestBody))
+ if err != nil {
+ l.env.Logger().Errorf("Error contacting verification service: %s", err)
+ return false, err
+ }
+ defer resp.Body.Close()
+
if resp.StatusCode != 200 {
- return false
+ return false, nil
} else {
- return true
+ return true, nil
}
}
diff --git a/adapter/apigeeKeyChecker_test.go b/adapter/apigeeKeyChecker_test.go
index 6f4862d..51d99ce 100644
--- a/adapter/apigeeKeyChecker_test.go
+++ b/adapter/apigeeKeyChecker_test.go
@@ -14,4 +14,124 @@
limitations under the License.
*/
-package apigee
+package adapter
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/apid/istioApigeeAdapter/adapter/config"
+ "github.com/apid/istioApigeeAdapter/mock"
+ "istio.io/mixer/pkg/adapter"
+)
+
+var mockServer *mock.MockServer
+var mockEnv adapter.Env
+
+func TestMain(m *testing.M) {
+ var err error
+ mockEnv = mock.NewMockEnvironment()
+ mockServer, err = mock.StartMockServer(0)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ result := m.Run()
+ mockServer.Stop()
+ os.Exit(result)
+}
+
+func TestMissingOrg(t *testing.T) {
+ cfg := &config.VerifyKeyParams{
+ Environment: "test",
+ }
+ builder := newKeyCheckBuilder()
+ ce := builder.ValidateConfig(cfg)
+ if ce == nil {
+ t.Fatal("Config should have returned an error")
+ }
+ fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestMissingEnv(t *testing.T) {
+ cfg := &config.VerifyKeyParams{
+ Organization: "test",
+ }
+ builder := newKeyCheckBuilder()
+ ce := builder.ValidateConfig(cfg)
+ if ce == nil {
+ t.Fatal("Config should have returned an error")
+ }
+ fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestInvalidURL(t *testing.T) {
+ cfg := &config.VerifyKeyParams{
+ Organization: "foo",
+ Environment: "test",
+ VerificationURL: ":",
+ }
+ builder := newKeyCheckBuilder()
+ ce := builder.ValidateConfig(cfg)
+ if ce == nil {
+ t.Fatal("Config should have returned an error")
+ }
+ fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestValidKey(t *testing.T) {
+ cfg := &config.VerifyKeyParams{
+ Organization: "foo",
+ Environment: "test",
+ VerificationURL: "http://" + mockServer.Address(),
+ }
+
+ builder := newKeyCheckBuilder()
+ ce := builder.ValidateConfig(cfg)
+ if ce != nil {
+ t.Fatalf("Error validating config: %s", ce)
+ }
+
+ aspect, err := builder.NewListsAspect(mockEnv, cfg)
+ if err != nil {
+ t.Fatalf("Error creating aspect: %s", err)
+ }
+ defer aspect.Close()
+
+ result, err := aspect.CheckList(mock.ValidAPIKey1)
+ if err != nil {
+ t.Fatalf("Error on list check: %s", err)
+ }
+ if !result {
+ t.Fatal("List check returned false")
+ }
+}
+
+func TestInvalidKey(t *testing.T) {
+ cfg := &config.VerifyKeyParams{
+ Organization: "foo",
+ Environment: "test",
+ VerificationURL: "http://" + mockServer.Address(),
+ }
+
+ builder := newKeyCheckBuilder()
+ ce := builder.ValidateConfig(cfg)
+ if ce != nil {
+ t.Fatalf("Error validating config: %s", ce)
+ }
+
+ aspect, err := builder.NewListsAspect(mockEnv, cfg)
+ if err != nil {
+ t.Fatalf("Error creating aspect: %s", err)
+ }
+ defer aspect.Close()
+
+ result, err := aspect.CheckList("99999")
+ if err != nil {
+ t.Fatalf("Error on list check: %s", err)
+ }
+ if result {
+ t.Fatal("List check returned true and should have failed")
+ }
+}
diff --git a/adapter/apigeeReport.go b/adapter/apigeeReport.go
index d9f9325..2500b57 100644
--- a/adapter/apigeeReport.go
+++ b/adapter/apigeeReport.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package apigee
+package adapter
import (
"bytes"
diff --git a/adapter/config/config.proto b/adapter/config/config.proto
index 48c7c6c..4d85bc7 100644
--- a/adapter/config/config.proto
+++ b/adapter/config/config.proto
@@ -1,28 +1,32 @@
-// Copyright 2017 Istio Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2017 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
syntax = "proto3";
package config;
message VerifyKeyParams {
- // The name of the parameter where the API key is stored.
- string key_parameter = 1;
-
- string organization = 2;
-
- string environment = 3;
+ // The name of the Apigee organization -- required
+ string organization = 1;
+ // The name of the Apigee environment -- required
+ string environment = 2;
+ // A URL to use to contact the verification service. Will be
+ // constructed from the organization and environment name if not
+ // specified, and assumes that the service runs at production "apigee.net".
+ string verificationURL = 3;
}
message ReportParams {
diff --git a/cmd/mockserver/BUILD b/cmd/mockserver/BUILD
new file mode 100644
index 0000000..0b55061
--- /dev/null
+++ b/cmd/mockserver/BUILD
@@ -0,0 +1,13 @@
+package(default_visibility = ["//visibility:public"])
+
+load("@io_bazel_rules_go//go:def.bzl", "go_binary")
+
+go_binary(
+ name = "mockserver",
+ srcs = [
+ "mockserver.go",
+ ],
+ deps = [
+ "//mock:go_default_library",
+ ],
+)
diff --git a/cmd/mockserver/mockserver.go b/cmd/mockserver/mockserver.go
new file mode 100644
index 0000000..c0c13f6
--- /dev/null
+++ b/cmd/mockserver/mockserver.go
@@ -0,0 +1,54 @@
+/*
+Copyright 2017 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/apid/istioApigeeAdapter/mock"
+)
+
+func main() {
+ var port int
+ var help bool
+
+ flag.IntVar(&port, "p", 0, "Port to listen on")
+ flag.BoolVar(&help, "h", false, "This help message right here")
+ flag.Parse()
+ if !flag.Parsed() || help {
+ flag.PrintDefaults()
+ os.Exit(2)
+ }
+
+ server, err := mock.StartMockServer(port)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
+ os.Exit(3)
+ }
+ fmt.Printf("Listening on %s\n", server.Address())
+
+ sc := make(chan os.Signal)
+ signal.Notify(sc, syscall.SIGTERM, syscall.SIGINT)
+
+ sig := <-sc
+ fmt.Printf("Exiting on signal %s\n", sig)
+ server.Stop()
+}
diff --git a/mock/BUILD b/mock/BUILD
index 4bf127c..4893bb3 100644
--- a/mock/BUILD
+++ b/mock/BUILD
@@ -6,14 +6,16 @@
name = "go_default_library",
srcs = [
"keys.go",
+ "mockenv.go",
"mockserver.go",
],
deps = [
"//common:go_default_library",
- "@com_github_julienschmidt_httprouter//:go_default_library",
+ "//external:mixer_adapter",
"@com_github_SermoDigital_jose//crypto:go_default_library",
"@com_github_SermoDigital_jose//jws:go_default_library",
"@com_github_SermoDigital_jose//jwt:go_default_library",
+ "@com_github_julienschmidt_httprouter//:go_default_library",
],
)
@@ -21,6 +23,7 @@
name = "small_tests",
size = "small",
srcs = [
+ "mockenv_test.go",
"mockserver_test.go",
],
library = ":go_default_library",
diff --git a/mock/mockenv.go b/mock/mockenv.go
new file mode 100644
index 0000000..1c9d176
--- /dev/null
+++ b/mock/mockenv.go
@@ -0,0 +1,73 @@
+/*
+Copyright 2017 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mock
+
+import (
+ "fmt"
+
+ "istio.io/mixer/pkg/adapter"
+)
+
+type mockEnvironment struct {
+ log *mockLogger
+}
+
+/*
+NewMockEnvironment returns an implementation of the Mixer adapter's "Env" class so that
+we can test without launching the whole thing.
+*/
+func NewMockEnvironment() adapter.Env {
+ return &mockEnvironment{
+ log: &mockLogger{},
+ }
+}
+
+func (e *mockEnvironment) Logger() adapter.Logger {
+ return e.log
+}
+
+func (e *mockEnvironment) ScheduleWork(fn adapter.WorkFunc) {
+ go func() {
+ fn()
+ }()
+}
+
+func (e *mockEnvironment) ScheduleDaemon(fn adapter.DaemonFunc) {
+ go func() {
+ fn()
+ }()
+}
+
+type mockLogger struct {
+}
+
+func (l *mockLogger) VerbosityLevel(level adapter.VerbosityLevel) bool {
+ return true
+}
+
+func (l *mockLogger) Infof(format string, args ...interface{}) {
+ fmt.Printf("INFO: "+format+"\n", args...)
+}
+
+func (l *mockLogger) Warningf(format string, args ...interface{}) {
+ fmt.Printf("WARN: "+format+"\n", args...)
+}
+
+func (l *mockLogger) Errorf(format string, args ...interface{}) error {
+ fmt.Printf("ERR: "+format+"\n", args...)
+ return nil
+}
diff --git a/mock/mockenv_test.go b/mock/mockenv_test.go
new file mode 100644
index 0000000..a312e1b
--- /dev/null
+++ b/mock/mockenv_test.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2017 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mock
+
+import (
+ "testing"
+)
+
+func TestMockWork(t *testing.T) {
+ env := NewMockEnvironment()
+
+ ch := make(chan bool)
+ env.Logger().Infof("Starting mock environment")
+
+ env.ScheduleWork(func() {
+ env.Logger().Infof("Did the work %d times", 1)
+ ch <- true
+ })
+
+ <-ch
+}
+
+func TestMockDaemon(t *testing.T) {
+ env := NewMockEnvironment()
+
+ ch := make(chan bool)
+ env.Logger().Infof("Starting mock environment")
+
+ env.ScheduleDaemon(func() {
+ env.Logger().Infof("Did the work %d times", 1)
+ ch <- true
+ })
+
+ <-ch
+}
diff --git a/testdata/configroot/scopes/global/adapters.yml b/testdata/configroot/scopes/global/adapters.yml
new file mode 100644
index 0000000..f4f2a68
--- /dev/null
+++ b/testdata/configroot/scopes/global/adapters.yml
@@ -0,0 +1,25 @@
+subject: global
+adapters:
+ - name: default
+ kind: quotas
+ impl: memQuota
+ params:
+ - name: default
+ impl: stdioLogger
+ params:
+ logStream: STDERR
+ - name: prometheus
+ kind: metrics
+ impl: prometheus
+ params:
+ - name: default
+ impl: denyChecker
+ - name: apigeeAPIKey
+ kind: lists
+ impl: apigeeKeyChecker
+ params:
+ organization: gregbrail
+ environment: prod
+ # Add verificationURL to use a different service or URL
+ #verificationURL: https://gregbrail-prod.apigee.net/edgemicro-auth
+
diff --git a/testdata/configroot/scopes/global/descriptors.yml b/testdata/configroot/scopes/global/descriptors.yml
new file mode 100644
index 0000000..341513e
--- /dev/null
+++ b/testdata/configroot/scopes/global/descriptors.yml
@@ -0,0 +1,191 @@
+subject: namespace:ns
+revision: "2022"
+manifests:
+ - name: kubernetes
+ revision: "1"
+ attributes:
+ source.ip:
+ valueType: IP_ADDRESS
+ source.labels:
+ valueType: STRING_MAP
+ source.name:
+ valueType: STRING
+ source.namespace:
+ valueType: STRING
+ source.service:
+ valueType: STRING
+ source.serviceAccount:
+ valueType: STRING
+ target.ip:
+ valueType: IP_ADDRESS
+ target.labels:
+ valueType: STRING_MAP
+ target.name:
+ valueType: STRING
+ target.namespace:
+ valueType: STRING
+ target.service:
+ valueType: STRING
+ target.serviceAccount:
+ valueType: STRING
+ - name: istio-proxy
+ revision: "1"
+ attributes:
+ origin.ip:
+ valueType: IP_ADDRESS
+ origin.uid:
+ valueType: STRING
+ origin.user:
+ valueType: STRING
+ request.headers:
+ valueType: STRING_MAP
+ request.id:
+ valueType: STRING
+ request.host:
+ valueType: STRING
+ request.method:
+ valueType: STRING
+ request.path:
+ valueType: STRING
+ request.reason:
+ valueType: STRING
+ request.referer:
+ valueType: STRING
+ request.scheme:
+ valueType: STRING
+ request.size:
+ valueType: INT64
+ request.time:
+ valueType: TIMESTAMP
+ request.useragent:
+ valueType: STRING
+ response.code:
+ valueType: INT64
+ response.duration:
+ valueType: DURATION
+ response.headers:
+ valueType: STRING_MAP
+ response.latency:
+ valueType: DURATION
+ response.size:
+ valueType: INT64
+ response.time:
+ valueType: TIMESTAMP
+ source.uid:
+ valueType: STRING
+ target.uid:
+ valueType: STRING
+ # DEPRECATED, to be removed. Use request.useragent instead.
+ request.user-agent:
+ valueType: STRING
+# Enums as struct fields can be symbolic names.
+# However enums inside maps *cannot* be symbolic names.
+metrics:
+ - name: request_count
+ kind: COUNTER
+ value: INT64
+ description: request count by source, target, service, and code
+ labels:
+ source: 1 # STRING
+ target: 1 # STRING
+ service: 1 # STRING
+ method: 1 # STRING
+ response_code: 2 # INT64
+ - name: request_duration
+ kind: DISTRIBUTION
+ value: DURATION
+ description: request duration by source, target, and service
+ buckets:
+ explicit_buckets:
+ bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+ # Examples of other possible bucket configurations:
+ # linear_buckets:
+ # num_finite_buckets: 10
+ # offset: 0.001
+ # width: 0.1
+ # exponential_buckets:
+ # num_finite_buckets: 15
+ # scale: 0.001
+ # growth_factor: 4
+ labels:
+ source: 1 # STRING
+ target: 1 # STRING
+ service: 1 # STRING
+ method: 1 # STRING
+ response_code: 2 # INT64
+ - name: request_size
+ kind: DISTRIBUTION
+ value: INT64
+ description: request size by source, target, and service
+ buckets:
+ exponentialBuckets:
+ numFiniteBuckets: 8
+ scale: 1
+ growthFactor: 10
+ # Examples of other possible bucket configurations:
+ # explicit_buckets:
+ # bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+ # linear_buckets:
+ # num_finite_buckets: 10
+ # offset: 0.001
+ # width: 0.1
+ labels:
+ source: 1 # STRING
+ target: 1 # STRING
+ service: 1 # STRING
+ method: 1 # STRING
+ response_code: 2 # INT64
+ - name: response_size
+ kind: DISTRIBUTION
+ value: INT64
+ description: response size by source, target, and service
+ buckets:
+ exponentialBuckets:
+ numFiniteBuckets: 8
+ scale: 1
+ growthFactor: 10
+ # Examples of other possible bucket configurations:
+ # explicitBuckets:
+ # bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+ # linearBuckets:
+ # numFiniteBuckets: 10
+ # offset: 0.001
+ # width: 0.1
+ labels:
+ source: 1 # STRING
+ target: 1 # STRING
+ service: 1 # STRING
+ method: 1 # STRING
+ response_code: 2 # INT64
+quotas:
+ - name: RequestCount
+ rate_limit: true
+logs:
+ - name: accesslog.common
+ display_name: Apache Common Log Format
+ payload_format: TEXT
+ log_template: '{{or (.originIp) "-"}} - {{or (.sourceUser) "-"}} [{{or (.timestamp.Format "02/Jan/2006:15:04:05 -0700") "-"}}] "{{or (.method) "-"}} {{or (.url) "-"}} {{or (.protocol) "-"}}" {{or (.responseCode) "-"}} {{or (.responseSize) "-"}}'
+ labels:
+ originIp: 6 # IP_ADDRESS
+ sourceUser: 1 # STRING
+ timestamp: 5 # TIMESTAMP
+ method: 1 # STRING
+ url: 1 # STRING
+ protocol: 1 # STRING
+ responseCode: 2 # INT64
+ responseSize: 2 # INT64
+ - name: accesslog.combined
+ display_name: Apache Combined Log Format
+ payload_format: TEXT
+ log_template: '{{or (.originIp) "-"}} - {{or (.sourceUser) "-"}} [{{or (.timestamp.Format "02/Jan/2006:15:04:05 -0700") "-"}}] "{{or (.method) "-"}} {{or (.url) "-"}} {{or (.protocol) "-"}}" {{or (.responseCode) "-"}} {{or (.responseSize) "-"}} {{or (.referer) "-"}} {{or (.userAgent) "-"}}'
+ labels:
+ originIp: 6 # IP_ADDRESS
+ sourceUser: 1 # STRING
+ timestamp: 5 # TIMESTAMP
+ method: 1 # STRING
+ url: 1 # STRING
+ protocol: 1 # STRING
+ responseCode: 2 # INT64
+ responseSize: 2 # INT64
+ referer: 1 # STRING
+ userAgent: 1 # STRING
diff --git a/testdata/configroot/scopes/global/subjects/global/rules.yml b/testdata/configroot/scopes/global/subjects/global/rules.yml
new file mode 100644
index 0000000..f38981a
--- /dev/null
+++ b/testdata/configroot/scopes/global/subjects/global/rules.yml
@@ -0,0 +1,59 @@
+subject: namespace:ns
+revision: "2022"
+rules:
+- selector: # must be empty for preprocessing adapters
+ aspects:
+ - kind: lists
+ adapter: apigeeAPIKey
+ params:
+ checkExpression: request.headers["apikey"]
+ - kind: quotas
+ params:
+ quotas:
+ - descriptorName: RequestCount
+ maxAmount: 5000
+ expiration: 1s
+ - kind: metrics
+ adapter: prometheus
+ params:
+ metrics:
+ - descriptor_name: request_count
+ # we want to increment this counter by 1 for each unique (source, target, service, method, response_code) tuple
+ value: "1"
+ labels:
+ source: source.labels["app"] | "unknown"
+ target: target.service | "unknown"
+ service: target.labels["app"] | "unknown"
+ method: request.path | "unknown"
+ response_code: response.code | 200
+ - descriptor_name: request_duration
+ value: response.latency | response.duration | "0ms"
+ labels:
+ source: source.labels["app"] | "unknown"
+ target: target.service | "unknown"
+ service: target.labels["app"] | "unknown"
+ method: request.path | "unknown"
+ response_code: response.code | 200
+ - kind: access-logs
+ params:
+ logName: access_log
+ log:
+ descriptor_name: accesslog.common
+ template_expressions:
+ originIp: origin.ip
+ sourceUser: origin.user
+ timestamp: request.time
+ method: request.method
+ url: request.path
+ protocol: request.scheme
+ responseCode: response.code
+ responseSize: response.size
+ labels:
+ originIp: origin.ip
+ sourceUser: origin.user
+ timestamp: request.time
+ method: request.method
+ url: request.path
+ protocol: request.scheme
+ responseCode: response.code
+ responseSize: response.size