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

Support reloading ES client's password from file #4342

Merged
merged 15 commits into from
Sep 9, 2023

Conversation

haanhvu
Copy link
Contributor

@haanhvu haanhvu commented Mar 26, 2023

Which problem is this PR solving?

Resolves the final 2nd and 3rd steps for #3924 as specified here

@haanhvu
Copy link
Contributor Author

haanhvu commented Mar 26, 2023

@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.

@haanhvu
Copy link
Contributor Author

haanhvu commented Apr 8, 2023

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...

@haanhvu haanhvu marked this pull request as draft April 10, 2023 06:27
@haanhvu haanhvu force-pushed the issue3924-2-1 branch 2 times, most recently from 546c472 to d388310 Compare April 15, 2023 17:22
@haanhvu
Copy link
Contributor Author

haanhvu commented Apr 15, 2023

@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?

@haanhvu haanhvu marked this pull request as ready for review April 15, 2023 18:54
@yurishkuro
Copy link
Member

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 factory.CreateSpanReader() is called only once, which will instantiate the client only once:

writer := esSpanStore.NewSpanWriter(esSpanStore.SpanWriterParams{
Client: client,

If recreating the client is the only option (which may be fine), then this is how I would go about it:

  • don't pass a client to NewSpanReader, but pass a function that returns a client
  • the function will retrieve the client from f.primaryClient which should be turned into atomic.Value
  • pass a callback function to fswatcher such that when that call back is invoked the client is re-created and replaced in f.primaryClient

@codecov
Copy link

codecov bot commented Apr 18, 2023

@haanhvu
Copy link
Contributor Author

haanhvu commented Apr 18, 2023

@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/spanstore/reader_test.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
pkg/es/config/config.go Outdated Show resolved Hide resolved
@haanhvu haanhvu marked this pull request as draft April 21, 2023 13:24
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
pkg/es/config/config.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
plugin/storage/es/factory.go Outdated Show resolved Hide resolved
}

func (f *Factory) onPrimaryPasswordChange() {
primaryClient, err := f.newClientFn(f.primaryConfig, f.logger, f.metricsFactory)
Copy link
Member

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

Copy link
Contributor Author

@haanhvu haanhvu Apr 23, 2023

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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 <haanh6594@gmail.com>
Signed-off-by: haanhvu <haanh6594@gmail.com>
Signed-off-by: haanhvu <haanh6594@gmail.com>
Signed-off-by: haanhvu <haanh6594@gmail.com>
Signed-off-by: haanhvu <haanh6594@gmail.com>
Signed-off-by: haanhvu <haanh6594@gmail.com>
Copy link
Contributor Author

@haanhvu haanhvu left a 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.

)

var _ storage.Factory = new(Factory)
/*var _ storage.Factory = new(Factory)
Copy link
Contributor Author

@haanhvu haanhvu Apr 28, 2023

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()
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 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.

Copy link
Member

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

Copy link
Member

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)

@@ -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")
}
Copy link
Member

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

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)
Copy link
Member

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 != ""

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)
Copy link
Member

Choose a reason for hiding this comment

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

also conditional

f.logger.Error("failed to reload password for primary Elasticsearch client", zap.Error(err))
} else {
newPrimaryCfg.Password = newPrimaryPassword
f.primaryConfig.Store(&newPrimaryCfg)
Copy link
Member

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, ...)

}

func createSpanReader(
mFactory metrics.Factory,
logger *zap.Logger,
client es.Client,
client *atomic.Pointer[es.Client],
Copy link
Member

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()
Copy link
Member

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

}

f := NewFactory()
f.newClientFn = func(c *escfg.Configuration, logger *zap.Logger, metricsFactory metrics.Factory) (es.Client, error) {
Copy link
Member

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()
Copy link
Member

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)

@yurishkuro yurishkuro changed the title Allow updating ES client's password from file [WIP] Allow updating ES client's password from file Jun 5, 2023
yurishkuro and others added 3 commits September 8, 2023 11:50
Signed-off-by: Yuri Shkuro <yurishkuro@users.noreply.github.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Signed-off-by: Yuri Shkuro <github@ysh.us>
@@ -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) {
Copy link
Member

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 <github@ysh.us>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Signed-off-by: Yuri Shkuro <github@ysh.us>
@yurishkuro yurishkuro changed the title [WIP] Allow updating ES client's password from file Support reloading ES client's password from file Sep 8, 2023
Signed-off-by: Yuri Shkuro <github@ysh.us>
@yurishkuro yurishkuro marked this pull request as ready for review September 8, 2023 23:10
Signed-off-by: Yuri Shkuro <github@ysh.us>
Signed-off-by: Yuri Shkuro <github@ysh.us>
@yurishkuro yurishkuro merged commit 2ac6aac into jaegertracing:main Sep 9, 2023
30 of 31 checks passed
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.

None yet

2 participants