-
Notifications
You must be signed in to change notification settings - Fork 14
feat: add Conflictingmarkers #126
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
base: main
Are you sure you want to change the base?
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: yongruilin The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
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.
Thanks for raising this, looking mostly there but found a few things I think we can improve, mainly around documentation and how we output the messages
docs/linters.md
Outdated
- Set A: `+default`, `+kubebuilder:default` | ||
- Set B: `+required`, `+kubebuilder:validation:Required` |
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.
Is there a k8s:default
marker confirmed yet? Or even, are we expecting to create one?
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.
Not for now, but we do expect to create one. No harm to add it here.
docs/linters.md
Outdated
The `conflictingmarkers` linter detects and reports when mutually exclusive markers are used on the same field. | ||
This prevents common configuration errors and unexpected behavior in Kubernetes API types. | ||
|
||
The linter includes built-in checks for the following conflicts: |
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.
Are there any other marker combinations that we want to include in this initial phase?
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.
Not that I'm aware of.
docs/linters.md
Outdated
### Configuration | ||
|
||
The linter is configurable and allows users to define their own custom sets of conflicting markers. | ||
Each custom conflict set must specify a name, two sets of markers, and a description of why they conflict. |
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'm guessing the description will be used in the linter output when it detects these conflicts?
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.
Yes. It is being used in the report.
func defaultConflictSets() []ConflictSet { | ||
return []ConflictSet{ | ||
{ | ||
Name: "optional_vs_required", |
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.
Is this used at all? I can't see it being used, should it be in the message, perhaps before the value lists
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 description should provide sufficient info? The name is used for linter author to track the sets easier.
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 don't actually see the name being used anywhere apart from it being checked that it is unique?
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 is used when validating the config but not used when reporting.
config := &conflictingmarkers.ConflictingMarkersConfig{ | ||
CustomConflicts: []conflictingmarkers.ConflictSet{ | ||
{ | ||
Name: "custom_conflict", |
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 if there is ever a case for a conflicting marker with attributes? For example XValidation in Kubebuilder has multiple properties, could you create conflicts with something like that? Are there any markers in the declarative validation project that have attributes like XValidation does?
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.
Hmmm, declarative validation parses tags differently using "arguments" . I think keeping conflicting set focus on marker id is simpler.
type ConflictSet struct { | ||
// Name is a human-readable name for this conflict set. | ||
Name string `json:"name"` | ||
// SetA contains the first set of conflicting markers. |
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.
Need to make this description obvious that it is A conflicts with B and not conflicts within setA
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.
Updated the comment.
@@ -0,0 +1,30 @@ | |||
package b | |||
|
|||
type CustomConflictStruct struct { |
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.
Include a case from the other tests to make sure that enabling custom markers doesn't disable the built in markers
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.
Added one more test case.
a1482d3
to
13da92b
Compare
Entry("With missing name", testCase{ | ||
config: conflictingmarkers.ConflictingMarkersConfig{ | ||
CustomConflicts: []conflictingmarkers.ConflictSet{ | ||
{ | ||
Name: "", | ||
SetA: []string{"marker1"}, | ||
SetB: []string{"marker2"}, | ||
Description: "Test conflict", | ||
}, | ||
}, | ||
}, | ||
expectedErr: "conflictingmarkers.customConflicts[0].name: Required value: name is required", | ||
}), |
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.
Please add a test for missing description as well
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.
Is the one on line91 you looking for?
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.
Yes, I can't read 😅
Had a chat with @everettraven about this linter and a pattern we were looking at in #111. I think we should look to implement the same pattern here. The pattern would be to create a generic base linter that has no configuration. Users could then configure this with their own config of conflicting markers should they so desire. Then, for the rules we know we want, we create instances of the base marker with pre-defined configuration. This means users would see I think in terms of implementation, the
This means that, should a user desire to have their own custom conflicting markers without the default set of conflicts we've identified, they can do so, and they can choose explicitly which of the conflicts they do and don't care about. |
It is a good idea. Then I think it is make sense to remove a built-in conflict sets to make it fully flexible? |
I might not have enough of the context to fully grok this question, so feel free to ignore if I've misinterpretted. My understanding is that we do still want to have some opinionated built-in conflict identification, but these should be distinct linters that can be enabled/disabled without also having to enable/disable the custom conflict rules. I would expect any conflicts that we identify from reading the Kube API conventions, declarative validation, and controller-gen markers we have a linter (or multiple) for so users don't have to connect those dots themselves in a set of custom conflict rules. |
fdabe40
to
f1480a6
Compare
I pushed a new commit to add an option to disable the built-in conflicts. |
@yongruilin I took your branch and pushed a couple of commits on a branch on my fork, this is kind of what I was imagining in my comment, WDYT of https://github.com/kubernetes-sigs/kube-api-linter/compare/main...JoelSpeed:kube-api-linter:pr/yongruilin/126?expand=1 @everettraven please also check this aligns with what you were expecting |
@JoelSpeed thanks for composing this. Basically we have 2 ways to proceed on conflictingmarkers
For 1, is it worth the effort to introduce new linters? Keeping common conflict cases as built-in looks simpler to me. |
This does align with what I was expecting.
I would agree that keeping common conflict cases would be simpler in implementation and maintenance burden on maintainers, but I think it causes UX limitations that are not ideal. Adding new conflicting sets for option 2 would also likely be considered a breaking change as it could be more restrictive. We would have to solve for how we add new built-in conflict rules as opt-in. I don't think option 1 has this same problem because we can always add new non-default linters that users can opt-in to use - we don't have to reinvent the wheel to solve that issue. For example, consider the following use cases:
IMO we should prefer UX > maintainer burden where possible. |
I can't really summarise my thoughts any better than @everettraven has, but yes, my priority here is not for maintainer burden (though I am trying hard to make that easy), but for the end user UX and making it easy to configure. I think the points about how would we possibly add new conflicts to the built in markers without being breaking is also an important point that weighs into the decision here. I'm very keen to go for option 1 at the moment and see if that decision ages well over time |
f1480a6
to
60f7f99
Compare
Ok, I agree with this approach. I've already updated to be fully configurable with no built-in conflict sets, which fits with the generic base linter concept. |
docs/linters.md
Outdated
setA: | ||
- "default" | ||
- "+kubebuilder:default" | ||
setB: | ||
- "required" | ||
- "+kubebuilder:validation:Required" | ||
- "+k8s:validation:required" |
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 setA and setB is ambiguous. For example, how about first
/ second
? If this is it, when we extend set, it's easy to add like third
.
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 was also thinking about the naming and couldn't come up with something I was absolutely happy with, this is a hard case to name 🤔
I could see first
and second
working, but does that imply some sort of ordering? Where I don't think there actually is any ordering?
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 we just made it:
conflicts:
- name: "conflict_one"
sets:
- ["default", "kubebuilder:default"]
- ["required", "kubebuilder:Required"]
- ...
We could, for now, limit sets to just two entries? Also, do we ever envision needing to extend this beyond 2 sets for a single conflict rule? How would/should that be handled?
Another naming option could be left
and right
? No ordering is signaled there and is something that I've seen used to represent comparison operations in the past.
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.
Good idea, we actually allow multiple sets in validation-gen's lint. We should handle it that all sets are conflicting with each other.
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 would open up the case for something that correctly allows you to check that you only have on of [MinItems,MaxItems], [MinProperties,MaxProperties], [MinLength,MaxLength] since we know that these are for different resources types (array, map/object, string)
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.
@everettraven
Good idea, totally agree it.
if len(in.expectedErr) > 0 { | ||
Expect(errs.ToAggregate()).To(MatchError(in.expectedErr)) | ||
} else { | ||
Expect(errs).To(HaveLen(0), "No errors were expected") | ||
} |
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.
if len(in.expectedErr) > 0 { | |
Expect(errs.ToAggregate()).To(MatchError(in.expectedErr)) | |
} else { | |
Expect(errs).To(HaveLen(0), "No errors were expected") | |
} | |
if len(in.expectedErr) > 0 { | |
Expect(errs.ToAggregate()).To(MatchError(in.expectedErr)) | |
return | |
} | |
Expect(errs).To(HaveLen(0), "No errors were expected") |
It's easy to read.
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.
Please add test case that has +marker1 and +marker2. It must be valid.
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.
Added.
docs/linters.md
Outdated
setA: | ||
- "default" | ||
- "+kubebuilder:default" | ||
setB: | ||
- "required" | ||
- "+kubebuilder:validation:Required" | ||
- "+k8s:validation:required" |
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 was also thinking about the naming and couldn't come up with something I was absolutely happy with, this is a hard case to name 🤔
I could see first
and second
working, but does that imply some sort of ordering? Where I don't think there actually is any ordering?
} | ||
|
||
// validateConfig validates the configuration in the config.ConflictingMarkersConfig struct. | ||
func validateConfig(cfg *ConflictingMarkersConfig, fldPath *field.Path) field.ErrorList { |
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.
We should probably validate that there is at least one item in the conflict set shouldn't we? Adding this with no config doesn't make sense
Name: name, | ||
Doc: "Check that fields do not have conflicting markers from mutually exclusive sets", |
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.
Nit: make this optionally configurable so we can easily change this in future wrappers of this analyzer
conflictSets []ConflictSet | ||
} | ||
|
||
func newAnalyzer(cfg *ConflictingMarkersConfig) *analysis.Analyzer { |
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.
Export this so other distinct linters can wrap this in the future?
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.
In the example I provided, I wrapped the Initializer
function rather than this function, I don't think this needs to be exported does it?
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.
Ah, I see.
What does the output of that linter look like if the initializer uses one name but the analyzer that is returned uses a different name?
In the example you provided, I suspect that the initializer properly returns that it is for the defaultorrequired
linter, but the analysis.Analyzer
returned would have the name conflictingmarkers
. If this is true and both names (or just the analysis.Analyzer
name) manifest in the output, it might be confusing to users that have disabled the conflictingmarkers
linter but see that name pop up if they have the defaultorrequired
linter enabled.
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.
AFAICT the name is only used on the outer layer, we see the name of the Initializer
in the linter line before the message, but, because we run the initialization and then wrap the initialized Analyzer, we override the name of the package output Initializer
on the new implementation, so, I believe this is safe
|
||
for i, conflictSet := range cfg.Conflicts { | ||
if nameSet.Has(conflictSet.Name) { | ||
fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("conflicts").Index(i).Child("name"), conflictSet.Name, "repeated value, names must be unique")) |
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 field.Duplicate()
function may be more appropriate here.
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.
Good catch, updated.
60f7f99
to
8bdfc41
Compare
8bdfc41
to
ed134ff
Compare
@@ -17,6 +18,7 @@ | |||
- [StatusOptional](#statusoptional) - Ensures status fields are marked as optional | |||
- [StatusSubresource](#statussubresource) - Validates status subresource configuration | |||
- [UniqueMarkers](#uniquemarkers) - Ensures unique marker definitions | |||
- [ConflictingMarkers](#conflictingmarkers) - Detects and reports when mutually exclusive markers are used on the same field |
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 entry is now in the list twice, and it appears we have the documentation twice too?
// Use configured documentation or fall back to default | ||
doc := cfg.Doc | ||
if doc == "" { | ||
doc = "Check that fields do not have conflicting markers from mutually exclusive sets" | ||
} |
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.
Oh interesting, hadn't thought about this, when we wrap the analyzer this makes sense doesn't it
LGTM one we've fixed that docs issue of seeing double, thanks for working on this one @yongruilin |
// Doc is the documentation string for the analyzer. | ||
// If not provided, a default description will be used. | ||
Doc string `json:"doc"` |
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.
Is this leaking implementation details into user-facing configuration options?
For example, as an end user I would now be able to configure this in a way that the conflicting markers analyzer uses a custom doc string.
I would generally think this is something that we don't want to allow users to change in configuration.
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.
Where does this doc string actually show up? Does it matter if analyzers wrapping this one don't have the option to change it?
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 believe it would show under the output of golangci-lint help linters
. I do not know if golangci-lint picks up the analyzers from the custom linter config though
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.
Can we add a test for the multi-way conflict that ensures we identify a conflict when only a subset of the conflicts are triggered?
In this case I would expect to see a test where there is conflicting markers from 2 sets in the 3-way conflict set still trigger the 3-way conflict.
Ref: #97
Adds a new configurable linter that detects mutually exclusive markers. The linter allows users to define custom conflict sets and is not enabled by default.