-
Notifications
You must be signed in to change notification settings - Fork 2k
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
cli: deprecate cli.StatusError
direct field usage
#5666
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5666 +/- ##
==========================================
+ Coverage 59.42% 59.47% +0.04%
==========================================
Files 347 348 +1
Lines 29402 29416 +14
==========================================
+ Hits 17472 17495 +23
+ Misses 10958 10948 -10
- Partials 972 973 +1 |
b6c8353
to
d7c81a3
Compare
9275a27
to
a20d0c8
Compare
@thaJeztah @vvoland do you have any concerns that would prevent this PR from getting merged? |
I'm also not sure if we should be changing the StatusError.. Rather than leaving the whole PR sitting while we try to decide, i think we can split that out and at least merge the ctx cancellation bits which are good to have and improve the ux. WDYT? |
We can separate it (it already is split up between 2 commits), but the UI will still print the error "user terminated the process" since the For example: ⋊> ~/G/cli-3 on 5f221783c ./build/docker run postgres:latest 12:49:47
Unable to find image 'postgres:latest' locally
latest: Pulling from library/postgres
fd674058ff8f: Pulling fs layer
1eab12a50bdf: Pulling fs layer
5a81b4aedb94: Pulling fs layer
502eeeb4a17b: Waiting
e9e19177b318: Waiting
2068838cf5fa: Waiting
45a271dbb114: Waiting
8f9ac4ec849d: Waiting
9d8b60e88ddb: Waiting
3ec4ef471804: Waiting
16d755b48cd4: Waiting
3d5d11fb541c: Waiting
d8ab5fe30360: Waiting
d19370fe7a12: Waiting
^Cdocker: user terminated the process
Run 'docker run --help' for more information
⋊> ~/G/cli-3 on 5f221783c The only option to not have You can try it for yourself, |
There are other options that aren't perfect, but work in the interim, such as @thaJeztah's solution here. Would require a pass over the codebase to look at all of the other places, but that's probably fine and something that can be done iteratively. There's always string matching too 😅 ...
if err != nil && !errdefs.IsCancelled(err) && !errors.Is(err, errCtxUserTerminated) {
... do ...
if err != nil {
// FIXME: replace this with errdefs.IsCancelled after changing StatusErr
if err.Error() != "context cancelled" {
... |
I also wrestled with this a bit and I still came to the conclusion that having it match with The "best" solution I can come up with at this point in time is to replace |
f9c3ab4
to
bed4155
Compare
I've updated the code to use a new error called Now the question is:
|
Yeah, I see a few instances (~250, but could be more if importing under other names) – including @ndeloof in swarmctl – of people doing this around Github, but that's a lot smaller so I'm less concerned. I'll leave it up to @thaJeztah and folks as to how acceptable that is. Perfect is the enemy of good, and I think in projects such as these compromises need to be made sometimes to make the current situation better without breaking other things, which is why I don't think string matching as a stop-gap (that can be reverted on the next major when As long as |
Yeah I mean, there are only three options here (I'm okay choosing any of these):
Currently the last commit introduces the third option, but we can drop it and just go with option 1. |
FYI @Benehiko that |
To be clear, the best solution is clearly changing We could simply add a field to the // StatusError reports an unsuccessful exit by a command.
type StatusError struct {
Cause error
// Deprecated: use Cause instead.
Status string
StatusCode int
} And use that in the CLI codebase from now on, and then remove the |
bed4155
to
c0065d2
Compare
c0065d2
to
e575ef9
Compare
context.Context
I split the PR. This PR has been updated to only include the "cause" of the context.Context cancellation. The other PR focuses on the |
e575ef9
to
b0b1748
Compare
b0b1748
to
e9ed787
Compare
context.Context
cli.StatusError
direct field usage
The exported `cli.StatusError` type may be used by some directly within their own projects, making it difficult to update the struct's fields. This patch converts the exported `cli.StatusError` to an interface instead, so that code wrapping the CLI would still be able to match the error and get the status code without exposing the fields. This is a breaking change for those relying on creating a `cli.StatusError{}` and accessing the error's fields. For those using `errors.As(err, &statusError)` and `err.(cli.StatusError)` will be able to continue using it without breakage. Users accessing the fields of `cli.StatusError{}.StatusCode` would need to use the new `GetStatusCode()` method. Signed-off-by: Alano Terblanche <[email protected]>
e9ed787
to
a7d43e3
Compare
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 will prevent CLI plugins to manage exit code from docker CLI - which is required by compose --exit-code-from
Apart avoiding types in public API vs interfaces, which is indeed a better approach, what's the issue you're trying to fix with this breaking change ?
if errors.As(err, &stErr) { | ||
_, _ = fmt.Fprintln(dockerCli.Err(), stErr) |
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 you make StatusError
internal, a CLI plugin won't be able to create such an error, and control the exit code
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.
hmm, as an interface it would force the plugin to implement their own error type and not rely on the struct. 🤔 - I'm wondering though if this is what we should do since plugins are essentially being forced to use the struct directly which to me seems bad as it makes it more difficult for the CLI to make any changes in the longer term and even more difficult knowing what contracts we actually have that we should respect.
The main case was to change the cli.StatusError{}.Status
field to be of type error
instead of type string
. The reason being we want to be able to use errors.Is
to match children of this error which is impossible with a string
type for Status
.
The second reason is to convert this to an interface so that third parties don't depend on this error type directly in their code. We are going to break third parties by changing cli.StatusError{}.Status -> cli.StatusError{}.Cause
in any case so I took the opportunity to try and do it in one go instead of having to go through multiple such breaking changes in the future.
package main
import (
"errors"
"fmt"
)
type StatusError struct {
Status error
Code int
}
func (s StatusError) Error() string {
return s.Status.Error()
}
// NEED this to get the child
func (s StatusError) Unwrap() error {
return s.Status
}
type barError struct{}
func (b barError) Error() string {
return "I am bar error"
}
func bar() error {
return barError{}
}
func foo() error {
err := bar()
return StatusError{
Status: fmt.Errorf("I am wrapping this error: %w", err),
Code: 125,
}
}
func main() {
err := foo()
if errors.Is(err, barError{}) {
fmt.Printf("Is bar error: %s\n", 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.
Why not just add a new Cause
attribute to StatusError
and implement Unwrap() error
? Doing so you can use errors.Is
as long as cause has been set, which would have lower impact - as this change requires major changes anyway
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 this option can be pursued which would make things easier in the near term.
My goal was to try to create a clear boundary for third parties to use instead - whilst we get to do a major release (v28). Since we didn't clearly define what "internal" code is in the past, we now have this situation where changing the CLI code can come with a lot of unknown pitfalls.
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 guess you could say I'm trying to kill two birds with one stone.
- match child error types
- reduce technical debt
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.
understood, but impact is huge for our ecosystem.
keeping code clean is easier when you have no users.
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.
Okay, I'll update the code. Perhaps we could do a slow transition to an exported interface?
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.
@ndeloof i pushed some changes in a separate commit, let me know what you think
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.
my preference goes for #5778, as lower impact and simplest solution for the need
This patch ensures that plugins can still use `cli.StatusError` and won't need to modify anything in the short term. In the longer term we should deprecate `cli.StatusError{}.Status` and instead only have `cli.StatusError{}.Cause`. A common interface is introduced for both `cli.StatusError` and `internal.StatusError` so that we can still use `errors.As` no matter which error is returned. Signed-off-by: Alano Terblanche <[email protected]>
The exported
cli.StatusError
type may be usedby some directly within their own projects, making it difficult to update the struct's fields.
This patch converts the exported
cli.StatusError
to an interface instead, so that code wrapping the CLI would still be able to match the error and get the status code without exposing the fields.This is a breaking change for those relying on creating a
cli.StatusError{}
and accessing the error's fields. For those usingerrors.As(err, &statusError)
anderr.(cli.StatusError)
will be able to continue using it without breakage.Users accessing the fields of
cli.StatusError{}.StatusCode
would need to use the newGetStatusCode()
method.- What I did
- How I did it
- How to verify it
- Description for the changelog
- A picture of a cute animal (not mandatory but encouraged)