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

Introducing Flags interface, with implementation for control server related flags #1114

Merged
merged 47 commits into from
Apr 17, 2023

Conversation

seejdev
Copy link
Contributor

@seejdev seejdev commented Mar 28, 2023

Related to #1086, and #1083

Motivation for these changes:

We have a number of launcher flags controlling various behaviors, specified through different means, and generally accessed and modified in non-standard and inconsistent ways. This PR lays the foundation for streamlining:

  • Hooking into our pervasive Knapsack to enable disparate areas of the code base to utilize common flag functionality easily and with low developer overhead.
  • How flags are defined, their defaults, their available command line options, and how they are stored in key-value stores.
  • What flag values take precedence over others.
  • Sanitizing flag values to prevent unreasonable values being used.
  • Enabling temporary overrides for flags in a generic fashion, avoiding custom one-off code written to accomplish short-lived modifications to flags.

What is in this PR:

  • The Flags interface: provides an easy-to-use and readable API for setting/getting launcher flags. It's been added to the Knapsack struct, so anywhere Knapsack is passed to, you can access Flags just as easily.
  • Implementations of default flag values, and command line options which can take precedence over defaults.
  • Implementation of stored flag values, backed by a key-value store. This is principally designed for control server to push updates of launcher flags, and these can take precedence over defaults, and command line options.
  • Implementation of flag value overrides, where a flag can be temporarily overridden for a period of time with a special-case value.
  • Sanitization of integer flag values to constrain values to sensible limits.
  • Re-working of the types.Updater interface to better separate JSON parsing from the logic of bulk-replacing the contents of the agent_flags key-value store. This was necessary to enable clients of the interface to have better control and knowledge of what keys are being changed.
  • Implementation of a flag change notification framework, where clients can register as observers of various FlagKey and receive notifications when they change.
  • Re-working of the control server request interval acceleration feature to tie into the Flags implementation, notification framework, and common temporary override functionality.
  • Using all of the above to re-implement control server flags: DesktopEnabled, DebugServerData, ForceControlSubsystems, ControlServerURL, ControlRequestInterval, DisableControlTLS, InsecureControlTLS

Copy link
Contributor

@James-Pickett James-Pickett left a comment

Choose a reason for hiding this comment

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

I like the general shape of this. Cool to see generics in action. Look forward to more tests.

Copy link
Contributor

@directionless directionless left a comment

Choose a reason for hiding this comment

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

There's a lot of meat here. But it feels very hard to understand.

pkg/agent/flags/flag_controller.go Outdated Show resolved Hide resolved
ee/desktop/runner/runner.go Outdated Show resolved Hide resolved
ee/desktop/runner/runner.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_values_constraints.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Outdated Show resolved Hide resolved
docs/architecture/knapsack.md Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_values_defaults.go Outdated Show resolved Hide resolved
cmd/launcher/launcher.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_value_sanitizer.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_values_stored.go Outdated Show resolved Hide resolved
Copy link
Contributor

@James-Pickett James-Pickett left a comment

Choose a reason for hiding this comment

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

... still digesting

docs/architecture/flags.md Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Show resolved Hide resolved
return k.flags.DebugServerData()
}

func (k *knapsack) SetForceControlSubsystems(force bool) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

The above funcs to retrieve KVStores are suffixed with Store should we consider suffixing the flags with Flag?

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 could do that if folks think it's more readable (or, conversely remove the Store suffix) from the ones above. Always a balance between readability and conciseness.

pkg/agent/types/keyvalue_store.go Show resolved Hide resolved
RebeccaMahany
RebeccaMahany previously approved these changes Apr 10, 2023
ee/desktop/runner/runner.go Show resolved Hide resolved
pkg/agent/flags/flag_controller.go Outdated Show resolved Hide resolved
pkg/agent/flags/flag_value_duration.go Show resolved Hide resolved
Copy link
Contributor

@directionless directionless left a comment

Choose a reason for hiding this comment

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

There's a lot here, so I'm not confident I absorbed it all. But this looks like it's a huge improvement. I say we merge, and iterate based on what we learn.

Caveat -- don't merge until we cut 1.0.7, since I'm hoping we can come up with that tomorrow

return k.getKVStore(storage.ServerProvidedDataStore)
}

func (k *knapsack) RegisterChangeObserver(observer types.FlagsChangeObserver, flagKeys ...keys.FlagKey) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not totally sold on this -- I think exposing keys.FlagKey feels wrong, it's abstraction breaking in a way. But I think it's fine to keep it in this PR

@@ -0,0 +1,189 @@
package flags
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we'll want to keep this, vs absorbing it into knapsack. I guess we'll see!

agentFlagsStore types.KVStore
overrideMutex sync.RWMutex
controlRequestOverride FlagValueOverride
observers map[types.FlagsChangeObserver][]keys.FlagKey
Copy link
Contributor

Choose a reason for hiding this comment

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

Does observers need a mutex?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doesn't hurt to add one - in practice I think most observers will register early and wait for notifications, so modifications to the map would be rare later on. But better to be safe.

Comment on lines +40 to +43
// getControlServerValue looks for a control-server-provided value for the key and returns it.
// If a control server value is not found, nil is returned.
func (fc *FlagController) getControlServerValue(key keys.FlagKey) []byte {
value, err := fc.agentFlagsStore.Get([]byte(key))
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment and name is control server, but the underlying store is simply agentFlagsStore. This is a bit confusing, given the next set function, which uses the same store, but does not use the control server name

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 I can clean up the language inconsistency here.

Comment on lines +72 to +73
// Extract just the keys from the key-value pairs
updatedKeys := make([]string, len(kvPairs))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine, but FYI there's now an x/exp/maps. (no idea how the generics work there, or if it would need casting, as said, this is fine)

}
}

func WithSanitizer(sanitizer func(value string) string) stringFlagValueOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense. Though I wonder if we should write a WithEnum as a precanned sanitizer. Eh, wait till we need it...


// When adding a new FlagKey:
// 1. Define the FlagKey identifier, and the string key value it corresponds to, in the block below
// 2. Add a getter and setter to the Flags interface (flags.go)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this still correct?

desktopRunner.WithLogger(logger),
desktopRunner.WithUpdateInterval(time.Second*5),
desktopRunner.WithMenuRefreshInterval(time.Minute*15),
desktopRunner.WithHostname(opts.KolideServerURL),
desktopRunner.WithAuthToken(ulid.New()),
desktopRunner.WithUsersFilesRoot(rootDirectory),
desktopRunner.WithProcessSpawningEnabled(desktopProcessSpawningEnabled),
desktopRunner.WithGetter(k.AgentFlagsStore()),
desktopRunner.WithProcessSpawningEnabled(k.DesktopEnabled()),
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, long term, we should replace things like desktopRunner.WithProcessSpawningEnabled(k.DesktopEnabled()) with having the underlying thing just call k.DesktopEnabled() directly.

We don't expect there to be other consumers of desktopRunner and the functional arguments exist mostly as a style to manage the developer experience around them. I think knapsack replaces them.

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 that's a good point, I'll add this to a list of cleanup items. I'll check and see if there are other option style functions that can be eliminated as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it's all the long arc of using knapsack.


## Retrieving Flags

```mermaid
Copy link
Contributor

Choose a reason for hiding this comment

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

This diagram is different than the implementation. It's semantically the same, but the implementation calculates each value, and then adds the next overlay. This diagram implies early returns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, I'll make this more accurate.

type Option func(*FlagController)

// WithStore sets the key/value store for control data
func WithCmdLineOpts(cmdLineOpts *launcher.Options) Option {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this basically required? Do you think we'll have more options? (eg: should we just add it to the method signature)

@seejdev seejdev merged commit 2d3108a into kolide:main Apr 17, 2023
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.

4 participants