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

Add CORS policy to config-contour #1069

Merged
merged 13 commits into from
Apr 10, 2024
16 changes: 16 additions & 0 deletions config/config-contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ data:
ClusterLocal:
class: contour-internal
service: contour-internal/envoy
# corsPolicy contains the configuration to set CORS policy for HTTPProxies.
corsPolicy: |
allowCredentials: true
allowOrigin:
- example.com
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
48 changes: 47 additions & 1 deletion pkg/reconciler/contour/config/contour.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package config

import (
"fmt"
"regexp"
"time"

corev1 "k8s.io/api/core/v1"
Expand All @@ -39,6 +40,7 @@ const (
defaultTLSSecretConfigKey = "default-tls-secret"
timeoutPolicyIdleKey = "timeout-policy-idle"
timeoutPolicyResponseKey = "timeout-policy-response"
corsPolicy = "corsPolicy"
izabelacg marked this conversation as resolved.
Show resolved Hide resolved
)

// Contour contains contour related configuration defined in the
Expand All @@ -49,18 +51,29 @@ type Contour struct {
DefaultTLSSecret *types.NamespacedName
TimeoutPolicyResponse string
TimeoutPolicyIdle string
CORSPolicy *CORSPolicy
izabelacg marked this conversation as resolved.
Show resolved Hide resolved
}

type CORSPolicy struct {
KauzClay marked this conversation as resolved.
Show resolved Hide resolved
AllowCredentials bool `json:"allowCredentials"`
AllowOrigin []string `json:"allowOrigin"`
AllowMethods []string `json:"allowMethods"`
AllowHeaders []string `json:"allowHeaders"`
ExposeHeaders []string `json:"exposeHeaders"`
MaxAge string `json:"maxAge"`
}

type visibilityValue struct {
Class string `json:"class"`
Service string `json:"service"`
}

// NewContourFromConfigMap creates an Contour config from the supplied ConfigMap
// NewContourFromConfigMap creates a Contour config from the supplied ConfigMap
func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
var tlsSecret *types.NamespacedName
var timeoutPolicyResponse = "infinity"
var timeoutPolicyIdle = "infinity"
var contourCORSPolicy *CORSPolicy

if err := configmap.Parse(configMap.Data,
configmap.AsOptionalNamespacedName(defaultTLSSecretConfigKey, &tlsSecret),
Expand All @@ -70,6 +83,37 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
return nil, err
}

cors, ok := configMap.Data[corsPolicy]
if ok {
if err := yaml.Unmarshal([]byte(cors), &contourCORSPolicy); err != nil {
return nil, err
dprotaso marked this conversation as resolved.
Show resolved Hide resolved
}

fields := [][]string{
contourCORSPolicy.AllowOrigin,
izabelacg marked this conversation as resolved.
Show resolved Hide resolved
contourCORSPolicy.AllowMethods,
contourCORSPolicy.AllowHeaders,
contourCORSPolicy.ExposeHeaders,
}
for _, field := range fields {
if len(field) > 0 {
var validOption = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$")
for _, option := range field {
if !validOption.MatchString(option) {
return nil, fmt.Errorf("option %s is not validly formated", option)
izabelacg marked this conversation as resolved.
Show resolved Hide resolved
}
}
} else {
return nil, fmt.Errorf("fields AllowOrigin, AllowMethods, AllowHeaders, and ExposeHeaders require at least one value")
}
}

_, err := time.ParseDuration(contourCORSPolicy.MaxAge)
if err != nil {
return nil, fmt.Errorf("failed to parse MaxAge: %w", err)
}
}

v, ok := configMap.Data[visibilityConfigKey]
if !ok {
// These are the defaults.
Expand All @@ -85,6 +129,7 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
},
TimeoutPolicyResponse: timeoutPolicyResponse,
TimeoutPolicyIdle: timeoutPolicyIdle,
CORSPolicy: contourCORSPolicy,
}, nil
}
entry := make(map[v1alpha1.IngressVisibility]visibilityValue)
Expand All @@ -107,6 +152,7 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
VisibilityClasses: make(map[v1alpha1.IngressVisibility]string, 2),
TimeoutPolicyResponse: timeoutPolicyResponse,
TimeoutPolicyIdle: timeoutPolicyIdle,
CORSPolicy: contourCORSPolicy,
}
for key, value := range entry {
// Check that the visibility makes sense.
Expand Down
200 changes: 200 additions & 0 deletions pkg/reconciler/contour/config/contour_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package config
import (
"testing"

"github.com/google/go-cmp/cmp"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -40,6 +42,204 @@ func TestContour(t *testing.T) {
}
}

func TestCORSPolicy(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
}

cfg, err := NewContourFromConfigMap(cm)
if err != nil {
t.Error("NewContourFromConfigMap(corsPolicy) =", err)
t.FailNow()
}

want := &CORSPolicy{
AllowCredentials: true,
AllowOrigin: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"authorization", "cache-control"},
ExposeHeaders: []string{"Content-Length", "Content-Range"},
MaxAge: "10m",
}
got := cfg.CORSPolicy
if !cmp.Equal(got, want) {
t.Errorf("Got = %v, want: %v, diff:\n%s", got, want, cmp.Diff(got, want))
}
}
func TestCORSPolicyConfigurationErrors(t *testing.T) {
tests := []struct {
name string
wantErr bool
config *corev1.ConfigMap
}{{
name: "failure parsing yaml",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: "moo",
},
},
}, {
name: "wrong type",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: 3
allowOrigin: true
allowMethods: "moo"
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "incomplete configuration",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "empty value",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: false
allowOrigin: []
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "wrong option",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- ((GET))
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "invalid duration",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10"
`,
},
},
}}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewContourFromConfigMap(tt.config)
t.Log(err)
if (err != nil) != tt.wantErr {
t.Fatalf("Test: %q; NewContourFromConfigMap() error = %v, WantErr %v", tt.name, err, tt.wantErr)
}
})
}
}

func TestDefaultTLSSecret(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Expand Down
41 changes: 41 additions & 0 deletions pkg/reconciler/contour/config/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading