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

Refactor client fingerprinters to return a diff of node attributes #3781

Merged
merged 12 commits into from
Feb 2, 2018

Conversation

chelseakomlo
Copy link
Contributor

No description provided.

@chelseakomlo chelseakomlo force-pushed the f-client-fingerprint-refactor branch 15 times, most recently from 5802f45 to 60b3c3d Compare January 24, 2018 16:32
@chelseakomlo chelseakomlo changed the title WIP: Refactor client fingerprinters to return a diff of node attributes Refactor client fingerprinters to return a diff of node attributes Jan 24, 2018
@chelseakomlo chelseakomlo requested review from dadgar and removed request for dadgar January 24, 2018 19:28
// and returns if the fingerprint was applicable and a potential error.
Fingerprint(*config.Config, *structs.Node) (bool, error)
// and returns a diff of updated node attributes and a potential error.
Fingerprint(*cstructs.FingerprintRequest, *cstructs.FingerprintResponse) error
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not make this be Fingerprint(*structs.FingerPrintRequest) (structs.FingerprintResponse, error) so it returns the response?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I think it could be either way. I'm curious what @dadgar thinks as we talked about this signature before.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suggested this so it will be easier to move to a remote RPC

Copy link
Contributor

Choose a reason for hiding this comment

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

For when this moves to a plugin? Makes sense

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep

client/client.go Outdated
if applies {

// remove attributes we are supposed to skip
for _, attr := range skipped {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we don't care about thiese skipped attributes and are going to delete them from the response as soon as the fingerprint call is done, I would suggest pushing that logic one level down. You could have the FingerprintRequest also take a list of skipped(better name would be ignore) and have the implementors of FingerPrint ignore or remove anything in skipped before it returns.

The current approach works, but it is not well encapsulated - its doing something, then mutating the response before using it.

}
if apply {
t.Fatalf("should not apply with config")
// test with an empty config
Copy link
Contributor

Choose a reason for hiding this comment

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

consider turning these into subtests with a test case struct to capture inputs and expected output

client/client.go Outdated

// if an attribute should be skipped, remove it from the list which we will
// later apply to the node
for _, e := range skipped {
Copy link
Contributor

Choose a reason for hiding this comment

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

These aren't attributes that are being skipped. These are whole fingerprinters so they won't have created any attribute that needs to be deleted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, only visible after expanding to get context that skipped is constructed from fingerprinter names. The diff made it look like it was mutating the output based on the skipped list.

client/client.go Outdated
delete(response.Attributes, e)
}

for name := range response.Attributes {
Copy link
Contributor

Choose a reason for hiding this comment

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

The applied list is applied fingerprinters not the attributes.

func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintResponse) {
c.configLock.Lock()
defer c.configLock.Unlock()
for name, val := range response.Attributes {
Copy link
Contributor

Choose a reason for hiding this comment

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

How are you handling removing Attributes/Links?

Copy link
Contributor

Choose a reason for hiding this comment

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

As in fingerprinter_foo adds attribute = {"foo": "bar", "bam": "baz"} and then the next time it is called only returns {"foo": "bar"} because "bam" should be removed.

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. One way we can handle this is if the attribute is equal to empty in the fingerprinter's response, we can delete this attribute from the node's attributes. That way we don't need to rely on the fingerprinter modifying the node directly.

client/client.go Outdated
c.config.Node.Links[name] = val
}

c.config.Node.Resources.Merge(response.Resources)
Copy link
Contributor

Choose a reason for hiding this comment

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

I am fine with this strategy for now but this doesn't let a fingerprinter report a non-zero value and then set it to zero. The long term solution is that we pass in a different struct type that has all the fields as a pointer and as long as the fingerprinter sets the pointer we merge the value

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'll add that as a tech task to complete after this refactor.

client/client.go Outdated
c.configLock.Unlock()

request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node}
response := &cstructs.FingerprintResponse{
Copy link
Contributor

Choose a reason for hiding this comment

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

Would prefer if it looked like:

var resp cstructs.FingerprintResponse
if err := d.Fingerprint(req, &resp); err != nil {
  // Handle
}

You can then have methods on the response to be like:

func (f *FingerprintResponse) AddAttribute(attr, value string) {...}

Where it initializes the value.

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 idea

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, we could have a constructor which would initialize all of these values. I will add getter/setter methods that does initialization for each field, let me know if you think it is cleaner to just do initialization all at once in a constructor.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah the constructor could work but it is just idiomatic go to call an RPC like I showed, which is the direction we will be moving.

write test for client periodic fingerprinters
}

type FingerprintResponse struct {
attributes map[string]string
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be public.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are getters and setters for these, to prevent against uninitialized fields. Alternatively, we could have a constructor for FingerprintResponse which would do initialization of values all at once.

type FingerprintResponse struct {
attributes map[string]string
links map[string]string
resources *structs.Resources
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have a Applicable bool in the response so we can maintain our old behavior of displaying whether the fingerprinter is being applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, we could do that, that would probably be cleaner than parsing attribute values after the fact.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second thought- would logging drivers where fingerprintSuccess is true achieve the same goal?

client/client.go Outdated
// Apply the finerprint to our list so that we can log it later
appliedFingerprints = append(appliedFingerprints, name)

request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe you can get rid of the lock yet. This is a shared data structure that is being mutated/read concurrently. Apples to all places you are doing a fingerprint.

public fields and remove getter functions
client/client.go Outdated
return err
}
if applies {
avail = append(avail, name)
c.configLock.Unlock()
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to unlock on error. Should probably just capture the error, unlock and then check if it isn't nil

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 catch

if response.Resources != nil {
c.config.Node.Resources.Merge(response.Resources)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You should change the node watcher from checking periodically to check via a channel trigger and send it when this function is called: https://github.com/hashicorp/nomad/blob/master/client/client.go#L1588

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will be in a follow up PR.


c1 := testClient(t, func(c *config.Config) {
c.Options = map[string]string{
"test.shutdown_periodic_after": "true",
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make this a constant on the mock driver.
"test.mock_driver.shutdown_periodic..."

}
t.Logf("Found docker version %s", node.Attributes["driver.docker.version"])

t.Logf("Found docker version %s", attributes["driver.docker.version"])
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be require.NotEmpty(attributes["driver.docker.version"])

}

attributes := response.Attributes
if attributes == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use require

// Only enable if cgroups are available and we are root
if !cgroupsMounted(node) {
if !cgroupsMounted(req.Node) {
if d.fingerprintSuccess == nil || *d.fingerprintSuccess {
d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make this an INFO level log

if d.fingerprintSuccess == nil || *d.fingerprintSuccess {
d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
delete(node.Attributes, execDriverAttr)
return false, nil
resp.RemoveAttribute(execDriverAttr)
Copy link
Contributor

Choose a reason for hiding this comment

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

In both these cases applicable should be true?

// Only enable if we are root and cgroups are mounted when running on linux systems.
if runtime.GOOS == "linux" && (syscall.Geteuid() != 0 || !cgroupsMounted(node)) {
if runtime.GOOS == "linux" && (syscall.Geteuid() != 0 || !cgroupsMounted(req.Node)) {
if d.fingerprintSuccess == nil || *d.fingerprintSuccess {
d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling")
Copy link
Contributor

Choose a reason for hiding this comment

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

INFO?

// if the shutdown configuration options are set, start the timer here.
// This config option defaults to false
if ctx.config != nil && ctx.config.ReadBoolDefault(fingerprint.ShutdownPeriodicAfter, false) {
duration, err := ctx.config.ReadInt(fingerprint.ShutdownPeriodicDuration)
Copy link
Contributor

Choose a reason for hiding this comment

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

How about storing the duration and start time as a field on the mock driver and changing the Fingerprint method to be if time.Now().After(d.shutdownFingerprintTime) {. Avoids the other function and goroutine

@@ -119,6 +119,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(javaDriverAttr)
resp.Detected = true
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably move this block to below checking for the java binary.

client/client.go Outdated
@@ -979,11 +990,18 @@ func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d t
for {
select {
case <-time.After(d):
request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node}
var response cstructs.FingerprintResponse
Copy link
Contributor

Choose a reason for hiding this comment

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

The lock should wrap creating the request since it is accessing the config. (not just here)

cstructs "github.com/hashicorp/nomad/client/structs"
"github.com/hashicorp/nomad/nomad/structs"
)

const (

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove blank line

@@ -184,3 +187,61 @@ func (d *DriverNetwork) Hash() []byte {
}
return h.Sum(nil)
}

type FingerprintRequest struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments on all public structs

@chelseakomlo chelseakomlo merged commit 7b9cf12 into master Feb 2, 2018
@chelseakomlo chelseakomlo deleted the f-client-fingerprint-refactor branch February 2, 2018 01:13
@github-actions
Copy link

I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants