Skip to content

Commit

Permalink
feat: Add trafficpolicy package and conversion util (#564)
Browse files Browse the repository at this point in the history
* feat: Add trafficpolicy package and conversion util

* fix: typo
  • Loading branch information
jonstacks authored Jan 9, 2025
1 parent 5e5c0b8 commit cf90f52
Show file tree
Hide file tree
Showing 8 changed files with 908 additions and 0 deletions.
12 changes: 12 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,15 @@ func (e ErrInvalidConfiguration) Error() string {
func (e ErrInvalidConfiguration) Unwrap() error {
return e.cause
}

type ErrModulesetNotConvertibleToTrafficPolicy struct {
message string
}

func NewErrModulesetNotConvertibleToTrafficPolicy(message string) error {
return ErrModulesetNotConvertibleToTrafficPolicy{message: message}
}

func (e ErrModulesetNotConvertibleToTrafficPolicy) Error() string {
return fmt.Sprintf("moduleset not convertible to traffic policy: %s", e.message)
}
7 changes: 7 additions & 0 deletions internal/secrets/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package secrets

import "context"

type Resolver interface {
GetSecret(ctx context.Context, namespace, name, key string) (string, error)
}
199 changes: 199 additions & 0 deletions internal/trafficpolicy/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package trafficpolicy

import "time"

// ActionType is a type of action that can be taken. Ref: https://ngrok.com/docs/traffic-policy/actions/
type ActionType string

// Expression is a string that represents a traffic policy expression.
type Expression string

const (
ActionType_AddHeaders ActionType = "add-headers"
ActionType_BasicAuth ActionType = "basic-auth"
ActionType_CircuitBreaker ActionType = "circuit-breaker"
ActionType_CompressResponse ActionType = "compress-response"
ActionType_CustomResponse ActionType = "custom-response"
ActionType_Deny ActionType = "deny"
ActionType_ForwardInternal ActionType = "forward-internal"
ActionType_JWTValidation ActionType = "jwt-validation"
ActionType_Log ActionType = "log"
ActionType_RateLimit ActionType = "rate-limit"
ActionType_Redirect ActionType = "redirect"
ActionType_RemoveHeaders ActionType = "remove-headers"
ActionType_RestrictIPs ActionType = "restrict-ips"
ActionType_TerminateTLS ActionType = "terminate-tls"
ActionType_URLRewrite ActionType = "url-rewrite"
ActionType_VerifyWebhook ActionType = "verify-webhook"
)

// TrafficPolicy is the configuration language for handling traffic received by ngrok
// for Edges and Endpoints. Specifically, it allows you to define rules for each
// phase in a connections lifecycle (on_http_request, on_http_response, and tcp_connect). These
// rules containin expressessions that match traffc and actions to take when those expressions match.
//
// Ref: https://ngrok.com/docs/traffic-policy/
type TrafficPolicy struct {
OnHTTPRequest []Rule `json:"on_http_request,omitempty"`
OnHTTPResponse []Rule `json:"on_http_response,omitempty"`
OnTCPConnect []Rule `json:"on_tcp_connect,omitempty"`
}

// NewTrafficPolicy creates a new TrafficPolicy with empty rules.
func NewTrafficPolicy() *TrafficPolicy {
return &TrafficPolicy{
OnHTTPRequest: []Rule{},
OnHTTPResponse: []Rule{},
OnTCPConnect: []Rule{},
}
}

// AddRuleOnHTTPRequest adds a rule to the OnHTTPRequest phase of the TrafficPolicy.
func (tp *TrafficPolicy) AddRuleOnHTTPRequest(rule Rule) {
tp.OnHTTPRequest = append(tp.OnHTTPRequest, rule)
}

// AddRuleOnHTTPResponse adds a rule to the OnHTTPResponse phase of the TrafficPolicy.
func (tp *TrafficPolicy) AddRuleOnHTTPResponse(rule Rule) {
tp.OnHTTPResponse = append(tp.OnHTTPResponse, rule)
}

// AddRuleOnTCPConnect adds a rule to the OnTCPConnect phase of the TrafficPolicy.
func (tp *TrafficPolicy) AddRuleOnTCPConnect(rule Rule) {
tp.OnTCPConnect = append(tp.OnTCPConnect, rule)
}

// IsEmpty returns true if the TrafficPolicy has no rules.
func (tp TrafficPolicy) IsEmpty() bool {
return len(tp.OnHTTPRequest) == 0 &&
len(tp.OnHTTPResponse) == 0 &&
len(tp.OnTCPConnect) == 0
}

// A Rule allows you to define how traffic is filtered and processed within a phase. Rules
// consist of expressions and actions. Ref: https://ngrok.com/docs/traffic-policy/concepts/phase-rules/
type Rule struct {
Expressions []string `json:"expressions,omitempty"`
Actions []Action `json:"actions"`
}

// An action allows you to manipulate, route, or manage traffic on an endpoint.
// Ref: https://ngrok.com/docs/traffic-policy/actions/
type Action struct {
Type ActionType `json:"type"`
Config any `json:"config"`
}

// NewAddHeadersAction creates a new action that adds headers to the request(OnHTTPRequest phase) or
// response(OnHTTPResponse phase).
func NewAddHeadersAction(headers map[string]string) Action {
config := struct {
Headers map[string]string `json:"headers"`
}{
Headers: headers,
}

return Action{
Type: ActionType_AddHeaders,
Config: config,
}
}

// NewRemoveHeadersAction creates a new action that removes headers from the request(OnHTTPRequest phase) or
// response(OnHTTPResponse phase).
func NewRemoveHeadersAction(headers []string) Action {
config := struct {
Headers []string `json:"headers"`
}{
Headers: headers,
}

return Action{
Type: ActionType_RemoveHeaders,
Config: config,
}
}

// NewCompressResponseAction creates a new action that compresses the response. Can only be used
// in the OnHTTPResponse phase.
func NewCompressResponseAction(algorithms []string) Action {
config := struct {
Algorithms []string `json:"algorithms,omitempty"`
}{
Algorithms: algorithms,
}

return Action{
Type: ActionType_CompressResponse,
Config: config,
}
}

// NewCicuitBreakerAction creates a new action that rejects requests when the error rate and request volume within a rolling
// window exceeds defined thresholds. Can only be used in the OnHTTPRequest phase.
func NewCircuitBreakerAction(errorThreshold float64, volumeThreshold *uint32, windowDuration *time.Duration, trippedDuration *time.Duration) Action {
config := struct {
ErrorThreshold float64 `json:"error_threshold"`
VolumeThreshold *uint32 `json:"volume_threshold,omitempty"`
WindowDuration *time.Duration `json:"window_duration,omitempty"`
TrippedDuration *time.Duration `json:"tripped_duration,omitempty"`
}{
ErrorThreshold: errorThreshold,
VolumeThreshold: volumeThreshold,
WindowDuration: windowDuration,
TrippedDuration: trippedDuration,
}
return Action{
Type: ActionType_CircuitBreaker,
Config: config,
}
}

// NewRestrictIPsActionFromIPPolicies creates a new action that restricts access to a set of IP policies.
// Supported on OnHTTPRequest, OnTCPConnect, and OnHTTPResponse phases.
func NewRestricIPsActionFromIPPolicies(policies []string) Action {
config := struct {
IPPolicies []string `json:"ip_policies"`
}{
IPPolicies: policies,
}

return Action{
Type: ActionType_RestrictIPs,
Config: config,
}
}

// TLSTerminationConfig is the configuration for terminating TLS on an endpoint.
type TLSTerminationConfig struct {
MinVersion *string `json:"min_version,omitempty"`
MaxVersion *string `json:"max_version,omitempty"`
ServerPrivateKey *string `json:"server_private_key,omitempty"`
ServerCertificate *string `json:"server_certificate,omitempty"`
MutualTLSCertificateAuthorities []string `json:"mutual_tls_certificate_authorities,omitempty"`
MutualTLSVerificationStrategy *string `json:"mutual_tls_verification_strategy,omitempty"`
}

// NewTerminateTLSAction creates a new action that configures how TLS is terminated on the endpoint.
func NewTerminateTLSAction(config TLSTerminationConfig) Action {
return Action{
Type: ActionType_TerminateTLS,
Config: config,
}
}

// NewWebhookVerificationAction creates a new action that verifies a webhook request.
func NewWebhookVerificationAction(provider, secret string) Action {
config := struct {
Provider string `json:"provider"`
Secret string `json:"secret"`
}{
Provider: provider,
Secret: secret,
}

return Action{
Type: ActionType_VerifyWebhook,
Config: config,
}
}
83 changes: 83 additions & 0 deletions internal/trafficpolicy/policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package trafficpolicy

import (
"encoding/json"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"k8s.io/utils/ptr"
)

func assertTrafficPolicyContent(t *testing.T, tp *TrafficPolicy, expected string) {
content, err := json.Marshal(tp)
assert.NoError(t, err)
assert.JSONEq(t, expected, string(content))
}

func loadTestData(name string) string {
data, err := os.ReadFile("testdata/" + name)
if err != nil {
panic(err)
}
return string(data)
}

func TestEmptyTrafficPolicy(t *testing.T) {
tp := NewTrafficPolicy()
assert.True(t, tp.IsEmpty())
assertTrafficPolicyContent(t, tp, `{}`)
}

func TestTrafficPolicy(t *testing.T) {
tp := NewTrafficPolicy()
if tp == nil {
t.Error("TrafficPolicy is nil")
}

tp.AddRuleOnHTTPRequest(
Rule{
Actions: []Action{
NewWebhookVerificationAction("github", "secret"),
},
},
)
assertTrafficPolicyContent(t, tp, loadTestData("policy-1.json"))

tp = NewTrafficPolicy()
tp.AddRuleOnTCPConnect(
Rule{
Expressions: []string{"[1,2,3].all(x, x > 0)"},
Actions: []Action{
NewRestricIPsActionFromIPPolicies([]string{"ipp_123", "ipp_456"}),
NewTerminateTLSAction(TLSTerminationConfig{MinVersion: ptr.To("1.2")}),
},
},
)
tp.AddRuleOnHTTPRequest(
Rule{
Actions: []Action{
NewCircuitBreakerAction(0.10, nil, nil, ptr.To(2*time.Minute)),
},
},
)

tp.AddRuleOnHTTPResponse(
Rule{
Actions: []Action{
NewAddHeadersAction(map[string]string{
"X-Header-1": "value1",
"X-Header-2": "value2",
}),
NewRemoveHeadersAction([]string{
"X-Header-3",
"X-Header-4",
}),
NewCompressResponseAction(nil),
},
},
)

assertTrafficPolicyContent(t, tp, loadTestData("policy-2.json"))
}
15 changes: 15 additions & 0 deletions internal/trafficpolicy/testdata/policy-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"on_http_request": [
{
"actions": [
{
"type":"verify-webhook",
"config": {
"provider": "github",
"secret": "secret"
}
}
]
}
]
}
67 changes: 67 additions & 0 deletions internal/trafficpolicy/testdata/policy-2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"on_tcp_connect": [
{
"expressions": [
"[1,2,3].all(x, x > 0)"
],
"actions": [
{
"type": "restrict-ips",
"config": {
"ip_policies": [
"ipp_123",
"ipp_456"
]
}
},
{
"type": "terminate-tls",
"config": {
"min_version": "1.2"
}
}
]
}
],
"on_http_request": [
{
"actions": [
{
"type": "circuit-breaker",
"config": {
"error_threshold": 0.1,
"tripped_duration": 120000000000
}
}
]
}
],
"on_http_response": [
{
"actions": [
{
"type": "add-headers",
"config": {
"headers": {
"X-Header-1": "value1",
"X-Header-2": "value2"
}
}
},
{
"type": "remove-headers",
"config": {
"headers": [
"X-Header-3",
"X-Header-4"
]
}
},
{
"type": "compress-response",
"config": {}
}
]
}
]
}
Loading

0 comments on commit cf90f52

Please sign in to comment.