Skip to content
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

Add dependency injection of remote store for NotaryRepository initial… #1094

Merged
merged 13 commits into from
Mar 1, 2017

Conversation

n4ss
Copy link
Contributor

@n4ss n4ss commented Feb 10, 2017

…ization

Signed-off-by: Nassim 'Nass' Eddequiouaq eddequiouaq.nassim@gmail.com

client/client.go Outdated

// NewNotaryRepository is the base method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (This is normally defaults to "~/.notary" or "~/.docker/trust" when enabling
Copy link
Contributor

@cyli cyli Feb 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: Perhaps these should be listed as examples rather than normal defaults, since in this particular function there is no default for the baseDir (these are just defaults for the CLI tool that uses it and in docker, which uses it)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Apologies, I know this wasn't your change, I just hadn't noticed it before)

client/client.go Outdated
// (This is normally defaults to "~/.notary" or "~/.docker/trust" when enabling
// docker content trust).
// It expects initiated remote store and cache.
func NewNotaryRepository(baseDir, gun, baseURL string, remoteStore store.RemoteStore, rt http.RoundTripper, cache store.MetadataStore,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe the RoundTripper object may no longer be necessary when store.RemoteStore is passed. NotaryRepository.roundTrip only seems to be passed to the getRemoteStore, getRemoteKey, and rotateRemoteKey helper functions. getRemoteKey and rotateRemoteKey just use it in order to call getRemoteStore, so they can probably just be passed the remote store instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had this in my todo list and skipped it somehow.. will fix.

client/client.go Outdated
if err != nil {
return err
}
remote := r.remoteStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is only used in the line below, would it make sense to just call r.remoteStore.SetMulti(updatedFiles)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, wondering if we should check if this is nil, first, and error if so? Sometimes we do not require the client to be online (if we're just reading from cache, for example), but write operations on the remote store definitely require us to be online.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait never mind, ignore me - I think we use OfflineStore in this case. Ok, that was why I think we used getRemoteStore everywhere - if there was an error, we return OfflineStore + the error, and in some cases, we fail the operation if there was an error.

In the case of downloading new data, we ignore the error and just use the OfflineStore because we can maybe read from cache.

This means that https://github.com/docker/notary/pull/1094/files#diff-1cd41a2611da49ce63630d5a51bd82c5R78 may not be the right behavior. However, if we don't propagate the error, we'd lose it.

Every operation from OfflineStore will result in a generic offline error, though, so wondering if we should update OfflineStore to take an error it can wrap, and propagate the original error that way?

cc @endophage

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch @cyli: I agree we need to wrap the OfflineStore and also have a check for nil here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes I've suggested to @n4ss:

  • In NewNotaryRepository check if remoteStore is nil and instantiate an OfflineStore if it is.
  • Create a getter for remoteStore that will do the same check and also initialize an OfflineStore if necessary.

re. the behaviour at https://github.com/docker/notary/pull/1094/files#diff-1cd41a2611da49ce63630d5a51bd82c5R78, I believe this is correct. The pre-existing behaviour was to directly return the error if getRemoteStore errored (i.e. https://github.com/docker/notary/pull/1094/files#diff-1cd41a2611da49ce63630d5a51bd82c5R78`), we're now simply detecting and aborting sooner.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@endophage: that sounds good 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client/client.go Outdated
if err != nil {
return err
}
remote := r.remoteStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to above, should we check that r.remoteStore isn't nil first, and then if it isn't, we can also just call r.remoteStore.RemoveAll()?

client/client.go Outdated
// It takes the base directory under where all the trust files will be stored
// (This is normally defaults to "~/.notary" or "~/.docker/trust" when enabling
// docker content trust).
// It expects initiated remote store and cache.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/initiated/initialized

Copy link
Contributor

@riyazdf riyazdf Feb 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following on from the points that @cyli mentioned - even if we expect the initialized store/cache, we should check the values in case someone accidentally calls this with nils. Would be great to have unit tests for these cases.

Also, maybe this maybe shouldn't have to be exported anymore? cc @endophage

client/client.go Outdated
// It expects initiated remote store and cache.
func NewNotaryRepository(baseDir, gun, baseURL string, remoteStore store.RemoteStore, rt http.RoundTripper, cache store.MetadataStore,
trustPinning trustpinning.TrustPinConfig, cryptoService signed.CryptoService) (
*NotaryRepository, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: this signature is kind of hard to read - not sure if you chose it or if gofmt imposed it to be this way. Maybe we could move the return values up a line such that we have 2 lines instead of 3?

client/client.go Outdated
if remoteErr != nil {
logrus.Error(remoteErr)
} else if !newBuilder.IsLoaded(data.CanonicalRootRole) || checkInitialized {
remote := r.remoteStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as @cyli's comment below:

Similarly to above, should we check that r.remoteStore isn't nil first, and then if it isn't, we can also just call r.remoteStore.RemoveAll()?

client/client.go Outdated
if err != nil {
return err
}
remote := r.remoteStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch @cyli: I agree we need to wrap the OfflineStore and also have a check for nil here

@@ -17,7 +17,6 @@ import (
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
store "github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder how this passed the gofmt before

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trustmanager was in use at line 92 prior to this PR.

@n4ss
Copy link
Contributor Author

n4ss commented Feb 13, 2017

Sorry, I pushed only the first commit by mistake - pushing the rest soon.
Please wait before reviewing again as all as everything is not fixed yet.

@endophage
Copy link
Contributor

Made a couple of updates:

  • changelists are now injectable (it's still fine to create an in memory changelist within specific operations but the file based changelist is injected).
  • DeleteTrustData is no longer defined on the repo, it's just a function now. There were errors we didn't care about but still had to deal with by initializing a repo for this operation.
  • The trust pinning test that triggers an error no longer covers the delete operation as delete no longer requires trust pinning (this is one of the errors we didn't care about but still had to deal with).

@endophage
Copy link
Contributor

endophage commented Feb 19, 2017

Also, I'd like to remove baseDir off the NotaryRepository but the tests rely on it heavily. I have an idea how to fix it without massive changes to the tests but it's not in scope for this PR.

The idea is create the following struct and use it at a few key points in the tests to wrap NotaryRepository:

type TestNotaryRepository struct {
    NotaryRepository
    baseDir string
}```
  

@endophage
Copy link
Contributor

@riyazdf @cyli thought on the remoteStore nil check. What about if we use a getter for remoteStore and have the getter always return an offline store if no other store was set?

@endophage endophage force-pushed the cli-grpc-refactor branch 2 times, most recently from 7927e96 to 7921e31 Compare February 19, 2017 19:09
@riyazdf
Copy link
Contributor

riyazdf commented Feb 21, 2017

@endophage - I think that logic for a remoteStore getter makes sense, would that getter be defined on the NotaryRepository?

@cyli
Copy link
Contributor

cyli commented Feb 21, 2017

@endophage That would be fine - would we care about propagating the error that caused the remote store to be nil? In some cases we want to error right away, and propagating why we couldn't get a remote store would be nice. Passing just nil as the remote store to the constructor would not propagate this error.

@endophage
Copy link
Contributor

@cyli I feel like as long as we log the error visibly, not just when running in debug, that's probably sufficient. A user doesn't necessarily care when in the flow an error gets emitted as long as there's sufficient information for them to understand what went wrong.

If that seems like a reasonable assumption, then we don't need to add the complexity of propagating the error until we might want to expose it.

@endophage
Copy link
Contributor

@riyazdf yeah, I was thinking it would be defined on NotaryRepository. It would just check the field and if it isn't nil, return the value, if it is nil, return an OfflineStore

@cyli
Copy link
Contributor

cyli commented Feb 21, 2017

@endophage That seems reasonable - would we still need a remote store getter in that case? There is only one place where we need an offline store - in the other two cases we just seem to return an error if the remote store is nil.

@endophage
Copy link
Contributor

@cyli on the basis that somebody could directly initialize a NotaryRepository without using a constructor function, it's probably still a good practice to use a getter.

@cyli
Copy link
Contributor

cyli commented Feb 21, 2017

@endophage Oh I mean returning OfflineStore won't be useful for 2 of the cases where we want to get the remote store - nil should error. Only in one case would we actually want the OfflineStore. If the user initializes a NotaryRepository directly without the constructor function, that just means there's no error to propagate/log, but the behavior of NotaryRepository with and without an actual remote store should be the same.

@endophage
Copy link
Contributor

@cyli I figured the OfflineStore would cause the desired behaviour by erroring when we need it to, rather than having additional preemptive checks on whether the NotaryRepository.remoteStore was nil.

@cyli
Copy link
Contributor

cyli commented Feb 22, 2017

@endophage Ah good point, you're right. For some reason I was thinking we'd return a different error like "invalid remote" but "offline" would be fine so long as that other error was logged.

Copy link
Contributor

@cyli cyli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@riyazdf riyazdf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

client/client.go Outdated
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/utils"
"os"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: weird that this got formatted to here in the import list

client/client.go Outdated
if err != nil {
return err
}
remote := r.remoteStore

return remote.SetMulti(data.MetadataRoleMapToStringMap(updatedFiles))
Copy link
Contributor

@riyazdf riyazdf Feb 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return r.remoteStore.SetMulti(data.MetadataRoleMapToStringMap(updatedFiles))

(no need for the extra remote var)

Copy link
Contributor

@cyli cyli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, sorry, I totally forgot about @endophage's suggestion about the getter and logging the remote store error.

n4ss and others added 7 commits February 25, 2017 03:05
…ization

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
…tiated

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)
Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
… or an OfflineStore

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
keyStores, trustPinning)
cryptoService := cryptoservice.NewCryptoService(keyStores...)

remoteStore, err := getRemoteStore(baseURL, gun, rt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we still seem to be using this initializer in the CLI for both reading notary data and publishing/initializing notary data, would it make sense to log this error instead? That way later the actual NotaryRepository object itself can decide whether to fail if it's offline (in the case of publishing and initializing) or continue reading from cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to me, will fix!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually added the error logging directly inside getRemoteStore without being too sure about this decision.. Should we let caller decide on wether to logging instead? It seems more reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let the caller decide on logging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(but in this specific case, always log the error)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cyli actually we should probably log when we get an OfflineStore only from getRemoteStore I think. If we get an err from getRemoteStore and hence from NewHTTPStore, it means that baseURL was incorrect, not that it was unreachable.

Copy link
Contributor

@cyli cyli Feb 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From your comment in slack:

We have the following behavior for NewHTTPStore:

  • if we have an invalid url from baseURL+gun then we get a nil store and a non-nil err -> this should error in callers, not switched to an OfflineStore

That makes sense to change this (from master, it seem sot already do so in this PR) to error on actual invalid values.

  • if we have an invalid http.roundtrip, we get an OfflineStore and a nil err -> this should be logged in callers

If we were looking just at NewHTTPStore, I'm wondering if we should actually convert a nil http.RoundTrip to a http.DefaultTransport, or count it as invalid input and return an error (I don't feel strongly either way).

But in our usage of this function, we seem to set the roundtrip to nil when doing offline operations, namely updating the changelist, so we don't want it to error or probably care about it logging. I'd argue that eventually we should probably just directly call NewNotaryRepository instead with a nil remote store or something. I think it doesn't even need the cache, or trust pinning, or a cryptoservice, so those could probably also all be nil.

In the meantime (unless we wanted to make that change here), since our usage is specifically "a nil http.Roundtrip means this is intended to be an offline operation", maybe we should explicitly add a parameter to denote that, or just ignore invalid roundtrips?

  • we have a valid HTTPStore -> all good

We don't check if the remote is actually reachable

👍

client/client.go Outdated

// GetRemoteStore returns the remoteStore of a repository if valid or
// or an OfflineStore otherwise
func (r *NotaryRepository) GetRemoteStore() (store.RemoteStore, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't seem to need to return an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also non-blocking nit: I'm not sure this needs to be exported, but if it does, would just RemoteStore() make sense as kind of an accessor function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on returning error, if it's not necessary, don't do it. Every unnecessary error return demands 3 lines of code to check it :-P

Also agree on exporting. If it doesn't need to be part of the interface, don't export it because then somebody might rely on it and we could break them if we change it in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
…it on error or not

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
Copy link
Contributor

@endophage endophage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!!!

@endophage
Copy link
Contributor

@cyli when you get a chance, can you do final review please. Don't want to merge while you're still an X but I think everything is now resolved.

client/client.go Outdated
remoteStore, err := getRemoteStore(baseURL, gun, rt)
// baseURL is syntactically invalid
if err != nil {
logrus.Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: I'm not sure we have to log as well as returning the error.

Also it looks like in the CLI, tuf.go's tokenAuth function also parses the URL and checks for validity, which is probably a better validity check than NewHTTPStore's, since it looks like a url like http:asdga gets through the url.Parse + checking for the existence of a scheme?:

$  bin/notary -D -s https:asdgadg list repo
DEBU[0000] Configuration file not found, using defaults 
DEBU[0000] Using the following trust directory: /Users/cyli/.notary 
ERRO[0000] could not reach https:asdgadg: Get https:///v2/: http: no Host in request URL 
INFO[0000] continuing in offline mode                   
DEBU[0000] No yubikey found, using alternative key storage: loaded library /usr/local/lib/libykcs11.dylib, but no HSM slots found 
DEBU[0000] Making dir path: /Users/cyli/.notary/tuf/repo/changelist 

* fatal: client is offline

We can probably fix that in a later PR, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just fixed it right now in a last small commit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks! That was my only comment! This otherwise looks good to merge on green!

Copy link
Contributor

@cyli cyli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than a non-blocking nitpick about a log and returning an error both, this LGTM!

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
@endophage
Copy link
Contributor

I'm not waiting on codecov to get worked out on this. It was OK on test coverage before and I don't see any updates that should significantly change that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants