-
Notifications
You must be signed in to change notification settings - Fork 31
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
⚠️ types tightening #384
base: main
Are you sure you want to change the base?
⚠️ types tightening #384
Conversation
6d3e91c
to
de1f42e
Compare
de1f42e
to
b8cbda9
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #384 +/- ##
==========================================
- Coverage 36.05% 35.36% -0.69%
==========================================
Files 14 14
Lines 796 803 +7
==========================================
- Hits 287 284 -3
- Misses 463 472 +9
- Partials 46 47 +1 ☔ View full report in Codecov by Sentry. |
@@ -164,6 +164,9 @@ type CatalogSource struct { | |||
} | |||
|
|||
// ResolvedCatalogSource is a discriminated union of resolution information for a Catalog. | |||
// ResolvedCatalogSource contains the information about a sourced Catalog | |||
// +union | |||
// +kubebuilder:validation:XValidation:rule="self.type != 'Image' || has(self.image)",message="sourceType 'Image' requires image 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.
I think we may want something more like:
// +kubebuilder:validation:XValidation:rule="self.type != 'Image' || has(self.image)",message="sourceType 'Image' requires image field" | |
// +kubebuilder:validation:XValidation:rule="self.type == 'Image' && has(self.image)",message="sourceType 'Image' requires image field" |
This ensures that we return "True" for the validation only if the type is Image
and the image
field is set.
IIUC, the current validation of self.type != 'Image' || has(self.image)
returns "True" in scenarios that would be invalid:
type
is set toFoo
(not currently possible based on enums, but for sake of ensuring we have a solid CEL validation assume we can) and theimage
field is settype
is set toImage
and theimage
field is unset
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 ran into some issues when using self.type == 'Image' && has(self.image)
, mostly when I tried validating with an unsupported image type and when I tried adding another supported sourceType
to test extensibility.
the +kubebuilder:validation:XValidation
tag requires all its validation checks to be true, so we were failing on the self.type=='Image'
part of the check even when the source type was something else. I added the self.type != 'Image'
so we wouldn't be doing that check on non-image sources.
I've added another validation to ensure we don't have the image
field on non-image sources to cover the case you highlighted, thanks for pointing that out! We'll still fail on non-supported sources because of the enum check, but this won't throw the type 'Image' requires image field
error when the source type isn't 'Image'
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 ran into some issues when using self.type == 'Image' && has(self.image), mostly when I tried validating with an unsupported image type and when I tried adding another supported sourceType to test extensibility
I think this is something that I would expect based on our allowed enum values.
the +kubebuilder:validation:XValidation tag requires all its validation checks to be true, so we were failing on the self.type=='Image' part of the check even when the source type was something else. I added the self.type != 'Image' so we wouldn't be doing that check on non-image sources.
I'm a bit confused here. Are you saying that it the XValidation tag doesn't evaluate the expression holistically but instead each individual expression within a chain? This sounds off to me - do you have any documentation or references you've found that state that is the case?
If I'm not mistaken, what seems more likely here is that the validations are failing on the fact that we only allow the Image
value as part of our enum checks so we don't even hit the CEL validations.
Holistically I would expect the following test cases to fail, but not hit our CEL validations until more than the Image
value is allowed:
- Attempting to set type to something other than
Image
- Attempting to set type to something other than
Image
AND setting image field
For ^ I would avoid checking for specific error messages for now and just ensuring that we encounter an error.
One that I would expect us to fail from the CEL validation:
- Attempting to set type to
Image
but nil image field
One that I would expect to succeed:
- Attempting to set type to
Image
and set image field
I do think we should be able to satisfy all these cases with a singular CEL expression with the current state of our API. Future iterations to the API that add new source types will likely need to update the CEL validations so I would avoid trying to accommodate future use cases of different source type values in the CEL introduced as part of this PR
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 this is something that I would expect based on our allowed enum values.
I'd added another allowed value to our enums and regenerated manifests to test this case
// +kubebuilder:validation:Enum:="Image";"someothertype"
Are you saying that it the XValidation tag doesn't evaluate the expression holistically but instead each individual expression within a chain?
It seems that way from https://book.kubebuilder.io/reference/markers/crd-validation
// +kubebuilder:validation:XValidation
marks a field as requiring a value for which a given expression evaluates to true.
This marker may be repeated to specify multiple expressions, all of which must evaluate to true.
I verified this with the test I mentioned earlier. A catalog with the type "someothertype" would fail with or without the image
field with
The ClusterCatalog "testcatalogothertype" is invalid: spec.source: Invalid value: "object": source type 'Image' requires image 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.
When we add another sourceType in the future, we will have to modify this expression. e.g.
(self.type == 'Image' && has(self.image)) || (self.type == 'Foo' && has(self.foo))
But since there's just one enum value now, I don't think we need to worry about making the CEL expression future proof.
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.
// +kubebuilder:validation:Enum:="Image";"someothertype"
@ankitathomas I don't think this is a valid validation tag. I think that it is supposed to be:
// +kubebuilder:validation:Enum:="Image,SomeOtherType"
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.
// +kubebuilder:validation:Enum:="Image,SomeOtherType"
That seems incorrect, the manifest generated that way considers the quoted value to be a single string, so it treats the valid value as "Image,SomeOtherType" rather than "Image" and "SomeOtherValue"
The ClusterCatalog "operatorhubio2" is invalid: spec.source.type: Unsupported value: "SomeOtherType": supported values: "Image,someothertype"
I also tried with // +kubebuilder:validation:Enum:="Image","someothertype"
, that fails to generate the manifest with Error: not all generators ran successfully
// +kubebuilder:validation:Enum:="Image";"someothertype"
works correctly though, if the CEL validation is removed, the catalog manifest with the "someothertype" applies without any issues
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.
(self.type == 'Image' && has(self.image)) || (self.type == 'Foo' && has(self.foo))
I think we won't be able to tailor our validation errors if we make the CEL validation one long validation for all types. We'd also be throwing the CEL validation error when we encounter an unsupported type
apiVersion: olm.operatorframework.io/v1alpha1
kind: ClusterCatalog
metadata:
name: operatorhubio2
spec:
source:
type: unsupported
The ClusterCatalog "operatorhubio2" is invalid:
* spec.source.type: Unsupported value: "unsupported": supported values: "Image", "Foo"
* spec: Invalid value: "object": no such key: image evaluating rule: cannot specify PollInterval while using digest-based image
* spec.source: Invalid value: "object": source type 'Image' requires image field
Not sure why it's throwing an error with the PollInterval though.
This was the expression set preserves a little more info about validation errors:
// +kubebuilder:validation:XValidation:rule="self.type != 'Image' || has(self.image)",message="source type 'Image' requires image field"
// +kubebuilder:validation:XValidation:rule="self.type == 'Image' || !has(self.image)",message="image field must only be set for source type 'Image'"
// +kubebuilder:validation:XValidation:rule="self.type != 'Foo' || has(self.foo)",message="source type 'Foo' requires foo field"
// +kubebuilder:validation:XValidation:rule="self.type == 'Foo' || !has(self.foo)",message="foo field must only be set for source type 'Foo'"
an unsuported type gives the following error set:
The ClusterCatalog "operatorhubio2" is invalid:
* spec.source.type: Unsupported value: "unsupported": supported values: "Image", "Foo"
* spec: Invalid value: "object": no such key: image evaluating rule: cannot specify PollInterval while using digest-based image
A catalog with an invalid field gives something like:
kind: ClusterCatalog
metadata:
name: operatorhubio2
spec:
source:
type: Foo
image:
ref: ""
The ClusterCatalog "operatorhubio2" is invalid:
* spec.source: Invalid value: "object": image field must only be set for source type 'Image'
* spec.source: Invalid value: "object": source type 'Foo' requires foo field
We can combine the validations for each type so we don't end up too verbose
// +kubebuilder:validation:XValidation:rule="(self.type != 'Image' || has(self.image)) && (self.type == 'Image' || !has(self.image))",message="source type 'Image' requires image field"
// +kubebuilder:validation:XValidation:rule="(self.type != 'Foo' && !has(self.foo)) || (self.type == 'Foo' && has(self.foo))",message="source type 'Foo' requires foo 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.
Ah I didn't realize we could have multiple validation rules like that, but I guess it makes sense. That's nicer because we can tailor the message for each one.
If we want to consolidate to a single rule, we can use messageExpression
instead of message
in order to use CEL to build a message. But I'm not sure that would give us the flexibility we could need to craft a helpful validation message.
0d394ca
to
1d2538c
Compare
A summary of changes :
|
…lidation for spec.source and status.resolvedSource Signed-off-by: Ankita Thomas <[email protected]>
1d2538c
to
28e951a
Compare
status.ResolvedSource = result.ResolvedSource | ||
status.ContentURL = contentURL | ||
status.ObservedGeneration = generation | ||
status.LastUnpacked = unpackedAt | ||
status.LastUnpacked = result.LastTransitionTime | ||
meta.SetStatusCondition(&status.Conditions, metav1.Condition{ |
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.
Should we be setting observedGeneration inside the condition struct?
return false | ||
} | ||
if _, isDigestBased := imgRef.(reference.Canonical); isDigestBased && | ||
catalog.Spec.Source.Image.Ref != catalog.Status.ResolvedSource.Image.Ref { |
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 have a bug
issue that captures that we are reading from our own status? IMO, we should instead be storing something in our cache that includes its digest, and we compare with that.
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.
would it be easier to drop the spec.image vs resolved image comparison here and re-implemented later when we optimize our caching?
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 we're better off keeping it vs removing it. But something I think we should come back to and improve.
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 couldn't find an issue for this specifically; created #420 to track it.
} | ||
// time to unpack | ||
return true | ||
nextPoll := catalog.Status.ResolvedSource.Image.LastSuccessfulPollAttempt.Add(catalog.Spec.Source.Image.PollInterval.Duration) |
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.
Another place that we are reading from status. We can probably fold this into the other bug issue I mentioned. Another thing that we should store/lookup from our cache.
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 something we can fold into our reconcile intervals? If we set the requeuing interval to the polling interval, would we effectively get the same behaviour?
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're using nextPoll
to set the RequeueAfter already I think. The problem is that we don't want to poll every time we reconcile and we also don't want to reset our timer and wait too long. So we need reconciles (R) and polls (P) that look like the following:
poll1 now poll2
-----RP-------------R---------R--------------RP
0 1 2 3 4 5 6 7 8 9
If our interval is 8 and we reconcile and poll at time 1, we don't want to poll again until time 9, even if we reconcile in the meantime. So if we reconcile again at time 4, we need to calculate a RequeueAfter
based on the current time, the last successful poll time and the interval.
In this case, we want:
(now - poll1) + requeueAfter = interval
requeueAfter = interval - now + poll1
requeueAfter = poll1 + interval - now
requeueAfter = 1 + 8 - 4
requeueAfter = 5
If we reconcile again at time 6, that's too early again, so we need to recalculate requeueAfter
(this time, it would be 3) so that we're always timing for another reconcile to happen at the poll interval.
This particular case of reading from status isn't that bad because the worst case scenario is that we fail to update status after a successful poll attempt. If that happens, we'll end up re-polling the image registry every single reconcile until status update succeeds.
If we read from an in-memory map, out lastSuccessfulPoll map would not survive restarts, so we would end up re-polling every single ClusterCatalog at cluster startup. This concern is partially why we're using jitter to slowly spread poll times of different ClusterCatalogs around, even if they have the same interval.
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.
That makes sense, I was curious because it seemed like we don't do anything apart from setting finalizers on catalogs that aren't ready for polling. Once we move to an async reconcile, we'd want to reconcile more frequently than our polling interval.
I've created #420 to track this and the previous bug you pointed out
internal/source/containers_image.go
Outdated
Message: fmt.Sprintf("unpacked %q successfully", canonicalRef), | ||
State: StateUnpacked, | ||
Message: fmt.Sprintf("unpacked %q successfully", canonicalRef), | ||
LastTransitionTime: lastUnpacked, |
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.
From the perspective of this library, this is timestamp for when we unpacked what we have. So I'd suggest UnpackTime
for the name of this field.
LastTransitionTime: lastUnpacked, | |
UnpackTime: lastUnpacked, |
@@ -381,8 +381,8 @@ func TestImageRegistry(t *testing.T) { | |||
|
|||
rs, err := imgReg.Unpack(ctx, tt.catalog) | |||
if !tt.wantErr { | |||
require.NoError(t, err) | |||
assert.Equal(t, fmt.Sprintf("%s@sha256:%s", imgName.Context().Name(), digest.Hex), rs.ResolvedSource.Image.ResolvedRef) | |||
assert.NoError(t, err) |
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 probably needs to stay as require
. Otherwise the dereferencing fields on rs
will panic.
internal/source/unpacker.go
Outdated
@@ -54,6 +56,9 @@ type Result struct { | |||
// Message is contextual information about the progress of unpacking the | |||
// catalog content. | |||
Message string | |||
|
|||
// LastTraansitionTime is the timestamp when the transition to the current State happened | |||
LastTransitionTime metav1.Time |
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 this should be a regular timestamp. We can convert to metav1.Time
when we're in the kubernetes object domain. This library isn't in that domain.
LastTransitionTime metav1.Time | |
UnpackTime time.Time |
…edGeneration in conditions
Solves #381