-
Notifications
You must be signed in to change notification settings - Fork 52
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
Handle errors when couldn't download config or compose #247
Conversation
d2c845e
to
abd0fbe
Compare
cmd/sourced/compose/file/file.go
Outdated
@@ -98,7 +102,13 @@ func InitDefaultOverride() (string, error) { | |||
|
|||
// Download downloads the docker-compose.yml file from the given revision | |||
// or URL. The file is set as the active compose file. | |||
func Download(revOrURL RevOrURL) error { | |||
func Download(revOrURL RevOrURL) (err error) { | |||
defer func() { |
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 know how much I like the usage of defer
to just wrap an error. Is this an idiomatic go thing? I mean I'd find the usefulness of this if you want to also recover from panic and maybe also wrap the panic error. But in this case, maybe a simple wrapper function would do the trick?
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 you mean something like the following?
func Download(revOrURL RevOrURL) error {
err := downloadWrappedErr(revOrURL)
if err != nil {
return ErrCouldNotGetComposer.Wrap(err)
}
return nil
}
func downloadWrappedErr(revOrURL RevOrURL) error {
// original content of Download(RevOrURL) error
}
It seems ugly imo
On the other hand, if I understood the proposal for try
, handling and wrapping errors in defer
seems to be something proposed by go team, isn't it?
We advocate using the existing defer statement and standard library functions to help with augmenting or wrapping of errors.
Does not seems a clean and explicit way to handle and wrap these errors?
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.
yup, but that proposal has been closed and rejected due to a lot of criticism AFAIU.
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.
BTW I was thinking something as follows:
func Download(revOrURL RevOrURL) error {
wf := func(err) {
if err != nil {
return ErrCouldNotGetComposer.Wrap(err)
}
return nil
}
...
return wf(err)
...
return nil
}
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.
But I'm not a golang expert, so I really cannot strongly advocate some styles over others.
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 goal of defer
is to ensure that Download
returns a ErrConfigDownload
, no matter where it's called.
If not used the defer
, I see three alternatives:
- a) make sure we wrap the returned error wherever
Download
is called, as I understood from @smacker Handle errors when couldn't download config or compose #247 (comment),what is error-prone imo, because// in cmd/sourced/compose/file/file.go func InitDefault() (string, error) { ... err := Download(version) if err != nil { return "", ErrConfigDownload.Wrap(err) } return activeFilePath, nil } // in cmd func (c *composeDownloadCmd) Execute(args []string) error { ... err := Download(version) if err != nil { return ErrConfigDownload.Wrap(err) } return nil }
Download
can be called elsewhere tomorrow, so then it could be not wrapped. - b) make sure that we wrap all the returned errors inside
Download
, what is repetitive and can be also error-prone if a new return is added later and not wrapped. That's what I understood from @se7entyse7en's Handle errors when couldn't download config or compose #247 (comment) - c) do as suggested by Handle errors when couldn't download config or compose #247 (comment), moving the current content of
Download
into a new (and unexported) function which will do the actual work, and then call it from (the exported)Download
, and wrap and return its error.
This would be bulletproof and not error-prone (if the new unexported function is not directly called from the same package elsewhere), but as I said, I find this procedure ugly and could be an anti-pattern.
If you strongly oppose to deferring the error handling, and you don't suggest another option, I'd vote for b as @se7entyse7en suggested, because even being error-prone as I said, the risk is limited to the scope of the same function.
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 also like option c) actually, and I don't think it's anti-pattern. For example have a look at the code of net/http
in client.go
here. It uses an expored method that only calls an unexported one. In this case there's no error handling, but I don't think would be considered an anti-pattern.
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 is error-prone imo, because Download can be called elsewhere tomorrow, so then it could be not wrapped.
you assume that caller from the different location must behave exactly like the current one, which isn't true.
make-up example:
err := Download(url)
// with current code I have to know that `Download` returns `go-error` type
// which I must inspect using `Case()` to make this check
if err == NetworkError {
return LocalFallbackUpdate()
}
// with current code produces weird text like
// "can't set new config: can't download config: no permission to change active config"
return ErrCanNotSetNewConfig.Wrap(err)
As you can see the error is just incorrect. It could download the config. It failed on SetActive
.
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.
@se7entyse7en jfyi we discussed it offline and I found a better explanation which David agreed with. With current state of go 1.12 you usually return errors "one level less". Let me give you an example:
func downloadFile() error {
// unwrapped
return err
}
func setComposeFIle() error {
// unwrapped
return err
}
func UpdateCompose() error {
if err := downloadFile(); err != nil {
return ErrDownload
}
if err := setComposeFIle(); err != nil {
return ErrSetComposeFile
}
}
You don't need to wrap an error in the function downloadFile
as the caller already knows it is download error. That information comes from the name of the function and UpdateCompose
function can inspect if it was network or fs error and do something else (like fallback) using only current features of the language without pulling external lib like go-error
into the codebase. On another hand UpdateCompose
(if it makes sense) wraps error underlying errors to allow the caller distinguish what happened. (without wrap you don't know if fs error was caused by downloading of set)
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 the explanation! Seems really clear!
If you prefer a local installation of Docker Compose, you can follow the [Docker Compose install guide](https://docs.docker.com/compose/install) | ||
If you prefer a local installation of Docker Compose, or you have no access to internet to download it, you can follow the [Docker Compose install guide](https://docs.docker.com/compose/install) | ||
|
||
## Internet Connection |
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.
Most people will not need this completely offline use case. And for people behind a firewall, I assume most of the times they will make it work configuring their firewall rules.
I think this section is a bit out of place in the quickstart, and it can be confusing to explain a use case that is not the common one, or supported.
What do you think about moving this to the FAQ? This way we have a place to link if somebody asks, but it's not distracting to first time 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.
I'm not strongly against moving this into our FAQ, but I really see the connection as a requirement to be made explicit when the user is installing source{d}.
We both agree that the intended use of source{d} requires Internet, but in the last few weeks, we found a couple of people having this very same problem #206 #241, what I think it's legit because of the target of this tool (being used by engineers to try its features, to be applied in big corps, where there can be firewalls or other net limitations).
On the other side, this doc does not explain how to use the tool without net connectivity, but only list the resources to be requested from the net, that should be satisfied if there is no connectivity available.
From the reader pov, I'd find easy to skip the whole section if they have connectivity, and just read it if they don't (what might be interesting considering what was exposed above).
So, if the goal is to have a shorter quickstart as possible, and you agree on the importance of not considering Internet as a supposed resource, would you find better if we just:
- mention the connectivity requirement in this section, and
- move the extra details about the required resources & what to do if not available to the FAQ.
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 view in general is that for docs, less is more. In a way I think that documenting too much is similar to feature creep in the code. This section is not bad. It's not misleading or confusing. But if we go this direction, things add up, and then it becomes noise in my opinion. Should we also include in the quickstart that you need to make sure the ports we'll try to use are free? storage requirements? Or taking it to the extreme: should we explain custom deployments, like for example how deploy connecting to a postgresql already deployed in your company network?
For the issues you link:
#206 already contains the error Get https://.... docker-compose.yml: Forbidden
. The documentation here does not add any extra information that would help to figure out how to configure the firewall.
In #241 the problem was that they could manually provide the file sourced-ce
needed, but didn't know where to copy it manually. The improved error messages from this PR would be enough to solve their problem.
In this case this section would also not provide a solution, we say you will need to provide the file manually but don't document where exactly. Also, the docs here link to the docker-compose.yml from master, and that's not correct, it can lead to the deployment not working as expected if the user places this file manually in their ~/.sourced
dir.
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.
About #206, when they tried to initialize sourced init local .
before:
Get https://raw.githubusercontent.com/src-d/sourced-ce/v0.14.0-rc.2/docker-compose.yml: Forbidden
The user doesn't know why is that file needed, and how to fix it to be used without internet (because it is not said to be a requirement)
now:
[...]: error downloading https://raw.githubusercontent.com/src-d/sourced-ce/master/docker-compose.yml to /.sourced/compose-files/master/docker-compose.yml: [...]
The source{d} CE config file could not be downloaded. see docs about internet requirement.
The error message now explains which kind of file is failing and points to docs explaining that internet connection is a requirement.
The error also explains where should be placed the file (which could be used by a ninja engineer to provide that file)
As agreed, the purpose of the docs is not to explain how to run source{d} offline, but at least explain clearly what and why is failing.
What we had before, already let the user know that a file could not be downloaded, but it seems it was not enough, because they felt the necessity of opening an issue, I guess if it was because they didn't assume that source{d} was not meant to be run offline.
About #241 it's kind of the same. It was needed three interactions to realize that the problem was a lack of connectivity, so I'd say that this assumption might be too much for our users.
For sure that if we start having questions/problems about port conflicts, we'll have to document or provide an alternative, but we can manage that problem when it happens.
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.
As agreed, the purpose of the docs is not to explain how to run source{d} offline, but at least explain clearly what and why is failing.
But the docs here don't explain what and why is failing. The error contains more info that the docs: trying to access the internet, the exact URL, the destination file, and the error.
For #206 the docs don't mention firewalls or Forbidden
errors, or the destination path for the file. Even the URL is wrong, it points to master when it will be http...<version>/docker-compose.yml
.
About #241 it's kind of the same. It was needed three interactions to realize that the problem was a lack of connectivity, so I'd say that this assumption might be too much for our users.
Of course they knew they needed connectivity to download a file! The problem was not that, the problem was that they didn't know where to place the file that was provided manually:
Edit: Also, I have in my notes that he tried fetching run.sh separately, but could not figure out where to put it, so that it would work correctly with the other files.
In this case, the new errors from this PR would solve the problem. But the docs don't mention the URL or destination path for the missing file, so it would not help in this case either.
For sure that if we start having questions/problems about port conflicts, we'll have to document or provide an alternative, but we can manage that problem when it happens.
I was actually arguing for the exact opposite... to not document every possible problem. sourced init
is already capable of telling the users when the containers cannot be created due to a port conflict.
cmd/sourced/cmd/root.go
Outdated
@@ -65,6 +67,12 @@ func log(err error) { | |||
printRed("Cannot perform this action, full re-initialization is needed, run 'prune' command first") | |||
case dir.ErrNotValid.Is(err): | |||
printRed("Cannot perform this action, config directory is not valid") | |||
case compose.ErrCouldNotGetComposer.Is(err): | |||
printRed("docker-compose is not installed, and there was an error while trying to use the container alternative" + | |||
"\nsee: https://docs.sourced.tech/community-edition/quickstart/1-install-requirements#docker-compose") |
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.
Similar to the other comment, I think for most people it can be confusing to explain how to run things offline, when that is not the intended use case.
I'd prefer to remove the link to the docs, the error message should be enough to let the user understand that they need to connect to the internet, open their firewall, or provide the file manually.
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 linked docs do not explain how to run source{d} offline. They only explain that docker-compose is a requirement, and how can it be satisfied.
I think that pointing to docs from errors can be a good idea.
My 5 cents. I agree with Carlos to put instruction on how to run it without the Internet into FAQ for the same reasons he explained above. |
77bbf35
to
c36f475
Compare
Thanks for your deep review.
|
Signed-off-by: David Pordomingo <[email protected]>
Signed-off-by: David Pordomingo <[email protected]>
38c1b23
to
d7ca397
Compare
Inline suggestions applied, thanks @carlosms ! |
fix #245
motivated by #241
If the user initializes or tries to download a config without access to the internet, the error is a bit cryptic.
This PR adds clearer error messages in such situations and also points to some docs explaining why is that assets needed.
When there is no local
docker-compose
installation, and it could not be downloaded:because of network error:

because of write error when saving the file:

When trying to download and activate a new
docker-compose.yml
config, but the process failed:because of network error:

because of write error when saving the file:

disclaimer
This PR does not explain how to run sourced offline, which could be done separatelly if needed.