Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add trafficpolicy package and conversion util #564

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading