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

Improve menu items to allow templated strings #992

Merged
merged 17 commits into from
Feb 13, 2023
Merged

Conversation

seejdev
Copy link
Contributor

@seejdev seejdev commented Jan 20, 2023

Fixed #988

This PR introduces functionality to the menu which allows templated strings to be used within menu item text fields. This enables launcher to fill in data that k2 may not be able to get, and further customize the experience to the individual launcher instance.

I've limited the template data to a few simple fields. We can always add more to the list to be used in future menus.

type TemplateData struct {
	LauncherVersion  string
	LauncherRevision string
	GoVersion        string
	ServerHostname   string
}

The template data is generated by the desktop runner, and applied to either menu updates from the control server, or the default menu. The template expansion is performed on the raw JSON from the control server, so we are able to parse and expand any and all fields, rather than parsing as-needed and per-field. The default menu is created if no there is no existing menu.

@seejdev seejdev force-pushed the issue988 branch 3 times, most recently from 614e2b1 to 768c436 Compare January 23, 2023 15:51
@seejdev seejdev marked this pull request as ready for review January 23, 2023 18:28
@seejdev seejdev marked this pull request as draft January 26, 2023 16:56
@seejdev
Copy link
Contributor Author

seejdev commented Jan 26, 2023

Moving this PR back to draft status while I refactor how template data is injected in to desktop.

…op via JSON. Applied some feedback as well.
@seejdev seejdev force-pushed the issue988 branch 3 times, most recently from 5c334d0 to 3195231 Compare January 30, 2023 15:31
@seejdev seejdev marked this pull request as ready for review January 30, 2023 15:53
RebeccaMahany
RebeccaMahany previously approved these changes Jan 30, 2023
RebeccaMahany
RebeccaMahany previously approved these changes Jan 31, 2023
Copy link
Contributor

@RebeccaMahany RebeccaMahany left a comment

Choose a reason for hiding this comment

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

LGTM!

directionless
directionless previously approved these changes Feb 3, 2023
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.

I'm happy to try this, though I think I have 2 questions.

First, I think this is doing the template expansion in the Desktop process, correct? That feels correct, in that it gets the desktop version, but I think it's going to be harder to pass highly dynamic data through. (Which is mostly only in the daemon) OTOH, maybe we'll drop a bunch of variables, and a template file. 🤷

Second, I think this is doing the templating expansion at each string? Does it make sense to expand once at the initial file read time?

@seejdev
Copy link
Contributor Author

seejdev commented Feb 3, 2023

I'm happy to try this, though I think I have 2 questions.

First, I think this is doing the template expansion in the Desktop process, correct? That feels correct, in that it gets the desktop version, but I think it's going to be harder to pass highly dynamic data through. (Which is mostly only in the daemon) OTOH, maybe we'll drop a bunch of variables, and a template file. 🤷

Second, I think this is doing the templating expansion at each string? Does it make sense to expand once at the initial file read time?

Yes, you're correct the template expansion is handled by desktop process. I hadn't thought about doing that in launcher proper, but it could work and may be a better approach. Launcher proper can easily access more data and handle more dynamic data queries/templates, but the user process could be better suited to handle things like i18n and user-centric data.

I like the idea of expanding once - it simplifies some of the design. In order to do that, I'd have to take the JSON from k2 control server, feed the raw JSON bytes to the template parsing, and then unmarshal the expanded output back into JSON. One potential drawback is that any templating error in any field would cause all of the menu data to be treated as invalid, rather than the one field.

But I still kind of like desktop being more "dumb" and not having to worry about template expansion, etc. I'll see how easy it is to move some of this to launcher proper.

@directionless
Copy link
Contributor

I like the idea of expanding once - it simplifies some of the design. In order to do that, I'd have to take the JSON from k2 control server, feed the raw JSON bytes to the template parsing, and then unmarshal the expanded output back into JSON. One potential drawback is that any templating error in any field would cause all of the menu data to be treated as invalid, rather than the one field.

Yes, I think that's right. Template expand before the json unmarshal. Template errors would produce bad content, but then again, so does bad json. So 🤷 server has to be careful either way

@seejdev
Copy link
Contributor Author

seejdev commented Feb 7, 2023

I like the idea of expanding once - it simplifies some of the design. In order to do that, I'd have to take the JSON from k2 control server, feed the raw JSON bytes to the template parsing, and then unmarshal the expanded output back into JSON. One potential drawback is that any templating error in any field would cause all of the menu data to be treated as invalid, rather than the one field.

Yes, I think that's right. Template expand before the json unmarshal. Template errors would produce bad content, but then again, so does bad json. So 🤷 server has to be careful either way

So I tried going down this path, but it has some wrinkles. My biggest concern was this: performing the template parsing/expansion on raw JSON strings means that the replacement strings in the template data must be JSON-compliant, and any special characters must be escaped. I felt that was a weird restriction to put on the template data, and it made me worried that adding this sort of non-standard manipulation of JSON exposes us to various ways we could produce malformed JSON as a result.

Having launcher desktop to do the template expansion also enables the default/fallback menu to use templates too.

So if we're okay with doing the template expansion in launcher desktop, the second question is should we do it on-demand per-field (how it's working now), or all at once. It probably doesn't make a big difference either way, other than that doing it up front entirely simplifies some design parts where the textParser dependency doesn't need to be passed to as many things.

@directionless
Copy link
Contributor

directionless commented Feb 8, 2023

So if we're okay with doing the template expansion in launcher desktop, the second question is should we do it on-demand per-field (how it's working now), or all at once. It probably doesn't make a big difference either way, other than that doing it up front entirely simplifies some design parts where the textParser dependency doesn't need to be passed to as many things.

Of the 4 choices enumerated (launcher vs desktop and single shot vs per-field) I'm good with whatever you chose.

I suspect doing the parsing as a single shot is going to be both simpler, and more powerful, so I'd bias that way. But, it's pretty easy to iterate.

CJ Barrett added 2 commits February 10, 2023 14:52
@seejdev
Copy link
Contributor Author

seejdev commented Feb 10, 2023

So if we're okay with doing the template expansion in launcher desktop, the second question is should we do it on-demand per-field (how it's working now), or all at once. It probably doesn't make a big difference either way, other than that doing it up front entirely simplifies some design parts where the textParser dependency doesn't need to be passed to as many things.

Of the 4 choices enumerated (launcher vs desktop and single shot vs per-field) I'm good with whatever you chose.

I suspect doing the parsing as a single shot is going to be both simpler, and more powerful, so I'd bias that way. But, it's pretty easy to iterate.

I made the change to expand templates up-front and as a single-pass of the entire menu data. I'm happy with this approach, as it reduces complexity in the desktop process, reduces dependencies of the menu itself, and we get the nice property that any templated data added to menus in the future will automatically get parsed with out additional launcher changes (other than adding new templated fields).

return "", fmt.Errorf("templateData is nil")
}

t, err := template.New("menu_template").Parse(text)
Copy link
Contributor

Choose a reason for hiding this comment

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

template dohicky is really cool and from stdlib ❤️

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.

Looks really clean. Some nits.

return "", fmt.Errorf("could not parse template: %w", err)
}

var b strings.Builder
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 fine with this. But... Since Execute takes an io.Writer, I'd probably move that up to the method call. Then you get to do things like return t.Execute(w, tp.td). I suspect callers will get simpler too

_, err := os.Stat(menuPath)

if os.IsNotExist(err) {
if err := r.generateMenuFile(bytes.NewReader(menu.InitialMenu)); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not totally sure what I think. I'll go with it, but it feels a little tangled using menu.InitialMenu here. I think to use that, we need to add in the defaults. Which was the purpose of calls like newInitialMenuData.

So this is going to write a partial menu, which then has to get read and have the defaults added in. Is it better to leave this blank and fall through later? I don't know. (And thus feel happy to roll with this)

Comment on lines +371 to +381
parser := menu.NewTemplateParser(td)
parsedMenuDataStr, err := parser.Parse(string(menuDataBytes))
if err != nil {
return fmt.Errorf("failed to parse menu data: %w", err)
}

// Convert the parsed string back to bytes, which can now be decoded per usual
parsedMenuDataBytes := []byte(parsedMenuDataStr)

var menu menu.MenuData
if err := json.NewDecoder(bytes.NewReader(parsedMenuDataBytes)).Decode(&menu); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say lets followup, but I think this is all cleaner if Parse works off io readers and writers. There's a lot of extra buffering

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, I'll add this to a follow up PR!

@seejdev seejdev merged commit 3809194 into kolide:main Feb 13, 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.

Improve menu items to allow templated labels
4 participants