feat(helm): support allowable errors for helm chart retries - #5287
Conversation
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
✅ Deploy Preview for zarf-docs canceled.
|
Codecov Report❌ Patch coverage is
... and 5 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
| return false | ||
| } | ||
|
|
||
| if wrapped, ok := err.(interface{ Unwrap() []error }); ok { |
There was a problem hiding this comment.
There are several problems in this entire block, and the next function:
- The rules for errors are such that you'd need to match both signatures
Unwrap() []errorandUnwrap() error, see https://pkg.go.dev/errors#pkg-overview. I've tried with usingfmt.Errorf("...%w...", ...err...)and that matched onlyUnwrap() error. - It's easier to just use AsType instead, which does the entire work for you. Here it will simplify this and the next method to look like so:
apiStatus, ok = errors.AsType[*apierrors.APIStatus](err)
if !ok {
return false
}
status := apiStatus.Status()
return status.Reason == metav1.StatusReasonInternalError && strings.Contains(status.Message, "failed calling webhook")There was a problem hiding this comment.
Agree on covering both Unwrap() []error and Unwrap() error - I added an additional test case for coverage with one nested through fmt.Errorf("%w") - otherwise I think we have coverage on the other cases.
My understanding was that errors.As / errors.AsType returns the first matching node and the policy I was looking to implement was more closely aligned with "all leaves must be allowed". AsType would report a match for a webhookFailure that also happens to be joined with another terminal failure.
Certainly open to thoughts - my goal was to ensure we're conservative with what is allowed and then loosen/add as required.
There was a problem hiding this comment.
Certainly open to thoughts - my goal was to ensure we're conservative with what is allowed and then loosen/add as required.
I believe here, the same rule applies you've mentioned in the other comment. Whenever errors are wrapped like NewInternalError the double wrapping will not happen, since the internal error will be printed as a string in the details of the message. So I believe, here, it'll be simpler for us to just find the first and only errors.APIStatus. It will also make the code simpler to read 😉
There was a problem hiding this comment.
This can be done in a followup.
| return false | ||
| } | ||
| status := apiStatus.Status() | ||
| return status.Reason == metav1.StatusReasonInternalError && strings.Contains(status.Message, "failed calling webhook") |
There was a problem hiding this comment.
I'm somehow worried about these string matching errors, they are not reliable enough, imo. Looking deeper, how about instead of matching apistatus trying to match ErrCallingWebhook instead? Or both?
There was a problem hiding this comment.
I was digging through this some more. If I understand correctly ErrCallingWebhook would be better if it survived to the client, but it looks like the api-server might wrap that internal type with apierrors.NewInternalError before serializing the response. Are you aware of a typed signal for this path I might have missed?
There was a problem hiding this comment.
You're right, it is only wrapped under apierrors.NewInternalError sadly.
| if errors.Is(histErr, driver.ErrReleaseNotFound) { | ||
| // No prior release, try to install it. | ||
| l.Info("performing Helm install", "chart", zarfChart.Name) | ||
| if errors.Is(histErr, driver.ErrReleaseNotFound) { |
There was a problem hiding this comment.
I'm not a helm expert, but codex suggest that a failed, retried releases might not return an error, but rather a failed release instead, which means this code will not go the install path, but rather upgrade one. This can potentially skip some install-related steps. Should we also look at the release status and check if it's reporting StatusFailed?
There was a problem hiding this comment.
I think this is a tricky balance - if we fail during install then the next retry will attempt upgrade, skipping the install specific steps. But if we implement a replace for install on retry then we could retry already executed install steps.
GIven we're narrowing when a retry can occur (webhook failed) - my inclination is that the current semantics should generally be okay. If we were retrying chart installs regardless of error type then we might have to think on this further.
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
| return false | ||
| } | ||
| status := apiStatus.Status() | ||
| return status.Reason == metav1.StatusReasonInternalError && strings.Contains(status.Message, "failed calling webhook") |
There was a problem hiding this comment.
You're right, it is only wrapped under apierrors.NewInternalError sadly.
| return false | ||
| } | ||
|
|
||
| if wrapped, ok := err.(interface{ Unwrap() []error }); ok { |
There was a problem hiding this comment.
Certainly open to thoughts - my goal was to ensure we're conservative with what is allowed and then loosen/add as required.
I believe here, the same rule applies you've mentioned in the other comment. Whenever errors are wrapped like NewInternalError the double wrapping will not happen, since the internal error will be printed as a string in the details of the message. So I believe, here, it'll be simpler for us to just find the first and only errors.APIStatus. It will also make the code simpler to read 😉
| return false | ||
| } | ||
|
|
||
| if wrapped, ok := err.(interface{ Unwrap() []error }); ok { |
There was a problem hiding this comment.
This can be done in a followup.
…#5287) Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com> Signed-off-by: Tim Seagren <timseagren@defenseunicorns.com>
Description
Adds support for a retry on helm chart installations when the failure isn't deterministic. There may be many but the first recorded one is the presence of an admission/mutation webhook and the inability to send requests as the endpoint may not be entirely reconciled by kubernetes networking when attempting to deploy resources that then call that webhook.
I looked into whether the 500 error returned might include a
Retry-Afterheader that we look for on 429 responses but it was not there (and probably rightfully so).Related Issue
Fixes #5286
Checklist before merging