Skip to content
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
2 changes: 1 addition & 1 deletion Taskfile.test-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,7 @@ tasks:
vars:
# Scenarios authored against this env's cluster names and downstream path.
# Older fixtures targeting the previous cluster names are excluded here.
DEFAULT_SCENARIOS: extension-server-smoke waf-enforcement branded-error-page connector-offline-503 atomic-reject-isolation oidc-missing-secret-isolation networkservice-endpoints
DEFAULT_SCENARIOS: extension-server-smoke waf-enforcement waf-section-scoping branded-error-page connector-offline-503 atomic-reject-isolation oidc-missing-secret-isolation networkservice-endpoints
# CLI_ARGS (after --) wins; else SCENARIOS env; else the default set.
SELECTED: '{{.CLI_ARGS | default .SCENARIOS | default .DEFAULT_SCENARIOS}}'
deps:
Expand Down
20 changes: 20 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,26 @@ webhooks:
resources:
- networks
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-networking-datumapis-com-v1alpha-trafficprotectionpolicy
failurePolicy: Fail
name: vtrafficprotectionpolicy-v1alpha.kb.io
rules:
- apiGroups:
- networking.datumapis.com
apiVersions:
- v1alpha
operations:
- CREATE
- UPDATE
resources:
- trafficprotectionpolicies
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
Expand Down
39 changes: 36 additions & 3 deletions internal/controller/trafficprotectionpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=trafficprotectionpolicies,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=trafficprotectionpolicies/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=trafficprotectionpolicies/finalizers,verbs=update
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=httpproxies,verbs=get;list;watch
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch

Expand Down Expand Up @@ -106,7 +107,12 @@ func (r *TrafficProtectionPolicyReconciler) Reconcile(ctx context.Context, req N
return ctrl.Result{}, err
}

r.collectTrafficProtectionPolicyAttachments(ctx, trafficProtectionPolicies, upstreamGateways.Items, upstreamHTTPRoutes.Items)
var upstreamHTTPProxies networkingv1alpha.HTTPProxyList
if err := cl.GetClient().List(ctx, &upstreamHTTPProxies, client.InNamespace(req.Namespace)); err != nil {
return ctrl.Result{}, err
}

r.collectTrafficProtectionPolicyAttachments(ctx, trafficProtectionPolicies, upstreamGateways.Items, upstreamHTTPRoutes.Items, upstreamHTTPProxies.Items)

r.setProgrammedConditionsFromDownstream(ctx, downstreamNamespaceName, trafficProtectionPolicies)

