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 methods for dynamic target matching #6

Merged
merged 7 commits into from
May 4, 2024
Merged

Conversation

JustinKuli
Copy link
Owner

The main idea was to extend the nice name + label matching things we have for Namespaces to other types. The individual commit messages have much more context.

JustinKuli added 4 commits May 4, 2024 02:04
This extends the idea of the NamespaceSelector to other types.

The FakePolicy tests demonstrate that Target can be used in a cluster-
scoped manner; it would be easy to restrict it to a specific namespace.
Using NamespaceSelector and Target to limit the policy to specific
resources in specific namespaces is possible, but might require a loop
over all matched namespaces, with a namespaced call in each.

With the new fields in the FakePolicy, one of the format verification
test cases was unnecessary.

Signed-off-by: Justin Kulikauskas <[email protected]>
The new method does not use a dynamic client, which allows it to utilize
the read cache that is configured by default on the controller-runtime
clients. This can improve the speed of the controller, while also
reducing load on the cluster's API server.

The FakePolicy controller demonstrates (and tests) how to implement the
new ResourceList interface, necessary because the `client.ObjectList`
interface does not provide any access to the list's items.

This is a partial implementation, which does not allow restricting the
matches by namespace (that is coming next).

Signed-off-by: Justin Kulikauskas <[email protected]>
GetMatches and GetMatchesDynamic are now able to be namespaced by the
Target. Previously, only GetMatchesDynamic could be namespaced, and it
had to be done by the user when creating the ResourceInterface. This
seems to be a more ergonomic solution.

The tests for the namespaced Targets are mostly duplicates of the tests
for the non-namespaced Targets; some refactoring allowed them to be
re-used without copying them, with only some small adjustments.

Signed-off-by: Justin Kulikauskas <[email protected]>
This reduces some duplicated logic, and provides another example for
users of how to implement the ResourceList interface.

Signed-off-by: Justin Kulikauskas <[email protected]>
Copy link

github-actions bot commented May 4, 2024

PR Review

⏱️ Estimated effort to review [1-5]

4, due to the extensive changes across multiple files, including new methods, types, and tests, which require careful review to ensure correctness and adherence to best practices.

🏅 Score

75

🧪 Relevant tests

Yes

🔍 Possible issues

Possible Bug: The use of reflection in ReflectiveResourceList might lead to performance issues and bugs if not handled correctly. Reflection is generally less performant and more error-prone compared to direct method calls.

Code Duplication: There seems to be a significant amount of boilerplate and duplicated logic, especially in the methods related to matching and filtering resources. This could be refactored to improve maintainability.

🔒 Security concerns

No


✨ Review tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

