-
Notifications
You must be signed in to change notification settings - Fork 39.6k
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
flexvolume prober: trigger plugin init only for the relevant plugin #58519
Conversation
/sig storage |
30de551
to
68e74ff
Compare
/ok-to-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.
Thanks a lot for the PR! My biggest concern is around how plugins are instantiated and how Probe()
is implemented.
Probe()
is called by the volume plugin manager to discover Flexvolume plugins, so the bulk of probing logic should be in here. Most of the heavy-lifting involving file operations should be in Probe()
. This includes things like scanning the plugin directory and instantiating a probed plugin (NewFlexVolumePlugin()
calls Init()
on the Flexvolume driver, which ultimately executes the driver file and can take arbitrarily long depending on driver implementation). On the other hand, filesystem watcher handler should be kept fast, and I provided some justifications inline with the handler code.
pkg/volume/plugins.go
Outdated
type Operation uint32 | ||
type ProbeEvent struct { | ||
Plugin VolumePlugin // VolumePlugin that was added/updated/removed. | ||
PluginName string |
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.
Is this necessary?
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 Op
is Remove
, PluginName
will be set, and Plugin is nil
pkg/volume/plugins.go
Outdated
@@ -35,12 +35,22 @@ import ( | |||
"k8s.io/kubernetes/pkg/util/mount" | |||
) | |||
|
|||
type Operation uint32 |
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.
Name should be more specific. Maybe "ProbeOperation"?
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.
nit: Prefer to keep Flex probing-related constructs close to the DynamicPluginProber interface.
pkg/volume/plugins.go
Outdated
const ( | ||
// Common parameter which can be specified in StorageClass to specify the desired FSType | ||
// Provisioners SHOULD implement support for this if they are block device based | ||
// Must be a filesystem type supported by the host operating system. | ||
// Ex. "ext4", "xfs", "ntfs". Default value depends on the provisioner | ||
VolumeParameterFSType = "fstype" | ||
|
||
AddOrUpdate Operation = 1 << iota |
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.
Parameter name should be more specific, maybe ProbeAddOrUpdate
pkg/volume/plugins.go
Outdated
glog.Errorf("Error initializing dynamically probed plugin %s; error: %s", | ||
plugin.GetPluginName(), err) | ||
continue | ||
if len(events) > 0 { |
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 check isn't necessary
pkg/volume/flexvolume/probe.go
Outdated
events = []volume.ProbeEvent{} | ||
for k, event := range prober.eventsMap { | ||
events = append(events, event) | ||
delete(prober.eventsMap, k) |
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'd advise against modifying a map while iterating over it.
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 it is safe to do this in golang.
https://stackoverflow.com/questions/23229975/is-it-safe-to-remove-selected-keys-from-golang-map-within-a-range-loop
const ( | ||
// TODO (cxing) Tune these params based on test results. | ||
// watchEventLimit is the max allowable number of processed watches within watchEventInterval. | ||
watchEventInterval = 5 * time.Second |
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 is rate limiting removed?
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.
for two reasons:
watchEventInterval
is used in funcupdateProbeNeeded()
, butprobeNeeded
is removed- In my approach, we can't miss any event, because
probe()
will not scan the whole pluginDir except the first time it is invoked
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.
Rate limiting is still necessary to prevent a malicious actor from overwhelming controller-manager / kubelet with frequent probes. For example, someone could constantly write to a dummy file inside a driver directory and cause every Probe()
call to scan that directory. It's designed to drop events exceeding the quota.
But since the rate limiting logic would be quite different with this change, we could add it in a separate PR and remove it here for the time being.
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 thought for a long time, and couldn't find any remedy after missing some event. I think we only need to consider the normal situation, malicious attacks as a security issue should be resolved from a higher level.
pkg/volume/flexvolume/probe.go
Outdated
probeEvent := volume.ProbeEvent{ | ||
Op: volume.AddOrUpdate, | ||
} | ||
plugin, pluginErr := prober.factory.NewFlexVolumePlugin(prober.pluginDir, parentInfo.Name()) |
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.
Plugin should not be initialized inside the filesystem watch handler, for two reasons:
- The handler has to be very light, and ideally it only provides a status update for the main Probe() goroutine to process. A heavy handler delays the processing of inotify events.
- Typically multiple inotify events are triggered when a file is created or modified. This will cause a driver to be initialized multiple times.
2897ad8
to
782af82
Compare
@verult fixed, PTAL |
782af82
to
8b2ddab
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.
The bulk of the logic is good. Three themes in my comments:
- If there's a file change in any of the subdirectories of a driver directory (regardless of how nested it is), the driver should be re-initialized
- Keep locking to a minimum
- More unit tests for new functionality
pkg/volume/plugins.go
Outdated
pm.probedPlugins = append(pm.probedPlugins, plugin) | ||
pm.probedPlugins[event.Plugin.GetPluginName()] = event.Plugin | ||
} else if event.Op == ProbeRemove { | ||
delete(pm.probedPlugins, event.Plugin.GetPluginName()) | ||
} |
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.
nit: Consider logging an error if event.Op
is not matched.
pm.probedPlugins = append(pm.probedPlugins, plugin) | ||
pm.probedPlugins[event.Plugin.GetPluginName()] = event.Plugin | ||
} else if event.Op == ProbeRemove { | ||
delete(pm.probedPlugins, event.Plugin.GetPluginName()) |
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.
Is event.Plugin
going to be nil? If so, please also include comments in the ProbeEvent
struct describing this behavior for remove events
@@ -38,24 +38,22 @@ func TestProberExistingDriverBeforeInit(t *testing.T) { | |||
driverPath, _, watcher, prober := initTestEnvironment(t) | |||
|
|||
// Act | |||
updated, plugins, err := prober.Probe() | |||
events, err := prober.Probe() |
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.
Could you add some test cases / assertions for existing test cases to make sure the events
are correct?
const ( | ||
// TODO (cxing) Tune these params based on test results. | ||
// watchEventLimit is the max allowable number of processed watches within watchEventInterval. | ||
watchEventInterval = 5 * time.Second |
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.
Rate limiting is still necessary to prevent a malicious actor from overwhelming controller-manager / kubelet with frequent probes. For example, someone could constantly write to a dummy file inside a driver directory and cause every Probe()
call to scan that directory. It's designed to drop events exceeding the quota.
But since the rate limiting logic would be quite different with this change, we could add it in a separate PR and remove it here for the time being.
pkg/volume/flexvolume/probe.go
Outdated
func (prober *flexVolumeProber) Probe() (updated bool, plugins []volume.VolumePlugin, err error) { | ||
probeNeeded := prober.testAndSetProbeNeeded(false) | ||
func (prober *flexVolumeProber) Probe() (events []volume.ProbeEvent, err error) { | ||
prober.mutex.Lock() |
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.
Keep locking to a minimum. Currently the filesystem watcher would block waiting for a probe to finish, which is unnecessary. I recommend separating all map and probeAllNeed
operations into separate functions and only lock in those functions.
pkg/volume/flexvolume/probe.go
Outdated
} | ||
} | ||
|
||
prober.updateProbeNeeded() | ||
prober.mutex.Lock() |
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'd recommend separating out the logic here to a function, too
pkg/volume/flexvolume/probe.go
Outdated
if prober.probeAllNeeded { | ||
return nil | ||
} | ||
if eventOpIs(event, fsnotify.Create) || eventOpIs(event, fsnotify.Write) || eventOpIs(event, fsnotify.Rename) { |
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 it's safer to include Chmod
as well, i.e. everything besides Remove
pkg/volume/flexvolume/probe.go
Outdated
} | ||
|
||
// event about subdir of pluginDirAbs | ||
if parentPathAbs == pluginDirAbs { |
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 anything inside a driver directory changes, even if it's many levels of subdirectories in, the driver should re-initialize. With your logic, newly added subdirectories inside a driver directory won't be watched. I believe the existing logic already works, please correct me if I'm wrong.
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, you are right.
pkg/volume/flexvolume/probe_test.go
Outdated
|
||
// Assert | ||
assert.True(t, updated) | ||
assert.Equal(t, 2, len(plugins)) // Number of plugins should not change. | ||
assert.Equal(t, 0, len(events)) // Number of plugins should not change. |
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.
Comment no longer applies
|
||
// Assert | ||
assert.True(t, updated) | ||
assert.Equal(t, 2, len(events)) |
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.
Shouldn't it be 1, since it's the same driver?
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.
initTestEnvironment(t)
create one driver
a752c57
to
0c4d042
Compare
@verult Thank you very much for your reviews, I learned a lot. |
Hello what is the status of this PR? thanks |
Sorry I haven't had the chance to review it; will get to it soon. The current plan is to cherry-pick this back to all PRs since dynamic plugin discovery was introduced, which I believe was 1.8. |
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 unit tests are great, most of the logic looks good
pkg/volume/flexvolume/probe_test.go
Outdated
assert.Equal(t, volume.ProbeAddOrUpdate, events[0].Op) | ||
assert.NoError(t, err) | ||
|
||
// Call probe again, should 0 event. |
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.
"should return 0 event"
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.
fixed
@@ -60,8 +53,8 @@ func GetDynamicPluginProber(pluginDir string) volume.DynamicPluginProber { | |||
} | |||
|
|||
func (prober *flexVolumeProber) Init() error { | |||
prober.testAndSetProbeNeeded(true) | |||
prober.lastUpdated = time.Now() | |||
prober.testAndSetProbeAllNeeded(true) |
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.
Is testAndSetProbeAllNeeded()
still necessary? Init()
and Probe()
are never executed at the same time
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 try to remove testAndSetProbeNeeded
, but pull-kubernetes-bazel-test
complaints race detected during execution of 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.
OK, I'm guessing it's because updateEventMap()
reads probeAllNeeded
.
pkg/volume/flexvolume/probe.go
Outdated
|
||
func (prober *flexVolumeProber) probeMap() (events []volume.ProbeEvent, err error) { | ||
prober.mutex.Lock() | ||
defer prober.mutex.Unlock() |
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 locking the entire map is OK for now. In the future I'd like to use something like a concurrent map, where only operations on individual elements are locked, rather than the entire map. If you could leave a TODO that'd help a lot.
Locking the entire map will delay other inotify events from being processed.
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 added
pkg/volume/flexvolume/probe.go
Outdated
} | ||
|
||
// event of subdir of pluginDirAbs | ||
if parentPathAbs == pluginDirAbs { |
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 is this check necessary? Upon prober initialization and every time the pluginDir is recreated, a recursive watch is added in all of pluginDir
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 a driver directory is created under pluginDir after prober initialization, I think we should add a watch, according to the original code: https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/flexvolume/probe.go#L146
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 it already does without this check: the check at line 193 (if eventOpIs(event, fsnotify.Create)
) applies for a newly created driver directory, since the pluginDir is always under watch.
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.
oh ,got it !
pkg/volume/flexvolume/probe.go
Outdated
|
||
// event inside specific driver dir | ||
if len(eventRelPathToPluginDir) > 0 { | ||
driverDirName := topDirName(eventRelPathToPluginDir) |
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 about strings.Split(eventRelPathToPluginDir, os.PathSeparator)[0]
?
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.
good idea!
pkg/volume/flexvolume/probe.go
Outdated
if len(eventRelPathToPluginDir) > 0 { | ||
driverDirName := topDirName(eventRelPathToPluginDir) | ||
driverDirAbs := filepath.Join(pluginDirAbs, driverDirName) | ||
if len(driverDirAbs) > 0 { |
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 think this check is necessary - pluginDirAbs always exists
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, the check will be removed
pkg/volume/flexvolume/probe.go
Outdated
driverDirName := topDirName(eventRelPathToPluginDir) | ||
driverDirAbs := filepath.Join(pluginDirAbs, driverDirName) | ||
if len(driverDirAbs) > 0 { | ||
parts := strings.Split(driverDirName, "~") |
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.
nit: Could you separate the logic to compute the path to the executable in a separate helper function? Easier to read that way
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.
done!
pkg/volume/flexvolume/probe_test.go
Outdated
assert.True(t, updated) | ||
assert.Equal(t, 2, len(plugins)) // Number of plugins should not change. | ||
assert.Equal(t, 1, len(events)) | ||
assert.Equal(t, volume.ProbeRemove, events[0].Op) |
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 surprised op
isn't ProbeAddOrUpdate
, because the driverPath
event is triggered last. If this is the case, maybe delete the executable, probe and test, delete the driver dir and probe again
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.
driverPath event handling returns fast, and will not trigger updateEventsMap
https://github.com/linyouchong/kubernetes/blob/0c4d0421bd693a4a28bbb9f72091ca6bd4368ce0/pkg/volume/flexvolume/probe.go#L189
if parentPathAbs == pluginDirAbs {
......................
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.
Gotcha. What if the driver directory gets renamed? The plugin name would change so we'd want to trigger an ProbeAddOrUpdate event. IMO it's safer to not return fast, what do 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.
I think we should trigger ProbeRemove event in 2 cases:
- The driverDir is removed
- The driver executable is removed
other case we should trigger ProbeAddOrUpdate event
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 OK with either this or the previous logic (only trigger ProbeRemove when executable is removed). If the dir is removed, the executable will be removed too. This logic would trigger two ProbeRemove events, which is OK.
// Arrange | ||
_, fs, watcher, _ := initTestEnvironment(t) | ||
// Assert | ||
assert.Equal(t, 2, len(watcher.watches)) // 2 from initial setup |
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.
nit: It's not necessary to test functions written in the test
@@ -154,7 +211,37 @@ func TestRemovePluginDir(t *testing.T) { | |||
assert.Equal(t, pluginDir, watcher.watches[len(watcher.watches)-1]) | |||
} | |||
|
|||
// Issue multiple events and probe multiple times. Should give true, false, false... | |||
// Issue an event to remove plugindir. New directory should still be watched. | |||
func TestNestedDriverDir(t *testing.T) { |
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 adding this test!
13a0a9d
to
9d77be6
Compare
pkg/volume/flexvolume/probe.go
Outdated
// getExecutableName returns the executableName of a flex plugin | ||
func getExecutableName(driverDirName string) string { | ||
parts := strings.Split(driverDirName, "~") | ||
return parts[len(parts)-1] |
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.
nit: You could also do the join here and return the abs path :)
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.
Done!
PTAL
/lgtm PTAL @chakri-nelluri @jingxu97 @linyouchong we'll eventually need an e2e test to test the driver isolation behavior (here). If you are interested, it'd be very helpful to add it either here or as a separate PR. |
@verult I will add an e2e test as a separate PR. |
Sounds good, and yes please squash the commits. Thanks! |
/lgtm |
/assign @thockin |
/approve |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: linyouchong, thockin, verult The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Automatic merge from submit-queue (batch tested with PRs 60073, 58519, 61860). If you want to cherry-pick this change to another branch, please follow the instructions here. |
@linyouchong after the e2e test is added, please cherry-pick this to 1.8, 1.9, 1.10 as well (instructions here) |
@verult ok, I am working on it |
thanks for fixing this issue btw is the fix relevant only for flexvolume plugins or its generic for all plugins? |
Yep it'll be part of 1.11. @linyouchong any update on the e2e tests? This is only relevant for Flexvolume - other plugins don't have a filesystem watch for scanning plugins |
@verult I am very sorry for the delay, for some reason, I can't commit my test code right now. |
No problem, thanks for the update! |
What this PR does / why we need it:
The automatic discovery trigger init only to the specific plugin directory that was updated, and not to all the plugins in the flexvolume plugin directory.
Which issue(s) this PR fixes (optional, in
fixes #<issue number>(, fixes #<issue_number>, ...)
format, will close the issue(s) when PR gets merged):Fixes #58352
Special notes for your reviewer:
NONE
Release note: