-
Notifications
You must be signed in to change notification settings - Fork 196
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
[plugins] Implement plugins v2 #1850
Conversation
e87d345
to
b8169c0
Compare
f07d2a1
to
2eba821
Compare
From reading the summary, a couple of callouts: Design of github plugins
Two concerns:
Plugins evaluation order
This makes sense. How are we handling plugins that are applied on packages added in the base-config? For example, a Rust plugin that must be applied to the version of Rust included in the base config. |
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.
Reviewed some. Pausing to do other work. Will resume.
Good:
- Love that we are moving plugin code out of functions like
computeEnv
Scenarios to consider:
- if a plugin changes, then
devbox shell
will get the latest content. How will direnv know to refresh its environment?
return false | ||
} | ||
return !cfg.Root.Equals(&DefaultConfig().Root) | ||
} |
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: Can we have this be IsDefault
and caller can use the not
operator?
Otherwise, in the future, if we'll want to write a function that needs IsDefault
and will have to call it as !devconfig.IsNotDefault
and we get a double negative
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.
will do in cleanup.
internal/devconfig/config.go
Outdated
b, err := os.ReadFile(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
baseConfig, err := loadBytes(b) | ||
return loadBytes(b) |
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.
can replace entire function's implemenation with return readFromFile(path)
}, nil | ||
} | ||
|
||
func (c *Config) LoadRecursive(lockfile *lock.File) error { |
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.
reviewed till here.
2eba821
to
b647d04
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.
Also still reviewing. I'll finish it up this afternoon.
internal/devconfig/config.go
Outdated
} | ||
|
||
func (c *Config) PackageMutator() *packagesMutator { | ||
func LoadConfigFromURL(url string) (*Config, error) { |
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.
Pass in context so HTTP request can be interrupted.
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.
Pausing for a bit. Left some more comments but not any fundamental design problems so far!
Question:
- is there a way to disable a builtin plugin by having an includable plugin supersede it? For example, the builtin python plugin may be doing something a user feels is undesirable and would like to either disable it completely, or override it with their own python plugin
internal/devconfig/config.go
Outdated
func (c *Config) LoadRecursive(lockfile *lock.File) error { | ||
included := make([]*Config, 0, len(c.Root.Include)) | ||
|
||
for _, importRef := range c.Root.Include { |
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: will be nice to stick to consistent terminology by calling it includeRef
instead of importRef
internal/devconfig/config.go
Outdated
} | ||
|
||
builtIns, err := plugin.GetBuiltinsForPackages( | ||
c.Root.PackagesMutator.Collection, |
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.
It's a bit odd to have PackagesMutator
be the source of reading which packages are in the devbox.json
file. Is there a nicer API we can use?
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.
Something like configFileStruct.Packages()
so the code here would be c.Root.Packages()
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 implementing Packages() in both the config and the configFile would be a bit confusing.
I actually think Collection
can be private. This would make it an implementation detail. Thoughts?
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.
Never mind, this is used outside of the configfile package.
I'm adding SingleFilePackages
and will make Collection
unexported.
internal/devconfig/config.go
Outdated
pluginData := builtIn.PluginOnlyData | ||
includable.pluginData = &pluginData |
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 follow: why are we re-setting the includable.pluginData
here after the LoadRecursive
call?
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 looks weird. This may be a mistake.
internal/devconfig/config.go
Outdated
return &c.Root.PackagesMutator | ||
} | ||
|
||
func (c *Config) Packages() []Package { | ||
return c.Root.PackagesMutator.collection | ||
func (c *Config) PluginConfigs() []*plugin.Config { |
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 we call this IncludedPluginConfigs()
?
Because: I'd have expected PluginConfigs()
to be IncludedPluginConfigs() + BuiltInPluginConfigs()
if c.pluginData != nil { | ||
configs = append(configs, &plugin.Config{ | ||
ConfigFile: c.Root, | ||
PluginOnlyData: *c.pluginData, | ||
}) | ||
} |
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 follow. Why do we add this additional plugin.Config
?
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 is the step that actually adds the current plugin in the recursive stack. In pseudocode:
configs = get-all-child-configs;
if cur-is-plugin:
configs = append(configs, config-for-cur)
It uses c.pluginData != nil
to ensure it doesn't add the top level devbox.json (which is not a plugin).
It runs after the for loop in order to respect the order configs are evaluated in.
func (c *ConfigFile) Bytes() []byte { | ||
b := c.ast.root.Pack() | ||
return bytes.ReplaceAll(b, []byte("\t"), []byte(" ")) | ||
} | ||
|
||
func (c *ConfigFile) Hash() (string, error) { | ||
if c.ast == nil { | ||
return cachehash.JSON(c) | ||
} |
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.
in what scenario would c.ast
be nil
?
Instead, should we be building the ast in this if-block?
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 configs don't use hujson (but will in the future). Scope of this change is already too big to include that moving part.
pkg.patchGlibc = sync.OnceValue(func() bool { | ||
return cfgPkg.PatchGlibc && nix.SystemIsLinux() |
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 changing to a function really needed?
I'm not convinced because:nix.SystemIsLinux
relies on nix.System
which is pre-computed once when running the cobra command, so likely won't be an actual system call here
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 is needed because unfortunately devbox CLI calls devbox.Open
when populating autocomplete for scripts. (https://github.com/jetpack-io/devbox/blob/main/internal/boxcli/run.go#L54-L54). Note that this code happens before ensureNixInstalled
is called. Arguably, we don't want to require nix for stuff like listing scripts which doesn't need nix.
So devbox.Open
cannot call anything requiring nix, otherwise all commends will fail. Without this change, just parsing a devbox.json requires nix.
We should probably replace command.ValidArgs
with command.ValidArgsFunction
but even then, I think it's useful to keep devbox.Open
working without nix.
@@ -4,11 +4,22 @@ import ( | |||
"strings" | |||
|
|||
"go.jetpack.io/devbox/internal/devpkg" | |||
"go.jetpack.io/devbox/internal/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.
Reviewed till here.
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.
@mikeland73 nice, overall lgtm. Please re-request review for when I should look again.
Please see above comments in the comment-box and in code for:
- questions about areas i couldn't follow why changes were made
- suggestions for minor improvements
A concern with this design is that scripts
must be fully embedded in the plugin's devbox.json
file, rather than moved into their own my_script.sh
file where they can be properly handled by tooling (e.g. syntax-hightlighting, linting, etc.). Enabling that scenario would be a big win IMO.
One idea for a follow up PR is to add special-case support for detecting script files in plugin-configs, and downloading them as well.
@@ -49,37 +43,3 @@ func (m *Manager) ApplyOptions(opts ...managerOption) { | |||
opt(m) | |||
} | |||
} | |||
|
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 is the role of the Plugin Manager, after this PR's changes?
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.
After this PR it does:
- Builds includables and creates files
- Figures out what services a plugin has.
We may want to split those out. Maybe the services stuff goes into the services package and the includable stuff moves into devconf
internal/plugin/plugin.go
Outdated
// If true, we remove the package that triggered this plugin from the environment | ||
// Useful when we want to replace with flake | ||
RemoveTriggerPackage bool `json:"__remove_trigger_package,omitempty"` | ||
Version string `json:"version"` | ||
Source Includable |
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 we add a comment explaining what Source
is?
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.
will do
} | ||
|
||
func parseReflike(s string) (Includable, error) { | ||
func parseReflike(s, projectDir string) (Includable, error) { |
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 is s
? Lets rename it to a bigger, meaningful term
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.
same for withFilename
below
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.
Will do as part of cleanup
internal/plugin/services.go
Outdated
pkgs []*devpkg.Package, | ||
includes []string, | ||
) (services.Services, error) { | ||
func (m *Manager) GetServices(configs []*Config) (services.Services, error) { |
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 we make this a standalone package function? Seems like it no longer needs to be a struct method
Currently you have to use
This use case is doable, but requires a bit more work from the user. If you want script files they must be copied over using |
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.
nice!
internal/devconfig/config.go
Outdated
func LoadConfigFromURL(url string) (*Config, error) { | ||
res, err := http.Get(url) | ||
func LoadConfigFromURL(ctx context.Context, url string) (*Config, error) { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, 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.
this should be http.MethodGet
, right?
On line 84, we are trying to read res.Body
which HEAD
requests don't return
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.
oof, good catch. Terrible copy pasta on my part
|
||
// SingleFilePackages returns the packages in the config file, but not the included ones. | ||
// Semi-awkwardly named to avoid confusion with the Packages method on Config. | ||
func (c *ConfigFile) SingleFilePackages() []Package { |
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.
TopLevelPackages
?
// Collection contains the set of package definitions | ||
Collection []Package | ||
// collection contains the set of package definitions | ||
collection []Package |
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.
👍🏾
## Summary Stacked on #1850 * Remove RefLike * Remove experimental tyson support (v2 plugins are a reasonable substitute) ## How was it tested? CICD
Suspect IssuesThis pull request was deployed and Sentry observed the following issues:
Did you find this useful? React with a 👍 or 👎 |
Summary
Apologies in advance for the size of this PR. Was not really sure how to split it up, since it's a fairly large step change.
This PR makes 2 major changes:
Env
,Scripts
, etc) they are now combined inside ofdevconfig
in a simple, predictable way.All fields from included plugins are added to the environment. The merge algorithm is as follows:
Known issues/limitations:
Additional requires testing:
add
a package that is already added by plugin.Follow up cleanup:
How was it tested?