diff --git a/agent/agent_configuration.go b/agent/agent_configuration.go index be3ca3fd35..460e11c989 100644 --- a/agent/agent_configuration.go +++ b/agent/agent_configuration.go @@ -36,6 +36,8 @@ type AgentConfiguration struct { StrictSingleHooks bool RunInPty bool KubernetesExec bool + LocalHooksFailureBehavior string + PluginsFailureBehavior string SigningJWKSFile string // Where to find the key to sign pipeline uploads with (passed through to jobs, they might be uploading pipelines) SigningJWKSKeyID string // The key ID to sign pipeline uploads with diff --git a/agent/job_runner.go b/agent/job_runner.go index 896c0d4736..da4002defa 100644 --- a/agent/job_runner.go +++ b/agent/job_runner.go @@ -46,6 +46,9 @@ const ( VerificationBehaviourWarn = "warn" VerificationBehaviourBlock = "block" + + DisabledBehaviourWarn = "warn" + DisabledBehaviourError = "error" ) // Certain env can only be set by agent configuration. @@ -490,6 +493,8 @@ func (r *JobRunner) createEnvironment(ctx context.Context) ([]string, error) { env["BUILDKITE_COMMAND_EVAL"] = fmt.Sprintf("%t", r.conf.AgentConfiguration.CommandEval) env["BUILDKITE_PLUGINS_ENABLED"] = fmt.Sprintf("%t", r.conf.AgentConfiguration.PluginsEnabled) env["BUILDKITE_LOCAL_HOOKS_ENABLED"] = fmt.Sprintf("%t", r.conf.AgentConfiguration.LocalHooksEnabled) + env["BUILDKITE_LOCAL_HOOKS_FAILURE_BEHAVIOR"] = r.conf.AgentConfiguration.LocalHooksFailureBehavior + env["BUILDKITE_PLUGINS_FAILURE_BEHAVIOR"] = r.conf.AgentConfiguration.PluginsFailureBehavior env["BUILDKITE_GIT_CHECKOUT_FLAGS"] = r.conf.AgentConfiguration.GitCheckoutFlags env["BUILDKITE_GIT_CLONE_FLAGS"] = r.conf.AgentConfiguration.GitCloneFlags env["BUILDKITE_GIT_FETCH_FLAGS"] = r.conf.AgentConfiguration.GitFetchFlags diff --git a/clicommand/agent_start.go b/clicommand/agent_start.go index 76ee23a3ee..e79ed25452 100644 --- a/clicommand/agent_start.go +++ b/clicommand/agent_start.go @@ -58,6 +58,8 @@ Example: var ( verificationFailureBehaviors = []string{agent.VerificationBehaviourBlock, agent.VerificationBehaviourWarn} + localHooksFailureBehaviors = []string{agent.VerificationBehaviourBlock, agent.VerificationBehaviourWarn} + pluginsFailureBehaviors = []string{agent.VerificationBehaviourBlock, agent.VerificationBehaviourWarn} buildkiteSetEnvironmentVariables = []*regexp.Regexp{ regexp.MustCompile("^BUILDKITE$"), @@ -140,14 +142,16 @@ type AgentStartConfig struct { GitMirrorsSkipUpdate bool `cli:"git-mirrors-skip-update"` NoGitSubmodules bool `cli:"no-git-submodules"` - NoSSHKeyscan bool `cli:"no-ssh-keyscan"` - NoCommandEval bool `cli:"no-command-eval"` - NoLocalHooks bool `cli:"no-local-hooks"` - NoPlugins bool `cli:"no-plugins"` - NoPluginValidation bool `cli:"no-plugin-validation"` - NoFeatureReporting bool `cli:"no-feature-reporting"` - AllowedRepositories []string `cli:"allowed-repositories" normalize:"list"` - AllowedPlugins []string `cli:"allowed-plugins" normalize:"list"` + NoSSHKeyscan bool `cli:"no-ssh-keyscan"` + NoCommandEval bool `cli:"no-command-eval"` + NoLocalHooks bool `cli:"no-local-hooks"` + NoPlugins bool `cli:"no-plugins"` + NoPluginValidation bool `cli:"no-plugin-validation"` + NoFeatureReporting bool `cli:"no-feature-reporting"` + AllowedRepositories []string `cli:"allowed-repositories" normalize:"list"` + AllowedPlugins []string `cli:"allowed-plugins" normalize:"list"` + LocalHooksFailureBehavior string `cli:"local-hooks-failure-behavior"` + PluginsFailureBehavior string `cli:"plugins-failure-behavior"` EnableEnvironmentVariableAllowList bool `cli:"enable-environment-variable-allowlist"` AllowedEnvironmentVariables []string `cli:"allowed-environment-variables" normalize:"list"` @@ -668,6 +672,18 @@ var AgentStartCommand = cli.Command{ Usage: fmt.Sprintf("The behavior when a job is received without a signature. One of: %v. Defaults to %s", verificationFailureBehaviors, agent.VerificationBehaviourBlock), EnvVar: "BUILDKITE_AGENT_JOB_VERIFICATION_NO_SIGNATURE_BEHAVIOR", }, + cli.StringFlag{ + Name: "local-hooks-failure-behavior", + Value: agent.VerificationBehaviourBlock, + Usage: fmt.Sprintf("The behavior when a job is not allowed to run local hooks. One of: %v. Defaults to %s", localHooksFailureBehaviors, agent.DisabledBehaviourError), + EnvVar: "BUILDKITE_AGENT_LOCAL_HOOKS_FAILURE_BEHAVIOR", + }, + cli.StringFlag{ + Name: "plugins-failure-behavior", + Value: agent.VerificationBehaviourBlock, + Usage: fmt.Sprintf("The behavior when a job is not allowed to run plugins. One of: %v. Defaults to %s", pluginsFailureBehaviors, agent.DisabledBehaviourError), + EnvVar: "BUILDKITE_AGENT_PLUGINS_FAILURE_BEHAVIOR", + }, cli.StringSliceFlag{ Name: "disable-warnings-for", Usage: "A list of warning IDs to disable", @@ -927,6 +943,8 @@ var AgentStartCommand = cli.Command{ CommandEval: !cfg.NoCommandEval, PluginsEnabled: !cfg.NoPlugins, PluginValidation: !cfg.NoPluginValidation, + LocalHooksFailureBehavior: cfg.LocalHooksFailureBehavior, + PluginsFailureBehavior: cfg.PluginsFailureBehavior, LocalHooksEnabled: !cfg.NoLocalHooks, AllowedEnvironmentVariables: allowedEnvironmentVariables, StrictSingleHooks: cfg.StrictSingleHooks, diff --git a/clicommand/bootstrap.go b/clicommand/bootstrap.go index 65bc0719fd..07687f32c2 100644 --- a/clicommand/bootstrap.go +++ b/clicommand/bootstrap.go @@ -81,6 +81,8 @@ type BootstrapConfig struct { PluginValidation bool `cli:"plugin-validation"` PluginsAlwaysCloneFresh bool `cli:"plugins-always-clone-fresh"` LocalHooksEnabled bool `cli:"local-hooks-enabled"` + LocalHooksFailureBehavior string `cli:"local-hooks-failure-behavior"` + PluginsFailureBehavior string `cli:"plugins-failure-behavior"` StrictSingleHooks bool `cli:"strict-single-hooks"` PTY bool `cli:"pty"` LogLevel string `cli:"log-level"` @@ -315,6 +317,16 @@ var BootstrapCommand = cli.Command{ Usage: "Allow local hooks to be run", EnvVar: "BUILDKITE_LOCAL_HOOKS_ENABLED", }, + cli.StringFlag{ + Name: "local-hooks-failure-behavior", + Usage: "The behavior when a job is not allowed to run local hooks.", + EnvVar: "BUILDKITE_LOCAL_HOOKS_FAILURE_BEHAVIOR", + }, + cli.StringFlag{ + Name: "plugins-failure-behavior", + Usage: "The behavior when a job is not allowed to run plugins.", + EnvVar: "BUILDKITE_PLUGINS_FAILURE_BEHAVIOR", + }, cli.BoolTFlag{ Name: "ssh-keyscan", Usage: "Automatically run ssh-keyscan before checkout", @@ -443,6 +455,8 @@ var BootstrapCommand = cli.Command{ HooksPath: cfg.HooksPath, JobID: cfg.JobID, LocalHooksEnabled: cfg.LocalHooksEnabled, + LocalHooksFailureBehavior: cfg.LocalHooksFailureBehavior, + PluginsFailureBehavior: cfg.PluginsFailureBehavior, OrganizationSlug: cfg.OrganizationSlug, Phases: cfg.Phases, PipelineProvider: cfg.PipelineProvider, diff --git a/internal/job/config.go b/internal/job/config.go index ca07fad9f2..f21bf4fa97 100644 --- a/internal/job/config.go +++ b/internal/job/config.go @@ -107,6 +107,12 @@ type ExecutorConfig struct { // Are local hooks enabled? LocalHooksEnabled bool + // What failure behaviour is configured for local hooks no being allowed to run + LocalHooksFailureBehavior string + + // What failure behaviour is configured for plugins no being allowed to run + PluginsFailureBehavior string + // Should we enforce that only one checkout and one command hook are run? StrictSingleHooks bool diff --git a/internal/job/executor.go b/internal/job/executor.go index c15b94ae80..8ab7bf3ee8 100644 --- a/internal/job/executor.go +++ b/internal/job/executor.go @@ -20,6 +20,7 @@ import ( "syscall" "time" + "github.com/buildkite/agent/v3/agent" "github.com/buildkite/agent/v3/agent/plugin" "github.com/buildkite/agent/v3/env" "github.com/buildkite/agent/v3/internal/experiments" @@ -693,7 +694,11 @@ func (e *Executor) executeLocalHook(ctx context.Context, name string) error { } if !localHooksEnabled { - return fmt.Errorf("Refusing to run %s, local hooks are disabled", localHookPath) + if e.ExecutorConfig.LocalHooksFailureBehavior == agent.DisabledBehaviourError { + return fmt.Errorf("Refusing to run %s, local hooks are disabled", localHookPath) + } + + e.shell.Warningf("Refusing to run %s, local hooks are disabled", localHookPath) } return e.executeHook(ctx, HookConfig{ diff --git a/internal/job/plugin.go b/internal/job/plugin.go index 793bb818ed..94e5e5d999 100644 --- a/internal/job/plugin.go +++ b/internal/job/plugin.go @@ -11,8 +11,10 @@ import ( "strings" "time" + "github.com/buildkite/agent/v3/agent" "github.com/buildkite/agent/v3/agent/plugin" "github.com/buildkite/agent/v3/internal/job/hook" + "github.com/buildkite/agent/v3/internal/job/shell" "github.com/buildkite/agent/v3/internal/utils" "github.com/buildkite/roko" ) @@ -39,18 +41,17 @@ func (e *Executor) preparePlugins() error { e.shell.Commentf("Plugin JSON is %s", e.Plugins) } - // Check if we can run plugins (disabled via --no-plugins) - if !e.ExecutorConfig.PluginsEnabled { - if !e.ExecutorConfig.LocalHooksEnabled { - return fmt.Errorf("Plugins have been disabled on this agent with `--no-local-hooks`") - } else if !e.ExecutorConfig.CommandEval { - return fmt.Errorf("Plugins have been disabled on this agent with `--no-command-eval`") - } else { - return fmt.Errorf("Plugins have been disabled on this agent with `--no-plugins`") - } + // Check if we can run plugins (disabled via --no-plugins or --no-local-hooks) + enabled, err := checkPluginsEnabled(e.shell.Logger, e.ExecutorConfig) + if err != nil { + return err + } + + // plugins are disabled, so we don't need to do anything + if !enabled { + return nil } - var err error e.plugins, err = plugin.CreateFromJSON(e.ExecutorConfig.Plugins) if err != nil { return fmt.Errorf("Failed to parse a plugin definition: %w", err) @@ -411,3 +412,27 @@ func (e *Executor) checkoutPlugin(ctx context.Context, p *plugin.Plugin) (*plugi return checkout, nil } + +// checkPluginsEnabled verifies that plugins are enabled on the agent, and uses the configured behaviour is to either return an error, or log a warning. +func checkPluginsEnabled(logger shell.Logger, executorConfig ExecutorConfig) (bool, error) { + if executorConfig.PluginsEnabled && executorConfig.LocalHooksEnabled && executorConfig.CommandEval { + return true, nil + } + + switch { + case !executorConfig.LocalHooksEnabled: + if executorConfig.LocalHooksFailureBehavior == agent.DisabledBehaviourError { + return false, fmt.Errorf("Plugins have been disabled on this agent with `--no-local-hooks`") + } + logger.Warningf("Plugins have been disabled on this agent with `--no-local-hooks`") + case !executorConfig.CommandEval: + return false, fmt.Errorf("Plugins have been disabled on this agent with `--no-command-eval`") + default: + if executorConfig.PluginsFailureBehavior == agent.DisabledBehaviourError { + return false, fmt.Errorf("Plugins have been disabled on this agent with `--no-plugins`") + } + logger.Warningf("Plugins have been disabled on this agent with `--no-plugins`") + } + + return false, nil +} diff --git a/internal/job/plugin_test.go b/internal/job/plugin_test.go new file mode 100644 index 0000000000..62baf3b6f0 --- /dev/null +++ b/internal/job/plugin_test.go @@ -0,0 +1,129 @@ +package job + +import ( + "testing" + + "github.com/buildkite/agent/v3/internal/job/shell" +) + +func Test_checkPluginsEnabled(t *testing.T) { + type args struct { + logger shell.Logger + executorConfig ExecutorConfig + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "Plugins and local hooks are Enabled", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: true, + LocalHooksEnabled: true, + CommandEval: true, + }, + }, + want: true, + }, + { + name: "Plugins are disabled", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: false, + LocalHooksEnabled: true, + CommandEval: true, + PluginsFailureBehavior: "error", + }, + }, + want: false, + wantErr: true, + }, + { + name: "Plugins are disabled, and failure behavior is warn", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: false, + LocalHooksEnabled: true, + CommandEval: true, + PluginsFailureBehavior: "warn", + }, + }, + want: false, + wantErr: false, + }, + { + name: "Local hooks are disabled, and failure behavior is error", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: true, + LocalHooksEnabled: false, + LocalHooksFailureBehavior: "error", + }, + }, + want: false, + wantErr: true, + }, + { + name: "Local hooks are disabled, and failure behavior is warn", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: true, + LocalHooksEnabled: false, + CommandEval: true, + LocalHooksFailureBehavior: "warn", + }, + }, + want: false, + wantErr: false, + }, + { + name: "Local hooks are disabled, and failure behavior is warn", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: false, + LocalHooksEnabled: true, + CommandEval: true, + PluginsFailureBehavior: "warn", + }, + }, + want: false, + wantErr: false, + }, + { + name: "command eval is disabled", + args: args{ + logger: shell.DiscardLogger, + executorConfig: ExecutorConfig{ + PluginsEnabled: true, + LocalHooksEnabled: true, + CommandEval: false, + }, + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkPluginsEnabled(tt.args.logger, tt.args.executorConfig) + if err != nil { + if !tt.wantErr { + t.Errorf("checkPluginsEnabled() error = %v, wantErr %v", err, tt.wantErr) + } + } + if got != tt.want { + t.Errorf("checkPluginsEnabled() = %v, want %v", got, tt.want) + } + }) + } + +}