-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Support reloading ES client's password from file #4342
Conversation
@yurishkuro Could you pls review if this implementation approach seems to make sense? If yes, I'll continue to fix current broken tests and add related tests. |
I'm changing the approach from resetting the password to swapping the whole ES client. That might be easier to test and wouldn't introduce subtle errors... |
546c472
to
d388310
Compare
@yurishkuro Could you help me a little here? My approach is creating a channel of ES options and a channel of ES client. Everytime password from file changes, a new array of options is channeled to create a new client. That client is channeled to the factory. Some tests are broken but basically the one problem here is the client channel can receive the client, but cannot assign client in the factory. The test result says assert.NotNil(t, <-f.primaryClientChan) passes but assert.NotNil(t, f.primaryClient) fails. When I debugged the problem seems to be in assigning client in factory. Could you take a look to see what I did wrong here? |
I think channels are overkill for this. There is already a callback mechanism in fswatcher that can be used to propagate all the changes. Critically though, your changes so far don't actually affect how the storage works, because jaeger/plugin/storage/es/factory.go Lines 195 to 196 in 6cbc11e
If recreating the client is the only option (which may be fine), then this is how I would go about it:
|
Codecov ReportPatch coverage is
📢 Thoughts on this report? Let us know!. |
@yurishkuro I implemented your approach. Could you review the implementation, especially the lint failures in onPasswordChange()? From the failures, it seems like I didn't assign client successfully in onPasswordChange() (and probably in initializeClient() too)? Maybe I didn't use the pointer correctly? I'll add tests when the implementation is fine. |
plugin/storage/es/factory.go
Outdated
} | ||
|
||
func (f *Factory) onPrimaryPasswordChange() { | ||
primaryClient, err := f.newClientFn(f.primaryConfig, f.logger, f.metricsFactory) |
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.
You are using the old config here, not the new password. This needs a unit test
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 current tests and lint all pass now so I'm adding new tests now.
But could you help explain why password is not updated here? I thought when we recall f.newClientFn(f.primaryConfig, ...) then we'd recall getConfigOptions and password file path would be reloaded?
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 is why when you develop new functionality you often start with a unit test that verifies what you're trying to implement. In this case we want to observe that a new client is created once pwd file is changed - if you had that test you would've caught several bugs I already pointed out.
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.
Yeah sorry I'll add that test now^^ I have exams until Thursday so maybe I'll push the test after 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.
no need to apologize, your contributions are greatly appreciated.
Signed-off-by: haanhvu <[email protected]>
Signed-off-by: haanhvu <[email protected]>
Signed-off-by: haanhvu <[email protected]>
Signed-off-by: haanhvu <[email protected]>
Signed-off-by: haanhvu <[email protected]>
Signed-off-by: haanhvu <[email protected]>
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.
@yurishkuro I added a test for password change in primary config.
Also, in the implementation, there was a race detected so I changed config type to atomic.
Please help review! The test passed but there're probably some problems.
plugin/storage/es/factory_test.go
Outdated
) | ||
|
||
var _ storage.Factory = new(Factory) | ||
/*var _ storage.Factory = new(Factory) |
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.
Since I changed config type to atomic, I have to change it in the existed tests too to make them pass. But for now I'm commenting these tests to focus on the TestPasswordFromFile test.
|
||
_, err = passwordFile.WriteString("baz") | ||
require.NoError(t, err) | ||
f.onPrimaryPasswordChange() |
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 why but I have to explicitly call f.onPrimaryPasswordChange() here to make the password change. Don't know if it's a problem of the watcher in the implementation or a problem in my test.
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 you need to either fsync or close the file after writing, otherwise the write may be still in the OS buffer
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.
Also, because watcher is running in a separate goroutine, you cannot immediately validate if the client was updated, usually we use assert.Eventually (grep for examples in this repo)
pkg/es/config/config.go
Outdated
@@ -296,6 +297,10 @@ func (c *Configuration) TagKeysAsFields() ([]string, error) { | |||
|
|||
// getConfigOptions wraps the configs to feed to the ElasticSearch client init | |||
func (c *Configuration) getConfigOptions(logger *zap.Logger) ([]elastic.ClientOptionFunc, error) { | |||
if c.Password != "" && c.PasswordFilePath != "" { | |||
return nil, fmt.Errorf("both Password and PasswordFilePath are 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.
please move just before L319, to keep related logic together
plugin/storage/es/factory.go
Outdated
f.archiveClient, err = f.newClientFn(f.archiveConfig, logger, metricsFactory) | ||
f.primaryClient.Store(&primaryClient) | ||
|
||
primaryWatcher, err := fswatcher.New([]string{f.primaryConfig.Load().PasswordFilePath}, f.onPrimaryPasswordChange, f.logger) |
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.
creating a watcher should be conditional on PasswordFilePath != ""
plugin/storage/es/factory.go
Outdated
if err != nil { | ||
return fmt.Errorf("failed to create archive Elasticsearch client: %w", err) | ||
} | ||
f.archiveClient.Store(&archiveClient) | ||
|
||
archiveWatcher, err := fswatcher.New([]string{f.archiveConfig.Load().PasswordFilePath}, f.onArchivePasswordChange, f.logger) |
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.
also conditional
plugin/storage/es/factory.go
Outdated
f.logger.Error("failed to reload password for primary Elasticsearch client", zap.Error(err)) | ||
} else { | ||
newPrimaryCfg.Password = newPrimaryPassword | ||
f.primaryConfig.Store(&newPrimaryCfg) |
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.
there is no need to overwrite the primary config:
newConfig := primaryConfig // copy by value
newConfig.Password = newPrimaryPassword
f.newClientFn(newConfig, ...)
plugin/storage/es/factory.go
Outdated
} | ||
|
||
func createSpanReader( | ||
mFactory metrics.Factory, | ||
logger *zap.Logger, | ||
client es.Client, | ||
client *atomic.Pointer[es.Client], |
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 recommend adding a function to the factory and passing it as argument to createSpanReader() and similar functions. This will reduce the coupling: createXyz functions would not need to know about atomic pointers.
func (f *Factory) getPrimaryClient() es.Client {
return f.primaryClient.Load()
}
|
||
_, err = passwordFile.WriteString("baz") | ||
require.NoError(t, err) | ||
f.onPrimaryPasswordChange() |
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 you need to either fsync or close the file after writing, otherwise the write may be still in the OS buffer
plugin/storage/es/factory_test.go
Outdated
} | ||
|
||
f := NewFactory() | ||
f.newClientFn = func(c *escfg.Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error) { |
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 do you need this function? It looks like it's repeating the same code as in the factory.
You need to verify that the es.Client is updated once the password is modified. The easiest way is to do factory.getPrimaryClient
and check its authentication settings, assuming they are accessible. If not accessible, the other option is to create a fake server and invoke a method on the client that would cause it to make an HTTP call. We don't care if the call succeeds or not, but the fake server will be able to inspect the auth headers in that call, which is the ultimate validation.
|
||
_, err = passwordFile.WriteString("baz") | ||
require.NoError(t, err) | ||
f.onPrimaryPasswordChange() |
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.
Also, because watcher is running in a separate goroutine, you cannot immediately validate if the client was updated, usually we use assert.Eventually (grep for examples in this repo)
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
pkg/es/config/config.go
Outdated
@@ -412,7 +424,7 @@ func GetHTTPRoundTripper(c *Configuration, logger *zap.Logger) (http.RoundTrippe | |||
return transport, nil | |||
} | |||
|
|||
func loadToken(path string) (string, error) { | |||
func LoadFileContent(path string) (string, error) { |
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.
TODO this should probably remain being called LoadToken, since it's not a plain file read
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Signed-off-by: Yuri Shkuro <[email protected]>
Which problem is this PR solving?
Resolves the final 2nd and 3rd steps for #3924 as specified here