-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Integrate binary provisioning into k6 root command #4801
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
Open
pablochacin
wants to merge
7
commits into
master
Choose a base branch
from
binary-provisioning/integrate-root-cmd
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e6564f6
initial integration
pablochacin b1ba5b7
refactor provisioning into an interface
pablochacin da4342e
attend review comments
pablochacin 89cf479
add reference to issue
pablochacin f3030f7
Merge branch 'master' into binary-provisioning/integrate-root-cmd
pablochacin f66ae6a
attend linter issue
pablochacin afd8b69
fix type in comment
pablochacin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -12,6 +12,7 @@ import ( | |||||||
"github.com/Masterminds/semver/v3" | ||||||||
"github.com/grafana/k6deps" | ||||||||
"github.com/grafana/k6provider" | ||||||||
"github.com/spf13/cobra" | ||||||||
"go.k6.io/k6/cloudapi" | ||||||||
"go.k6.io/k6/cmd/state" | ||||||||
"go.k6.io/k6/internal/build" | ||||||||
|
@@ -25,63 +26,65 @@ var ( | |||||||
// commandExecutor executes the requested k6 command line command. | ||||||||
// It abstract the execution path from the concrete binary. | ||||||||
type commandExecutor interface { | ||||||||
run(*state.GlobalState) | ||||||||
run(*state.GlobalState) error | ||||||||
} | ||||||||
|
||||||||
// Launcher is a k6 launcher. It analyses the requirements of a k6 execution, | ||||||||
// then if required, it provisions a binary executor to satisfy the requirements. | ||||||||
type Launcher struct { | ||||||||
// gs is the global state of k6. | ||||||||
gs *state.GlobalState | ||||||||
// customBinary runs the requested commands on a different binary on a subprocess passing the | ||||||||
// original arguments | ||||||||
type customBinary struct { | ||||||||
// path represents the local file path | ||||||||
// on the file system of the binary | ||||||||
path string | ||||||||
} | ||||||||
|
||||||||
// provision generates a custom binary from the received list of dependencies | ||||||||
// with their constrains, and it returns an executor that satisfies them. | ||||||||
provision func(*state.GlobalState, k6deps.Dependencies) (commandExecutor, error) | ||||||||
// provisioner defines the interface for provisioning a commandExecutor for a set of dependencies | ||||||||
type provisioner interface { | ||||||||
provision(k6deps.Dependencies) (commandExecutor, error) | ||||||||
} | ||||||||
|
||||||||
// commandExecutor executes the requested k6 command line command | ||||||||
// launcher is a k6 launcher. It analyses the requirements of a k6 execution, | ||||||||
// then if required, it provisions a binary executor to satisfy the requirements. | ||||||||
type launcher struct { | ||||||||
gs *state.GlobalState | ||||||||
provider provisioner | ||||||||
commandExecutor commandExecutor | ||||||||
} | ||||||||
|
||||||||
// NewLauncher creates a new Launcher from a GlobalState using the default fallback and provision functions | ||||||||
func NewLauncher(gs *state.GlobalState) *Launcher { | ||||||||
defaultExecutor := ¤tBinary{} | ||||||||
return &Launcher{ | ||||||||
gs: gs, | ||||||||
provision: k6buildProvision, | ||||||||
commandExecutor: defaultExecutor, | ||||||||
// newLauncher creates a new Launcher from a GlobalState using the default provision function | ||||||||
func newLauncher(gs *state.GlobalState) *launcher { | ||||||||
return &launcher{ | ||||||||
gs: gs, | ||||||||
provider: newK6BuildProvider(gs), | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
// Launch executes k6 either by launching a provisioned binary or defaulting to the | ||||||||
// current binary if this is not necessary. | ||||||||
// The commandExecutor can exit the process so don't assume it will return | ||||||||
func (l *Launcher) Launch() { | ||||||||
// If binary provisioning is not enabled, continue with the regular k6 execution path | ||||||||
if !l.gs.Flags.BinaryProvisioning { | ||||||||
l.gs.Logger.Debug("Binary Provisioning feature is disabled.") | ||||||||
l.commandExecutor.run(l.gs) | ||||||||
return | ||||||||
// launch analyzies the command to be executed and its input (e.g. script) to identify its dependencies. | ||||||||
// If it has dependencies that cannot be satisfied by the current binary, it obtains a custom commandExecutor | ||||||||
// usign the provision function and delegates the execution of the command to this commandExecutor. | ||||||||
// On the contrary, continues with the execution of the command in the current binary. | ||||||||
func (l *launcher) launch(cmd *cobra.Command, args []string) error { | ||||||||
if !isAnalysisRequired(cmd) { | ||||||||
l.gs.Logger. | ||||||||
WithField("command", cmd.Name()). | ||||||||
Debug("command does not require dependency analysis") | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
l.gs.Logger. | ||||||||
Debug("Binary Provisioning feature is enabled.") | ||||||||
deps, err := analyze(l.gs, l.gs.CmdArgs[1:]) | ||||||||
deps, err := analyze(l.gs, args) | ||||||||
if err != nil { | ||||||||
l.gs.Logger. | ||||||||
WithError(err). | ||||||||
Error("Binary provisioning is enabled but it failed to analyze the dependencies." + | ||||||||
" Please, make sure to report this issue by opening a bug report.") | ||||||||
l.gs.OSExit(1) | ||||||||
return // this is required for testing | ||||||||
return err | ||||||||
} | ||||||||
|
||||||||
// if the command does not have dependencies nor a custom build is required | ||||||||
if !customBuildRequired(build.Version, deps) { | ||||||||
if !isCustomBuildRequired(build.Version, deps) { | ||||||||
l.gs.Logger. | ||||||||
Debug("The current k6 binary already satisfies all the required dependencies," + | ||||||||
" it isn't required to provision a new binary.") | ||||||||
l.commandExecutor.run(l.gs) | ||||||||
return | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
l.gs.Logger. | ||||||||
|
@@ -90,29 +93,30 @@ func (l *Launcher) Launch() { | |||||||
" The current k6 binary doesn't satisfy all dependencies, it's required to" + | ||||||||
" provision a custom binary.") | ||||||||
|
||||||||
customBinary, err := l.provision(l.gs, deps) | ||||||||
customBinary, err := l.provider.provision(deps) | ||||||||
if err != nil { | ||||||||
l.gs.Logger. | ||||||||
WithError(err). | ||||||||
Error("Failed to provision a k6 binary with required dependencies." + | ||||||||
" Please, make sure to report this issue by opening a bug report.") | ||||||||
l.gs.OSExit(1) | ||||||||
return | ||||||||
return err | ||||||||
} | ||||||||
|
||||||||
customBinary.run(l.gs) | ||||||||
l.commandExecutor = customBinary | ||||||||
|
||||||||
// override command's RunE method to be processed by the command executor | ||||||||
cmd.RunE = l.runE | ||||||||
|
||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
// customBinary runs the requested commands | ||||||||
// on a different binary on a subprocess passing the original arguments | ||||||||
type customBinary struct { | ||||||||
// path represents the local file path | ||||||||
// on the file system of the binary | ||||||||
path string | ||||||||
// runE executes the k6 command using a command executor | ||||||||
func (l *launcher) runE(_ *cobra.Command, _ []string) error { | ||||||||
return l.commandExecutor.run(l.gs) | ||||||||
} | ||||||||
|
||||||||
//nolint:forbidigo | ||||||||
func (b *customBinary) run(gs *state.GlobalState) { | ||||||||
func (b *customBinary) run(gs *state.GlobalState) error { | ||||||||
cmd := exec.CommandContext(gs.Ctx, b.path, gs.CmdArgs[1:]...) //nolint:gosec | ||||||||
|
||||||||
// we pass os stdout, err, in because passing them from GlobalState changes how | ||||||||
|
@@ -124,6 +128,8 @@ func (b *customBinary) run(gs *state.GlobalState) { | |||||||
// Copy environment variables to the k6 process and skip binary provisioning feature flag to disable it. | ||||||||
// If not disabled, then the executed k6 binary would enter an infinite loop, where it continuously | ||||||||
// process the input script, detect dependencies, and retrigger provisioning. | ||||||||
// This can be avoided by checking if the current binary has already extensions that | ||||||||
// satisfies the dependencies. See comment in isCustomBuildRequired function. | ||||||||
env := []string{} | ||||||||
for k, v := range gs.Env { | ||||||||
if k == state.BinaryProvisioningFeatureFlag { | ||||||||
|
@@ -143,7 +149,7 @@ func (b *customBinary) run(gs *state.GlobalState) { | |||||||
gs.Logger. | ||||||||
WithError(err). | ||||||||
Error("Failed to run the provisioned k6 binary") | ||||||||
gs.OSExit(1) | ||||||||
return err | ||||||||
} | ||||||||
|
||||||||
// wait for the subprocess to end | ||||||||
|
@@ -155,15 +161,7 @@ func (b *customBinary) run(gs *state.GlobalState) { | |||||||
for { | ||||||||
select { | ||||||||
case err := <-done: | ||||||||
rc := 0 | ||||||||
if err != nil { | ||||||||
rc = 1 | ||||||||
var eerr *exec.ExitError | ||||||||
if errors.As(err, &eerr) { | ||||||||
rc = eerr.ExitCode() | ||||||||
} | ||||||||
} | ||||||||
gs.OSExit(rc) | ||||||||
return err | ||||||||
case sig := <-sigC: | ||||||||
gs.Logger. | ||||||||
WithField("signal", sig.String()). | ||||||||
|
@@ -172,18 +170,12 @@ func (b *customBinary) run(gs *state.GlobalState) { | |||||||
} | ||||||||
} | ||||||||
|
||||||||
// currentBinary runs the requested commands on the current binary | ||||||||
type currentBinary struct{} | ||||||||
|
||||||||
func (b *currentBinary) run(gs *state.GlobalState) { | ||||||||
ExecuteWithGlobalState(gs) | ||||||||
} | ||||||||
|
||||||||
// customBuildRequired checks if the build is required | ||||||||
// isCustomBuildRequired checks if the build is required | ||||||||
// it's required if there is one or more dependencies other than k6 itself | ||||||||
// or if the required k6 version is not satisfied by the current binary's version | ||||||||
// TODO: get the version of any built-in extension and check if they satisfy the dependencies | ||||||||
func customBuildRequired(baseK6Version string, deps k6deps.Dependencies) bool { | ||||||||
// (see https://github.com/grafana/k6/issues/4697) | ||||||||
func isCustomBuildRequired(baseK6Version string, deps k6deps.Dependencies) bool { | ||||||||
if len(deps) == 0 { | ||||||||
return false | ||||||||
} | ||||||||
|
@@ -215,11 +207,19 @@ func customBuildRequired(baseK6Version string, deps k6deps.Dependencies) bool { | |||||||
return !k6Dependency.Constraints.Check(k6Ver) | ||||||||
} | ||||||||
|
||||||||
// k6buildProvision returns the path to a k6 binary that satisfies the dependencies and the list of versions it provides | ||||||||
func k6buildProvision(gs *state.GlobalState, deps k6deps.Dependencies) (commandExecutor, error) { | ||||||||
token, err := extractToken(gs) | ||||||||
// k6buildProvider provides a k6 binary that satisfies the dependencies using the k6build service | ||||||||
type k6buildProvider struct { | ||||||||
gs *state.GlobalState | ||||||||
} | ||||||||
|
||||||||
func newK6BuildProvider(gs *state.GlobalState) provisioner { | ||||||||
return &k6buildProvider{gs: gs} | ||||||||
} | ||||||||
|
||||||||
func (p *k6buildProvider) provision(deps k6deps.Dependencies) (commandExecutor, error) { | ||||||||
token, err := extractToken(p.gs) | ||||||||
if err != nil { | ||||||||
gs.Logger.WithError(err).Debug("Failed to get a valid token") | ||||||||
p.gs.Logger.WithError(err).Debug("Failed to get a valid token") | ||||||||
} | ||||||||
|
||||||||
if token == "" { | ||||||||
|
@@ -228,22 +228,22 @@ func k6buildProvision(gs *state.GlobalState, deps k6deps.Dependencies) (commandE | |||||||
} | ||||||||
|
||||||||
config := k6provider.Config{ | ||||||||
BuildServiceURL: gs.Flags.BuildServiceURL, | ||||||||
BuildServiceURL: p.gs.Flags.BuildServiceURL, | ||||||||
BuildServiceAuth: token, | ||||||||
BinaryCacheDir: gs.Flags.BinaryCache, | ||||||||
BinaryCacheDir: p.gs.Flags.BinaryCache, | ||||||||
} | ||||||||
|
||||||||
provider, err := k6provider.NewProvider(config) | ||||||||
if err != nil { | ||||||||
return nil, err | ||||||||
} | ||||||||
|
||||||||
binary, err := provider.GetBinary(gs.Ctx, deps) | ||||||||
binary, err := provider.GetBinary(p.gs.Ctx, deps) | ||||||||
if err != nil { | ||||||||
return nil, err | ||||||||
} | ||||||||
|
||||||||
gs.Logger. | ||||||||
p.gs.Logger. | ||||||||
Info("A new k6 binary has been provisioned with version(s): ", formatDependencies(binary.Dependencies)) | ||||||||
|
||||||||
return &customBinary{binary.Path}, nil | ||||||||
|
@@ -283,19 +283,13 @@ func analyze(gs *state.GlobalState, args []string) (k6deps.Dependencies, error) | |||||||
Manifest: k6deps.Source{Ignore: true}, | ||||||||
} | ||||||||
|
||||||||
if !isAnalysisRequired(args) { | ||||||||
gs.Logger. | ||||||||
Debug("The command to execute does not require dependency analysis.") | ||||||||
return k6deps.Dependencies{}, nil | ||||||||
} | ||||||||
|
||||||||
scriptname := scriptNameFromArgs(args) | ||||||||
if len(scriptname) == 0 { | ||||||||
if len(args) == 0 { | ||||||||
gs.Logger. | ||||||||
Debug("The command did not receive an input script.") | ||||||||
return nil, errScriptNotFound | ||||||||
} | ||||||||
|
||||||||
scriptname := args[0] | ||||||||
if scriptname == "-" { | ||||||||
gs.Logger. | ||||||||
Debug("Test script provided by Stdin is not yet supported from Binary provisioning feature.") | ||||||||
|
@@ -319,56 +313,19 @@ func analyze(gs *state.GlobalState, args []string) (k6deps.Dependencies, error) | |||||||
return k6deps.Analyze(dopts) | ||||||||
} | ||||||||
|
||||||||
// isAnalysisRequired searches for the command and returns a boolean indicating if dependency analysis is required | ||||||||
func isAnalysisRequired(args []string) bool { | ||||||||
// return early if no arguments passed | ||||||||
if len(args) == 0 { | ||||||||
return false | ||||||||
} | ||||||||
|
||||||||
// search for a command that requires binary provisioning and then get the target script or archive | ||||||||
// we handle cloud login subcommand as a special case because it does not require binary provisioning | ||||||||
for i, arg := range args { | ||||||||
switch arg { | ||||||||
case "--help", "-h": | ||||||||
return false | ||||||||
case "cloud": | ||||||||
for _, arg = range args[i+1:] { | ||||||||
if arg == "login" || arg == "--help" || arg == "-h" { | ||||||||
return false | ||||||||
} | ||||||||
} | ||||||||
return true | ||||||||
case "archive", "inspect": | ||||||||
// isAnalysisRequired returns a boolean indicating if dependency analysis is required for the command | ||||||||
func isAnalysisRequired(cmd *cobra.Command) bool { | ||||||||
switch cmd.Name() { | ||||||||
case "run": | ||||||||
// exclude `k6 cloud run` command | ||||||||
if cmd.Parent() != nil && cmd.Parent().Name() == "cloud" { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
return true | ||||||||
} | ||||||||
return false | ||||||||
case "archive", "inspect", "upload", "cloud": | ||||||||
return true | ||||||||
} | ||||||||
|
||||||||
// not found | ||||||||
return false | ||||||||
} | ||||||||
|
||||||||
// scriptNameFromArgs returns the file name passed as input and true if it's a valid script name | ||||||||
func scriptNameFromArgs(args []string) string { | ||||||||
// return early if no arguments passed | ||||||||
if len(args) == 0 { | ||||||||
return "" | ||||||||
} | ||||||||
|
||||||||
for _, arg := range args { | ||||||||
if strings.HasPrefix(arg, "-") { | ||||||||
if arg == "-" { // we are running a script from stdin | ||||||||
return arg | ||||||||
} | ||||||||
continue | ||||||||
} | ||||||||
if strings.HasSuffix(arg, ".js") || | ||||||||
strings.HasSuffix(arg, ".tar") || | ||||||||
strings.HasSuffix(arg, ".ts") { | ||||||||
return arg | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
// not found | ||||||||
return "" | ||||||||
} |
Oops, something went wrong.
Oops, something went 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.
I have the feeling we have an issue for this TODO. Please, mention here directly the issue so they are bidirectionally linked.
Uh oh!
There was an error while loading. Please reload this page.
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.
Added. This isssue is addressed in this PR #4812