Expand Down Expand Up @@ -319,6 +325,7 @@ type policyGatewayTargetContext struct {

type policyRouteTargetContext struct {
*gatewayv1.HTTPRoute
proxyRuleNames sets.Set[string]
attached bool
attachedToRouteRules sets.Set[string]
}
Expand All @@ -328,6 +335,7 @@ func (r *TrafficProtectionPolicyReconciler) collectTrafficProtectionPolicyAttach
trafficProtectionPolicies []*policyContext,
upstreamGateways []gatewayv1.Gateway,
upstreamHTTPRoutes []gatewayv1.HTTPRoute,
upstreamHTTPProxies []networkingv1alpha.HTTPProxy,
) []policyAttachment {
logger := log.FromContext(ctx)

Expand All @@ -341,10 +349,16 @@ func (r *TrafficProtectionPolicyReconciler) collectTrafficProtectionPolicyAttach
routeMapSize := len(upstreamHTTPRoutes)
gatewayMapSize := len(upstreamGateways)

proxiesByName := make(map[string]*networkingv1alpha.HTTPProxy, len(upstreamHTTPProxies))
for i := range upstreamHTTPProxies {
proxiesByName[upstreamHTTPProxies[i].Name] = &upstreamHTTPProxies[i]
}

routeMap := make(map[client.ObjectKey]*policyRouteTargetContext, routeMapSize)
for i, route := range upstreamHTTPRoutes {
routeMap[client.ObjectKeyFromObject(&route)] = &policyRouteTargetContext{
HTTPRoute: &upstreamHTTPRoutes[i],
HTTPRoute: &upstreamHTTPRoutes[i],
proxyRuleNames: httpProxyRuleNames(&upstreamHTTPRoutes[i], proxiesByName),
}
}

Expand Down Expand Up @@ -478,7 +492,7 @@ func (r *TrafficProtectionPolicyReconciler) processTrafficProtectionPolicyForHTT
}
route.attached = true
} else {
found := false
found := route.proxyRuleNames.Has(string(*targetRef.SectionName))
for _, r := range route.Spec.Rules {
if r.Name != nil && *r.Name == *targetRef.SectionName {
found = true
Expand Down Expand Up @@ -813,6 +827,7 @@ func (r *TrafficProtectionPolicyReconciler) SetupWithManager(mgr mcmanager.Manag
Watches(&networkingv1alpha.TrafficProtectionPolicy{}, EnqueueRequestForObjectNamespace).
Watches(&gatewayv1.Gateway{}, EnqueueRequestForObjectNamespace).
Watches(&gatewayv1.HTTPRoute{}, EnqueueRequestForObjectNamespace).
Watches(&networkingv1alpha.HTTPProxy{}, EnqueueRequestForObjectNamespace).
WatchesRawSource(downstreamTPPSource).
Named("trafficprotectionpolicy").
Complete(r)
Expand Down Expand Up @@ -880,3 +895,21 @@ func (r NamespaceReconcileRequest) WithCluster(name multicluster.ClusterName) Na
r.ClusterName = name
return r
}

func httpProxyRuleNames(route *gatewayv1.HTTPRoute, proxiesByName map[string]*networkingv1alpha.HTTPProxy) sets.Set[string] {
proxyName := route.Name
if owner := metav1.GetControllerOf(route); owner != nil && owner.Kind == "HTTPProxy" {
proxyName = owner.Name
}
proxy, ok := proxiesByName[proxyName]
if !ok {
return nil
}
names := make(sets.Set[string])
for _, rule := range proxy.Spec.Rules {
if rule.Name != nil {
names.Insert(string(*rule.Name))
}
}
return names
}
101 changes: 101 additions & 0 deletions internal/controller/trafficprotectionpolicy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/uuid"
Expand Down Expand Up @@ -44,6 +46,7 @@ func TestCollectTrafficProtectionPolicyAttachments(t *testing.T) {
name string
gateways []gatewayv1.Gateway
httpRoutes []gatewayv1.HTTPRoute
httpProxies []networkingv1alpha.HTTPProxy
trafficProtectionPolicies []networkingv1alpha.TrafficProtectionPolicy
assert func(t *testContext, policyAttachments []policyAttachment)
}{
Expand Down Expand Up @@ -362,6 +365,7 @@ func TestCollectTrafficProtectionPolicyAttachments(t *testing.T) {
tppContexts,
tt.gateways,
tt.httpRoutes,
tt.httpProxies,
)

testCtx := &testContext{
Expand Down Expand Up @@ -953,3 +957,100 @@ func newTrafficProtectionPolicy(

return tpp
}

func TestTrafficProtectionPolicySectionResolvesAgainstHTTPProxy(t *testing.T) {
operatorConfig := config.NetworkServicesOperator{}

newProxy := func(name string, ruleNames ...string) networkingv1alpha.HTTPProxy {
proxy := networkingv1alpha.HTTPProxy{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: name}}
for _, n := range ruleNames {
proxy.Spec.Rules = append(proxy.Spec.Rules, networkingv1alpha.HTTPProxyRule{Name: ptr.To(gatewayv1.SectionName(n))})
}
return proxy
}
routeOwnedBy := func(name, proxyName string) gatewayv1.HTTPRoute {
route := newHTTPRoute("default", name)
route.OwnerReferences = []metav1.OwnerReference{{
APIVersion: networkingv1alpha.GroupVersion.String(),
Kind: "HTTPProxy",
Name: proxyName,
Controller: ptr.To(true),
}}
return *route
}

tests := []struct {
name string
route gatewayv1.HTTPRoute
proxies []networkingv1alpha.HTTPProxy
section string
wantAccepted bool
}{
{
name: "same-named proxy has the rule",
route: *newHTTPRoute("default", "alb"),
proxies: []networkingv1alpha.HTTPProxy{newProxy("alb", "exempt", "protected")},
section: "protected",
wantAccepted: true,
},
{
name: "owning proxy has the rule",
route: routeOwnedBy("route-1", "alb"),
proxies: []networkingv1alpha.HTTPProxy{newProxy("alb", "protected")},
section: "protected",
wantAccepted: true,
},
{
name: "proxy lacks the rule",
route: *newHTTPRoute("default", "alb"),
proxies: []networkingv1alpha.HTTPProxy{newProxy("alb", "exempt")},
section: "protected",
},
{
name: "no proxy",
route: *newHTTPRoute("default", "alb"),
section: "protected",
},
{
name: "unrelated proxy is not consulted",
route: *newHTTPRoute("default", "alb"),
proxies: []networkingv1alpha.HTTPProxy{newProxy("other", "protected")},
section: "protected",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxiesByName := map[string]*networkingv1alpha.HTTPProxy{}
for i := range tt.proxies {
proxiesByName[tt.proxies[i].Name] = &tt.proxies[i]
}
route := tt.route
routeMap := map[client.ObjectKey]*policyRouteTargetContext{
client.ObjectKeyFromObject(&route): {
HTTPRoute: &route,
proxyRuleNames: httpProxyRuleNames(&route, proxiesByName),
},
}
policy := &policyContext{TrafficProtectionPolicy: ptr.To(newTrafficProtectionPolicy("default", "tpp-1"))}
targetRef := gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{
LocalPolicyTargetReference: gatewayv1.LocalPolicyTargetReference{Kind: "HTTPRoute", Name: gatewayv1.ObjectName(route.Name)},
SectionName: ptr.To(gatewayv1.SectionName(tt.section)),
}

reconciler := &TrafficProtectionPolicyReconciler{Config: operatorConfig}
reconciler.processTrafficProtectionPolicyForHTTPRoute(t.Context(), routeMap, nil, nil, policy, targetRef)

require.Len(t, policy.Status.Ancestors, 1)
cond := apimeta.FindStatusCondition(policy.Status.Ancestors[0].Conditions, string(gatewayv1.PolicyConditionAccepted))
require.NotNil(t, cond)
if tt.wantAccepted {
assert.Equal(t, metav1.ConditionTrue, cond.Status)
assert.Equal(t, string(gatewayv1.PolicyReasonAccepted), cond.Reason)
} else {
assert.Equal(t, metav1.ConditionFalse, cond.Status)
assert.Equal(t, string(gatewayv1.PolicyReasonTargetNotFound), cond.Reason)
}
})
}
}
18 changes: 13 additions & 5 deletions internal/extensionserver/cache/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import (
// they are prepended to every policy's per-rule directive list.
func BuildPolicyIndexFromClient(ctx context.Context, cl client.Client, baseDirectives []string) (*PolicyIndex, error) {
idx := &PolicyIndex{
DStoUS: make(map[string]string),
ProjectNames: make(map[string]string),
TPPs: make(map[string][]TPPInfo),
Connectors: make(map[ConnectorKey]ConnectorInfo),
VPCPods: make(map[VPCPodKey]VPCPodInfo),
DStoUS: make(map[string]string),
ProjectNames: make(map[string]string),
TPPs: make(map[string][]TPPInfo),
HTTPProxyRules: make(map[HTTPProxyKey][]string),
Connectors: make(map[ConnectorKey]ConnectorInfo),
VPCPods: make(map[VPCPodKey]VPCPodInfo),
}
if err := populateFromClient(ctx, cl, idx, baseDirectives); err != nil {
return nil, err
Expand Down Expand Up @@ -133,6 +134,13 @@ func populateFromClient(ctx context.Context, cl client.Client, idx *PolicyIndex,
// Resolve the effective upstream namespace for the ConnectorKey, consistent
// with TPP indexing above. In two-cluster replica HTTPProxies carry
// UpstreamOwnerNamespaceLabel; in single-cluster fall back to proxy.Namespace.
ruleNames := make([]string, len(proxy.Spec.Rules))
for i, rule := range proxy.Spec.Rules {
if rule.Name != nil {
ruleNames[i] = string(*rule.Name)
}
}
idx.HTTPProxyRules[HTTPProxyKey{Namespace: proxy.Namespace, Name: proxy.Name}] = ruleNames
effectiveNS := proxy.Labels[downstreamclient.UpstreamOwnerNamespaceLabel]
if effectiveNS == "" {
effectiveNS = proxy.Namespace
Expand Down
38 changes: 38 additions & 0 deletions internal/extensionserver/cache/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1"
Expand Down Expand Up @@ -1664,3 +1665,40 @@ func TestBuildPolicyIndexFromClient_NetworkService_NoGalacticSliceLeavesTenantEm
info := idx.VPCPods[VPCPodKey{UpstreamNS: upstreamNS, HTTPProxyName: "my-proxy", RuleIndex: 0}]
assert.Empty(t, info.TenantID)
}

func TestBuildPolicyIndexFromClient_HTTPProxyRuleNames(t *testing.T) {
scheme := indexTestScheme(t)
name := func(s string) *gatewayv1.SectionName { return (*gatewayv1.SectionName)(&s) }

tests := []struct {
name string
rules []networkingv1alpha.HTTPProxyRule
want []string
}{
{name: "no rules", rules: nil, want: []string{}},
{
name: "all named",
rules: []networkingv1alpha.HTTPProxyRule{{Name: name("exempt")}, {Name: name("protected")}},
want: []string{"exempt", "protected"},
},
{
name: "unnamed rule keeps its position",
rules: []networkingv1alpha.HTTPProxyRule{{}, {Name: name("second")}},
want: []string{"", "second"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy := &networkingv1alpha.HTTPProxy{
ObjectMeta: metav1.ObjectMeta{Name: "alb", Namespace: "ns-abc"},
Spec: networkingv1alpha.HTTPProxySpec{Rules: tt.rules},
}
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(proxy).Build()

idx, err := BuildPolicyIndexFromClient(context.Background(), cl, nil)
require.NoError(t, err)
assert.Equal(t, tt.want, idx.HTTPProxyRules[HTTPProxyKey{Namespace: "ns-abc", Name: "alb"}])
assert.NotContains(t, idx.HTTPProxyRules, HTTPProxyKey{Namespace: "other", Name: "alb"})
})
}
}
11 changes: 11 additions & 0 deletions internal/extensionserver/cache/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ type PolicyIndex struct {
// namespaces are not (they are commonly all "default").
TPPs map[string][]TPPInfo

// HTTPProxyRules maps (downstream replica namespace, httpProxyName) to the
// rule names of that HTTPProxy, indexed by rule position. Entries are empty
// strings for unnamed rules. Envoy Gateway numbers HTTPRoute rules in the
// same order, so the position is the rule index in route and cluster names.
HTTPProxyRules map[HTTPProxyKey][]string

// Connectors maps (upstreamNS, httpProxyName, ruleIndex) to ConnectorInfo.
// Only populated for HTTPProxy rules that have a Connector backend.
// Accumulated across all engaged clusters.
Expand All @@ -64,6 +70,11 @@ type PolicyIndex struct {
VPCPods map[VPCPodKey]VPCPodInfo
}

type HTTPProxyKey struct {
Namespace string
Name string
}

// TPPInfo holds the fields of a TrafficProtectionPolicy needed by the
// mutation layer to inject Coraza WAF config.
type TPPInfo struct {
Expand Down
Loading
Loading