-
Notifications
You must be signed in to change notification settings - Fork 98
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
Client Settings Policy Attachment #1878
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1878 +/- ##
============================================
+ Coverage 86.20% 100.00% +13.79%
============================================
Files 83 1 -82
Lines 5540 209 -5331
Branches 52 52
============================================
- Hits 4776 209 -4567
+ Misses 715 0 -715
+ Partials 49 0 -49 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I like the approach.
} | ||
} | ||
|
||
var clientSettingsTemplate = ` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var clientSettingsTemplate = ` | |
const clientSettingsTemplate = ` |
56d4d2c
to
4c44dc1
Compare
for _, pr := range route.ParentRefs { | ||
ancestor := PolicyAncestor{ | ||
Ancestor: PolicyAncestorRef{ | ||
Kind: "Gateway", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about this, the more I believe the ancestor should be the HTTPRoute instead of the Gateway. The policy affects the location blocks in the NGINX config which traces back to the HTTPRoute, not the Gateway.
I think we want an entry for each HTTPRoute in the ancestor status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ancestor language always confused me a little bit. Before reading it, I assumed ancestor just mean who the policy is attached to, since that's who it affects. But the language says to go all the way up the chain. So wouldn't everything just have Gateway as ancestor? What's the point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's extremely confusing, and I've read it at least 10 times.
Here are my notes:
- Describes the status of a policy with respect to an ancestor
- Ancestors are objects that are either the Target of a policy or above it in the hierarchy.
- Almost always, the Gateway will be the most useful object to place Policy status on. Implementations SHOULD use Gateway at the status object unless there’s a very good reason otherwise.
- Ancestor is used to distinguish which resource results in a distinct application of this policy. For example, if a policy targets a Service, it may have a distinct result per attached Gateway.
- Max number of ancestors is 16. if ancestors is full, the policy can not be implemented by any additional ancestor
I keep coming back to this bullet point:
Ancestor is used to distinguish which resource results in a distinct application of this policy. For example, if a policy targets a Service, it may have a distinct result per attached Gateway.
For this policy, when attached to a route, there is a distinct result per Route, not per Gateway. This is especially true if we want to support multiple target refs. Let's say a policy attaches to route foo
and route bar
. Both routes attach to the same Gateway listener, but foo
is invalid. What would the ancestor status be if the ancestor object is the Gateway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about this, the more I believe the ancestor should be the HTTPRoute instead of the Gateway. The policy affects the location blocks in the NGINX config which traces back to the HTTPRoute, not the Gateway.
What if Gateway has 1000+ HTTPRoutes? Will it blow up the status?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, we also have the case where a policy targets a route that references multiple listeners on a gateway.
Route bar
:
- parentRefs:
- name: gateway
sectionName: listener-1 - name: gateway
sectionName: listener-2
- name: gateway
Policy csp
- targetRef:
- name: bar
kind: HTTPRoute
- name: bar
Let's say bar
attaches to the listener-1
on the gateway, but not listener-2
.
That means the policy will be implemented in the listener-1
server block, but not the listener-2
server block.
In this case, the ancestors should be the Gateway listeners:
- kind: Gateway
name: gateway
sectionName: listener-1
conditions:
- Accepted/True/Accepted
- kind: Gateway
name: gateway
sectionName: listener-2
conditions:
- Accepted/False/TargetRef failed to attach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me think that Client Settings shouldn't support multiple target refs. Since it can be attached at a Gateway, I don't think there's a big need for multiple target refs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if Gateway has 1000+ HTTPRoutes? Will it blow up the status?
When attaching to a Gateway, the ancestor object would be the Gateway so I don't see this as an issue.
However, I do see an issue with using HTTPRoutes as the ancestor. See my last two comments
staticConds.NewPolicyTargetNotFound("TargetRef is invalid"), | ||
) | ||
} else if policy.Valid { | ||
ancestor.Conditions = append(ancestor.Conditions, staticConds.NewPolicyAccepted()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll remove this and add the policy accepted when building the status.
return ancestor, false | ||
} | ||
|
||
ancestor.Conditions = append(ancestor.Conditions, staticConds.NewPolicyAccepted()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here about moving the accepted status.
) | ||
|
||
// attachPolicies attaches the graph's processed policies to the resources they target. It modifies the graph in place. | ||
func (g *Graph) attachPolicies() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add logic to verify that there's enough room in the ancestors slice to attach the policy. Similar to this function:
func validateAncestorMaxCount(backendTLSPolicy *v1alpha2.BackendTLSPolicy, ctlrName string, gateway *Gateway) error { |
Great work @kate-osborn! I like the approach, looks good! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kate-osborn great approach!
I see the following potential problems in the future:
- if a Policy needs to modify existing directives (like adding slow start to upstream server)
- if a Policy can't merge config based on NGINX config - needs NGF-level based logic. Example - auth. Auth in location resets any authentication in server context.
- If multiple Policies produce the same directives (ex. proxy_set_headers for a particular feature... )
@@ -87,6 +89,7 @@ type ChangeProcessorImpl struct { | |||
updater Updater | |||
// getAndResetClusterStateChanged tells if and how the cluster state has changed. | |||
getAndResetClusterStateChanged func() ChangeType | |||
extractGVK extractGVKFunc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't look like it is used anywhere except NewChangeProcessorImpl
for _, pr := range route.ParentRefs { | ||
ancestor := PolicyAncestor{ | ||
Ancestor: PolicyAncestorRef{ | ||
Kind: "Gateway", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about this, the more I believe the ancestor should be the HTTPRoute instead of the Gateway. The policy affects the location blocks in the NGINX config which traces back to the HTTPRoute, not the Gateway.
What if Gateway has 1000+ HTTPRoutes? Will it blow up the status?
|
||
// NewClientSettingsGeneratorFunc returns a function that generates configuration as []byte for a ClientSettingsPolicy. | ||
func NewClientSettingsGeneratorFunc() func(policy policies.Policy) []byte { | ||
return func(policy policies.Policy) []byte { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need to return a function here?
the returned function has the same signature as the generator function, so NewClientSettingsGeneratorFunc() always create the same function
Source: policy, | ||
Valid: len(conds) == 0, | ||
Conditions: conds, | ||
TargetRef: PolicyTargetRef{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks like we're attaching already here (relevant to my previous comment about similarity of functions for processing and attaching Policies)
}, | ||
) | ||
|
||
for i := range policyList { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this part is a bit complicated (imho)
it looks like every policy could conflict with every other policy.
However, once we iterate over loop, we for j := i + 1...
, we mark some conflicted policies as invalid, which I think combined with
if !policyList[i].Valid {
continue
}
might free some other policies later in the sorted order from conflicts...
I wonder if it is possible to add a comment that clarifies the logic
|
||
for _, policyList := range possibles { | ||
if len(policyList) > 1 { | ||
sort.SliceStable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why stable is needed.
ClientObject sorts based on timestamp, namespace, and name, which should give a unique position in the order. So not sure why stable is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a copy-paste. I'll update
@pleshakov thanks for the review and the great callouts! See below for my perspective on the future risks
Yes, this solution only works for pure additions to the NGINX config, but I think it can be extended in the future to handle modifications. What if I changed some of the names to make it clear that these are additions and we plan on extending this solution to support modification when we implement a policy that involves modifications?
That is an issue I didn't consider with this solution. Your example of auth, however, doesn't strictly apply since we are planning on implementing auth as a filter. I think almost all nginx directives follow the standard nginx inheritance behavior. Can we punt on dealing with this until we have a policy that needs special merge rules?
We should avoid adding policies that produce the same directives. Policies should not conflict with one another. I think this should influence how we create policies and not something we should enforce in the code. |
👍
👍
Things I had in mind are auth-related again - propagating auth results to backends via headers. Since we're gonna start with filters for auth, as you mentioned, we don't need to consider it now |
I will re-open once the PR is ready. |
Please review the approach only. Unit tests and lint fixes will be added after the approach is approved.
Proposed changes
Implement Client Settings Policy
Problems:
Solution:
Steps to add a new NGF policy:
Testing: Manual testing covering the following:
Closes #1792 #1760
Checklist
Before creating a PR, run through this checklist and mark each as complete.
Release notes
If this PR introduces a change that affects users and needs to be mentioned in the release notes,
please add a brief note that summarizes the change.