Comment on lines 20 to 21
func (l fakeObjList) SetResourceVersion(version string) {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The method SetResourceVersion in fakeObjList and fakeObj should set the resource version to the provided string. Currently, it does nothing, which might not be the intended behavior. [bug]

Suggested change
func (l fakeObjList) SetResourceVersion(version string) {
}
func (l *fakeObjList) SetResourceVersion(version string) {
*l = fakeObjList(version)
}

Comment on lines 48 to 50
func (l fakeObjList) DeepCopyObject() runtime.Object {
return l
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The method DeepCopyObject in fakeObjList and fakeObj should return a deep copy of the object instead of returning the receiver directly. This prevents modifications to the copy from affecting the original object. [bug]

Suggested change
func (l fakeObjList) DeepCopyObject() runtime.Object {
return l
}
func (l fakeObjList) DeepCopyObject() runtime.Object {
copy := l
return copy
}

Comment on lines 41 to 42
func (l fakeObjList) SetRemainingItemCount(c *int64) {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The method SetRemainingItemCount in fakeObjList should set the remaining item count to the provided pointer. Currently, it does nothing, which might not be the intended behavior. [bug]

Suggested change
func (l fakeObjList) SetRemainingItemCount(c *int64) {
}
func (l *fakeObjList) SetRemainingItemCount(c *int64) {
// Assuming there's a field to store this value, which should be added to the struct.
// l.remainingItemCount = c
}

Comment on lines 42 to 47
// ReflectiveResourceList implements ResourceList for the wrapped client.ObjectList, by using
// reflection. The wrapped list must have an Items field, with a slice of items which satisfy the
// client.Object interface - most types which satisfy client.ObjectList seem to follow this
// convention. Using this type is not recommended: implementing ResourceList yourself will generally
// lead to better performance.
type ReflectiveResourceList struct {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The ReflectiveResourceList uses reflection which can be error-prone and inefficient. Consider implementing specific ResourceList types for known ObjectList types to avoid reflection and improve performance. [enhancement]

Suggested change
// ReflectiveResourceList implements ResourceList for the wrapped client.ObjectList, by using
// reflection. The wrapped list must have an Items field, with a slice of items which satisfy the
// client.Object interface - most types which satisfy client.ObjectList seem to follow this
// convention. Using this type is not recommended: implementing ResourceList yourself will generally
// lead to better performance.
type ReflectiveResourceList struct {
// Example for a specific implementation for a PodList
type PodResourceList struct {
PodList *corev1.PodList
}
func (l *PodResourceList) Items() ([]client.Object, error) {
items := make([]client.Object, len(l.PodList.Items))
for i, pod := range l.PodList.Items {
items[i] = &pod
}
return items, nil
}
func (l *PodResourceList) ObjectList() client.ObjectList {
return l.PodList
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you just re-word the warnings in my docstring as a suggestion for me? 😆 I mean, it's true, but...

Comment on lines +231 to 174
include, err = filepath.Match(string(includePattern), name)
if err != nil {
return false, fmt.Errorf("error parsing 'include' pattern '%s': %w", string(includePattern), err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The error handling in match function should provide more context about the error, especially which pattern caused the error. This will help in debugging issues with pattern matching. [enhancement]

Suggested change
include, err = filepath.Match(string(includePattern), name)
if err != nil {
return false, fmt.Errorf("error parsing 'include' pattern '%s': %w", string(includePattern), err)
}
include, err = filepath.Match(string(includePattern), name)
if err != nil {
return false, fmt.Errorf("error parsing 'include' pattern '%s' for name '%s': %w", string(includePattern), name, err)
}

Comment on lines +31 to +42
By("Creating sample configmaps")
for _, cm := range sampleConfigMaps {
ns, name, _ := strings.Cut(cm, "/")
cmObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"sample": name},
},
Data: map[string]string{"foo": "bar"},
}
Expect(cleanlyCreate(cmObj)).To(Succeed())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: To improve the efficiency of the test setup, consider using a batch creation approach for ConfigMaps if the API supports it, instead of creating them one by one inside a loop. This reduces the number of API calls and can significantly speed up the test setup phase. [performance]

Suggested change
By("Creating sample configmaps")
for _, cm := range sampleConfigMaps {
ns, name, _ := strings.Cut(cm, "/")
cmObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"sample": name},
},
Data: map[string]string{"foo": "bar"},
}
Expect(cleanlyCreate(cmObj)).To(Succeed())
// Example of batch creation, adjust based on actual API capabilities
var configMaps []*corev1.ConfigMap
for _, cm := range sampleConfigMaps {
ns, name, _ := strings.Cut(cm, "/")
cmObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"sample": name},
},
Data: map[string]string{"foo": "bar"},
}
configMaps = append(configMaps, cmObj)
}
Expect(batchCreateConfigMaps(configMaps)).To(Succeed())

Comment on lines +8 to +9
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: It's recommended to avoid using dot imports as they can pollute the namespace and make it harder to understand where identifiers come from. Consider importing the packages with a namespace and using them explicitly. [best practice]

Suggested change
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
ginkgo "github.com/onsi/ginkgo/v2"
gomega "github.com/onsi/gomega"

Comment on lines +141 to +147
Eventually(func(g Gomega) {
foundPolicy := fakev1beta1.FakePolicy{}
g.Expect(k8sClient.Get(ctx, getNamespacedName(&policy), &foundPolicy)).To(Succeed())
g.Expect(foundPolicy.Status.SelectionComplete).To(BeTrue())
g.Expect(foundPolicy.Status.DynamicSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.ClientSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.SelectionError).To(Equal(selErr))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The use of Eventually should include a timeout to avoid indefinitely waiting for a condition that may never be met, which can hang the test suite. [best practice]

Suggested change
Eventually(func(g Gomega) {
foundPolicy := fakev1beta1.FakePolicy{}
g.Expect(k8sClient.Get(ctx, getNamespacedName(&policy), &foundPolicy)).To(Succeed())
g.Expect(foundPolicy.Status.SelectionComplete).To(BeTrue())
g.Expect(foundPolicy.Status.DynamicSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.ClientSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.SelectionError).To(Equal(selErr))
Eventually(func(g Gomega) {
foundPolicy := fakev1beta1.FakePolicy{}
g.Expect(k8sClient.Get(ctx, getNamespacedName(&policy), &foundPolicy)).To(Succeed())
g.Expect(foundPolicy.Status.SelectionComplete).To(BeTrue())
g.Expect(foundPolicy.Status.DynamicSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.ClientSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.SelectionError).To(Equal(selErr))
}, "30s").Should(Succeed()) # Example timeout of 30 seconds

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not provided, Eventually uses a default timeout (I think 1 second)

Comment on lines +28 to +56
allConfigMaps := append(defaultConfigMaps, sampleConfigMaps...)

beforeFunc := func() {
By("Creating sample configmaps")
for _, cm := range sampleConfigMaps {
ns, name, _ := strings.Cut(cm, "/")
cmObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"sample": name},
},
Data: map[string]string{"foo": "bar"},
}
Expect(cleanlyCreate(cmObj)).To(Succeed())
}

By("Ensuring the allConfigMaps list is correct")
// constructing the default / allConfigMaps lists is complicated because of how ginkgo
// runs the table tests... this seems better than other workarounds.
cmList := corev1.ConfigMapList{}
Expect(k8sClient.List(ctx, &cmList)).To(Succeed())

foundCM := make([]string, len(cmList.Items))
for i, cm := range cmList.Items {
foundCM[i] = cm.GetNamespace() + "/" + cm.GetName()
}

Expect(allConfigMaps).To(ConsistOf(foundCM))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: To ensure that the allConfigMaps list is correctly populated before it's used in assertions, consider adding a check to ensure that sampleConfigMaps and defaultConfigMaps are not empty. This prevents false positives in tests where the configuration might not be loaded as expected. [enhancement]

Suggested change
allConfigMaps := append(defaultConfigMaps, sampleConfigMaps...)
beforeFunc := func() {
By("Creating sample configmaps")
for _, cm := range sampleConfigMaps {
ns, name, _ := strings.Cut(cm, "/")
cmObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{"sample": name},
},
Data: map[string]string{"foo": "bar"},
}
Expect(cleanlyCreate(cmObj)).To(Succeed())
}
By("Ensuring the allConfigMaps list is correct")
// constructing the default / allConfigMaps lists is complicated because of how ginkgo
// runs the table tests... this seems better than other workarounds.
cmList := corev1.ConfigMapList{}
Expect(k8sClient.List(ctx, &cmList)).To(Succeed())
foundCM := make([]string, len(cmList.Items))
for i, cm := range cmList.Items {
foundCM[i] = cm.GetNamespace() + "/" + cm.GetName()
}
Expect(allConfigMaps).To(ConsistOf(foundCM))
allConfigMaps := append(defaultConfigMaps, sampleConfigMaps...)
Expect(defaultConfigMaps).NotTo(BeEmpty())
Expect(sampleConfigMaps).NotTo(BeEmpty())
Expect(allConfigMaps).To(ConsistOf(foundCM))

Comment on lines +11 to +14
targetConfigMaps:
matchExpressions:
- key: sample
operator: Exists
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: It's recommended to specify values or value when using the Exists operator in matchExpressions to ensure clarity and explicit behavior, even though it might be optional depending on the context. [enhancement]

Suggested change
targetConfigMaps:
matchExpressions:
- key: sample
operator: Exists
targetConfigMaps:
matchExpressions:
- key: sample
operator: Exists
values: ["desired_value_here"]

// provided Interface is already namespaced, the namespace of the Interface will be used (possibly
// overriding the namespace specified in the Target).
//
// NOTE: unlike the NamespaceSelector, an empty Target will match *all* resources on the cluster.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something I need to reconsider

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful documentation will be the best approach. The behavior of selectors is often confusing to me (and I've read through it on various occasions). Nil selectors might select nothing? But empty selectors select everything. It's like they're deliberately trying to confuse me. So we will treat nil as empty, and if a user wants to select nothing, they can Exclude: "*".

Comment on lines +38 to +40
matchingNames := make([]string, 0, len(set))
for ns := range set {
matchingNames = append(matchingNames, ns)
}

return matchingNames, nil
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea. Not in this test function specifically, but more generally in the GetMatches and GetMatchesDynamic methods.

Comment on lines +141 to +147
Eventually(func(g Gomega) {
foundPolicy := fakev1beta1.FakePolicy{}
g.Expect(k8sClient.Get(ctx, getNamespacedName(&policy), &foundPolicy)).To(Succeed())
g.Expect(foundPolicy.Status.SelectionComplete).To(BeTrue())
g.Expect(foundPolicy.Status.DynamicSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.ClientSelectedConfigMaps).To(ConsistOf(desiredMatches))
g.Expect(foundPolicy.Status.SelectionError).To(Equal(selErr))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not provided, Eventually uses a default timeout (I think 1 second)

Copy link
Owner Author

@JustinKuli JustinKuli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some diffs are not lining up... that makes this tricky to review.

@JustinKuli
Copy link
Owner Author

/describe

Copy link

github-actions bot commented May 4, 2024

Title

Add methods for dynamic target matching


User description

The main idea was to extend the nice name + label matching things we have for Namespaces to other types. The individual commit messages have much more context.


Type

enhancement, tests


Description

  • Introduced Target struct enhancements for dynamic and static resource matching.
  • Added new methods in NamespaceSelector for fetching and filtering namespaces.
  • Implemented fake client objects for testing and added comprehensive unit tests.
  • Updated FakePolicy types and controller to handle ConfigMap targeting based on new Target struct capabilities.

Changes walkthrough

Relevant files
Tests
clientobjectfakes_test.go
Implement fake client.Object and client.ObjectList for testing

api/v1beta1/clientobjectfakes_test.go

  • Added fake implementations for client.Object and client.ObjectList
    interfaces.
  • Implemented methods to handle resource version, self link,
    continuation, and other metadata.
  • +168/-0 
    target_test.go
    Add comprehensive tests for Target struct's functionality

    api/v1beta1/target_test.go

  • Added extensive tests for the Target struct's matching functionality.
  • Tested both match and matches methods with various scenarios.
  • +109/-0 
    target_test.go
    Implement Ginkgo tests for TargetConfigMaps in FakePolicy

    test/fakepolicy/test/target_test.go

  • Implemented tests for TargetConfigMaps in FakePolicy using Ginkgo.
  • Covered various scenarios including label selection and namespace
    restriction.
  • +210/-0 
    Enhancement
    policycore_types.go
    Extend NamespaceSelector with namespace fetching and filtering

    api/v1beta1/policycore_types.go

  • Added GetNamespaces method to NamespaceSelector to fetch and filter
    namespaces based on labels and lists.
  • Implemented namespaceResList struct to handle namespace lists with
    client.Object interface.
  • +49/-0   
    target.go
    Enhance Target with dynamic and static resource matching capabilities

    api/v1beta1/target.go

  • Introduced Target struct with fields for label selectors and namespace
    scoping.
  • Added GetMatches and GetMatchesDynamic methods for resource matching
    based on labels and names.
  • Implemented ReflectiveResourceList using reflection to handle generic
    client.ObjectList types.
  • +207/-66
    fakepolicy_types.go
    Update FakePolicy types to support ConfigMap targeting     

    test/fakepolicy/api/v1beta1/fakepolicy_types.go

  • Added TargetConfigMaps field to FakePolicySpec to specify ConfigMaps
    targeting.
  • Introduced fields in FakePolicyStatus for storing selected ConfigMaps
    dynamically and via client.
  • +14/-0   
    fakepolicy_controller.go
    Enhance FakePolicy controller with dynamic client and resource
    selection

    test/fakepolicy/controllers/fakepolicy_controller.go

  • Updated FakePolicyReconciler to handle namespace and ConfigMap
    selection based on FakePolicy spec.
  • Added dynamic client initialization and usage for dynamic resource
    handling.
  • +72/-6   

    PR-Agent usage:
    Comment /help on the PR to get a list of all available PR-Agent tools and their descriptions

    @JustinKuli
    Copy link
    Owner Author

    JustinKuli commented May 4, 2024

    AI Suggestions Score

    Category Count Comment
    Great 2 Sorting returned lists and splitting some logic is genuinely good advice
    Good 6 General, but decent, advice. I won't be doing most of it, but it's a stylistic choice
    Bad 6 General advice that is not taking into account the specific context: eg dot-importing ginkgo
    Terrible 2 Incorrect/misleading comments

    JustinKuli and others added 3 commits May 4, 2024 18:19
    This type implements ResourceList using reflection, so that consumers
    don't need to implement the methods themselves. FakePolicyController
    tests verify that the behavior of using this implementation matches the
    behavior of the bespoke implementation.
    
    Signed-off-by: Justin Kulikauskas <[email protected]>
    Signed-off-by: Justin Kulikauskas <[email protected]>
    Signed-off-by: Justin Kulikauskas <[email protected]>
    @JustinKuli JustinKuli force-pushed the updated-dyn-target branch from a65fcc0 to b888d94 Compare May 4, 2024 19:04
    @JustinKuli JustinKuli merged commit 8f86bd2 into main May 4, 2024
    8 checks passed
    @JustinKuli JustinKuli deleted the updated-dyn-target branch May 4, 2024 19:14
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    None yet
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    1 participant