-
Notifications
You must be signed in to change notification settings - Fork 480
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
Make gwctl work with Policies having multiple targetRefs #3217
Changes from all commits
667e73b
09fe7a8
d13c46c
7408571
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,14 +22,13 @@ import ( | |
"fmt" | ||
"strings" | ||
|
||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/client-go/dynamic" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" | ||
"sigs.k8s.io/gateway-api/gwctl/pkg/common" | ||
|
@@ -216,10 +215,10 @@ func (p PolicyCRD) IsClusterScoped() bool { | |
|
||
type Policy struct { | ||
u unstructured.Unstructured | ||
// targetRef references the target object this policy is attached to. This | ||
// targetRefs references the target object this policy is attached to. This | ||
// only makes sense in case of a directly-attached-policy, or an | ||
// unmerged-inherited-policy. | ||
targetRef common.ObjRef | ||
targetRefs []common.ObjRef | ||
// Indicates whether the policy is supposed to be "inherited" (as opposed to | ||
// "direct"). | ||
inherited bool | ||
|
@@ -230,29 +229,53 @@ func (p Policy) ClientObject() client.Object { return p.Unstructured() } | |
func PolicyFromUnstructured(u unstructured.Unstructured, policyCRDs map[PolicyCrdID]PolicyCRD) (Policy, error) { | ||
result := Policy{u: u} | ||
|
||
// Identify targetRef of Policy. | ||
// Identify targetRefs of Policy. | ||
type genericPolicy struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ObjectMeta `json:"metadata,omitempty"` | ||
Spec struct { | ||
TargetRef gatewayv1alpha2.NamespacedPolicyTargetReference | ||
TargetRefs []gatewayv1alpha2.NamespacedPolicyTargetReference `json:"targetRefs"` | ||
TargetRef *gatewayv1alpha2.NamespacedPolicyTargetReference `json:"targetRef"` | ||
} | ||
} | ||
structuredPolicy := &genericPolicy{} | ||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), structuredPolicy); err != nil { | ||
return Policy{}, fmt.Errorf("failed to convert unstructured policy resource to structured: %v", err) | ||
} | ||
result.targetRef = common.ObjRef{ | ||
Group: string(structuredPolicy.Spec.TargetRef.Group), | ||
Kind: string(structuredPolicy.Spec.TargetRef.Kind), | ||
Name: string(structuredPolicy.Spec.TargetRef.Name), | ||
Namespace: structuredPolicy.GetNamespace(), | ||
} | ||
if result.targetRef.Namespace == "" { | ||
result.targetRef.Namespace = result.u.GetNamespace() | ||
|
||
// Process targetRefs | ||
for _, targetRef := range structuredPolicy.Spec.TargetRefs { | ||
objRef := common.ObjRef{ | ||
Group: string(targetRef.Group), | ||
Kind: string(targetRef.Kind), | ||
Name: string(targetRef.Name), | ||
Namespace: u.GetNamespace(), | ||
} | ||
if objRef.Namespace == "" { | ||
objRef.Namespace = u.GetNamespace() | ||
} | ||
if targetRef.Namespace != nil { | ||
objRef.Namespace = string(*targetRef.Namespace) | ||
} | ||
Comment on lines
+257
to
+259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're willing to import objRef.Namespace = ptr.Deref(targetRef.Namespace, structuredPolicy.GetNamespace()) |
||
result.targetRefs = append(result.targetRefs, objRef) | ||
} | ||
if structuredPolicy.Spec.TargetRef.Namespace != nil { | ||
result.targetRef.Namespace = string(*structuredPolicy.Spec.TargetRef.Namespace) | ||
|
||
// Process single targetRef (if present and targetRefs is empty) | ||
if structuredPolicy.Spec.TargetRef != nil && len(result.targetRefs) == 0 { | ||
targetRef := structuredPolicy.Spec.TargetRef | ||
objRef := common.ObjRef{ | ||
Group: string(targetRef.Group), | ||
Kind: string(targetRef.Kind), | ||
Name: string(targetRef.Name), | ||
Namespace: u.GetNamespace(), | ||
} | ||
if objRef.Namespace == "" { | ||
objRef.Namespace = u.GetNamespace() | ||
} | ||
if targetRef.Namespace != nil { | ||
objRef.Namespace = string(*targetRef.Namespace) | ||
} | ||
result.targetRefs = append(result.targetRefs, objRef) | ||
} | ||
|
||
// Get the CRD corresponding to this policy object. | ||
|
@@ -274,8 +297,8 @@ func (p Policy) PolicyCrdID() PolicyCrdID { | |
return PolicyCrdID(p.u.GetObjectKind().GroupVersionKind().Kind + "." + p.u.GetObjectKind().GroupVersionKind().Group) | ||
} | ||
|
||
func (p Policy) TargetRef() common.ObjRef { | ||
return p.targetRef | ||
func (p Policy) TargetRefs() []common.ObjRef { | ||
return p.targetRefs | ||
} | ||
|
||
func (p Policy) IsInherited() bool { | ||
|
@@ -287,19 +310,24 @@ func (p Policy) IsDirect() bool { | |
} | ||
|
||
func (p Policy) IsAttachedTo(objRef common.ObjRef) bool { | ||
if p.targetRef.Kind == "Namespace" && p.targetRef.Name == "" { | ||
p.targetRef.Name = "default" | ||
} | ||
if objRef.Kind == "Namespace" && objRef.Name == "" { | ||
objRef.Name = "default" | ||
} | ||
if p.targetRef.Kind != "Namespace" && p.targetRef.Namespace == "" { | ||
p.targetRef.Namespace = "default" | ||
} | ||
if objRef.Kind != "Namespace" && objRef.Namespace == "" { | ||
objRef.Namespace = "default" | ||
for _, targetRef := range p.targetRefs { | ||
if targetRef.Kind == "Namespace" && targetRef.Name == "" { | ||
targetRef.Name = "default" | ||
} | ||
if objRef.Kind == "Namespace" && objRef.Name == "" { | ||
objRef.Name = "default" | ||
} | ||
Comment on lines
+317
to
+319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this could be done outside the loop. |
||
if targetRef.Kind != "Namespace" && targetRef.Namespace == "" { | ||
targetRef.Namespace = "default" | ||
} | ||
if objRef.Kind != "Namespace" && objRef.Namespace == "" { | ||
objRef.Namespace = "default" | ||
} | ||
Comment on lines
+323
to
+325
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. Could be done outside the loop. |
||
if targetRef == objRef { | ||
return true | ||
} | ||
} | ||
return p.targetRef == objRef | ||
return false | ||
} | ||
|
||
func (p Policy) Unstructured() *unstructured.Unstructured { | ||
|
@@ -308,9 +336,9 @@ func (p Policy) Unstructured() *unstructured.Unstructured { | |
|
||
func (p Policy) DeepCopy() Policy { | ||
clone := Policy{ | ||
u: *p.u.DeepCopy(), | ||
targetRef: p.targetRef, | ||
inherited: p.inherited, | ||
u: *p.u.DeepCopy(), | ||
targetRefs: p.targetRefs, | ||
inherited: p.inherited, | ||
} | ||
return clone | ||
} | ||
|
@@ -320,18 +348,28 @@ func (p Policy) Spec() map[string]interface{} { | |
if err != nil || !ok { | ||
return nil | ||
} | ||
|
||
result, ok := spec.(map[string]interface{}) | ||
if !ok { | ||
return nil | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit out of scope, but we could probably use |
||
if targetRef, hasTargetRef := result["targetRef"]; hasTargetRef { | ||
if _, hasTargetRefs := result["targetRefs"]; !hasTargetRefs { | ||
result["targetRefs"] = []interface{}{targetRef} | ||
} else { | ||
result["targetRefs"] = append(result["targetRefs"].([]interface{}), targetRef) | ||
} | ||
delete(result, "targetRef") | ||
} | ||
|
||
return result | ||
} | ||
|
||
func (p Policy) EffectiveSpec() (map[string]interface{}, error) { | ||
if !p.IsInherited() { | ||
// No merging is required in case of Direct policies. | ||
result := p.Spec() | ||
delete(result, "targetRefs") | ||
delete(result, "targetRef") | ||
return result, nil | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,7 +58,14 @@ func MergePoliciesOfSimilarKind(policies []Policy) (map[PolicyCrdID]Policy, erro | |
return nil, err | ||
} | ||
|
||
result[policyCrdID] = mergedPolicies[policyCrdID] | ||
mergedPolicy := mergedPolicies[policyCrdID] | ||
|
||
// Check and set targetRefs to nil if it's an empty slice | ||
if len(mergedPolicy.targetRefs) == 0 { | ||
mergedPolicy.targetRefs = nil | ||
} | ||
|
||
result[policyCrdID] = mergedPolicy | ||
} | ||
return result, nil | ||
} | ||
|
@@ -149,10 +156,26 @@ func mergePolicy(parent, child Policy) (Policy, error) { | |
result.u.SetUnstructuredContent(resultUnstructured) | ||
// Merging two policies means the targetRef no longer makes any sense since | ||
// since they can be conflicting. So we unset the targetRef. | ||
result.targetRef = common.ObjRef{} | ||
result.targetRefs = mergeTargetRefs(parent.targetRefs, child.targetRefs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Say Sorry, I may need to educate myself on how policies to be merged are selected in the first place – or, more precisely, how the policies are assigned to a particular node in the resource model. It just triggered me the idea that, if you ask for the effective policy for Concrete example – Assume IOW: kind: Gateway
metadata:
name: gateway-1
---
kind: Gateway
metadata:
name: gateway-2
---
kind: HTTPRoute
metadata:
name: route-1
spec:
parentRefs:
- kind: Gateway
name: gateway-1
---
kind: HTTPRoute
metadata:
name: route-2
spec:
parentRefs:
- kind: Gateway
name: gateway-2
---
kind: Policy
metadata:
name: parent
spec:
targetRefs:
- kind: Gateway
name: gateway-1
- kind: HTTPRoute
name: route-1
---
kind: Policy
metadata:
name: child
spec:
targetRefs:
- kind: Gateway
name: gateway-1
- kind: HTTPRoute
name: route-2 Effective policies:
By reading the first line, if you trust |
||
return result, nil | ||
} | ||
|
||
// Helper function to merge targetRefs from two policies | ||
func mergeTargetRefs(refs1, refs2 []common.ObjRef) []common.ObjRef { | ||
refMap := make(map[common.ObjRef]struct{}) | ||
for _, ref := range refs1 { | ||
refMap[ref] = struct{}{} | ||
} | ||
for _, ref := range refs2 { | ||
refMap[ref] = struct{}{} | ||
} | ||
result := make([]common.ObjRef, 0, len(refMap)) | ||
for ref := range refMap { | ||
result = append(result, ref) | ||
} | ||
return result | ||
} | ||
|
||
func mergeUnstructured(parent, patch map[string]interface{}) (map[string]interface{}, error) { | ||
currentJSON, err := json.Marshal(parent) | ||
if err != nil { | ||
|
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 may be missing something, but I understand
objRef.Namespace
has been initialized tou.GetNamespace()
already, no? Then, if it's an empty string, it's set again to the sameu.GetNamespace()
? Maybe you meant: