common/executor.go (141 lines of code) (raw):

package common import ( "context" "errors" "fmt" "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-runner/common/buildlogger" ) // ExecutorData is an empty interface representing free-form data // executor will use. Meant to be casted, e.g. virtual machine details. type ExecutorData interface{} // ExecutorCommand stores the script executor will run on a given stage. // If Predefined it will try to use already allocated resources. type ExecutorCommand struct { Script string Stage BuildStage Predefined bool Context context.Context } // ExecutorStage represents a stage of build execution in the executor scope. type ExecutorStage string const ( // ExecutorStageCreated means the executor is being initialized, i.e. created. ExecutorStageCreated ExecutorStage = "created" // ExecutorStagePrepare means the executor is preparing its environment, initializing dependencies. ExecutorStagePrepare ExecutorStage = "prepare" // ExecutorStageFinish means the executor has finished build execution. ExecutorStageFinish ExecutorStage = "finish" // ExecutorStageCleanup means the executor is cleaning up resources. ExecutorStageCleanup ExecutorStage = "cleanup" ) // ExecutorPrepareOptions stores any data necessary for the executor to prepare // the environment for running a build. This includes runner configuration, build data, etc. type ExecutorPrepareOptions struct { Config *RunnerConfig Build *Build BuildLogger buildlogger.Logger User string Context context.Context } type NoFreeExecutorError struct { Message string } func (e *NoFreeExecutorError) Error() string { return e.Message } // Executor represents entities responsible for build execution. // It prepares the environment, runs the build and cleans up resources. // See more in https://docs.gitlab.com/runner/executors/ type Executor interface { // Shell returns data about the shell and scripts this executor is bound to. Shell() *ShellScriptInfo // Prepare prepares the environment for build execution. e.g. connects to SSH, creates containers. Prepare(options ExecutorPrepareOptions) error // Run executes a command on the prepared environment. Run(cmd ExecutorCommand) error // Finish marks the build execution as finished. Finish(err error) // Cleanup cleans any resources left by build execution. Cleanup() // GetCurrentStage returns current stage of build execution. GetCurrentStage() ExecutorStage // SetCurrentStage sets the current stage of build execution. SetCurrentStage(stage ExecutorStage) } type ManagedExecutorProvider interface { // Init initializes the executor provider. // // Some providers may require that a non-trivial setup will be done for them to work properly. They may also // run a goroutines handling provider's state and management layer. // // Init method is a hook allowing to add such behavior. // // Init MUST BE NON-BLOCKING! Init() // Shutdown terminates the executor provider. // // As noted above, some executor providers may require to maintain a long-running state and management // layer. // // Shutdown method is a hook that allows to inform the executor provider that it should terminate // itself. // // Shutdown MUST BE BLOCKING until termination is done or provided context is canceled. // // First argument receive a context.Context object that will be canceled when shutting down will exceed // configured timeout. Shutdown(ctx context.Context) } // ExecutorProvider is responsible for managing the lifetime of executors, acquiring resources, // retrieving executor metadata, etc. type ExecutorProvider interface { // CanCreate returns whether the executor provider has the necessary data to create an executor. CanCreate() bool // Create creates a new executor. No resource allocation happens. Create() Executor // Acquire acquires the necessary resources for the executor to run, e.g. finds a virtual machine. Acquire(config *RunnerConfig) (ExecutorData, error) // Release releases any resources locked by Acquire. Release(config *RunnerConfig, data ExecutorData) // GetFeatures returns metadata about the features the executor supports, e.g. variables, services, shell. GetFeatures(features *FeaturesInfo) error // GetConfigInfo extracts metadata about the config the executor is using, e.g. GPUs. GetConfigInfo(input *RunnerConfig, output *ConfigInfo) // GetDefaultShell returns the name of the default shell for the executor. GetDefaultShell() string } // BuildError represents an error during build execution, not related to // the job script, e.g. failed to create container, establish ssh connection. type BuildError struct { Inner error FailureReason JobFailureReason ExitCode int } // Error implements the error interface. func (b *BuildError) Error() string { if b.Inner == nil { return "error" } return b.Inner.Error() } func (b *BuildError) Is(err error) bool { buildErr, ok := err.(*BuildError) if !ok { return false } return buildErr.FailureReason == b.FailureReason } func (b *BuildError) Unwrap() error { return b.Inner } func (b *BuildError) WithFailureReason(reason JobFailureReason) *BuildError { b.FailureReason = reason return b } // MakeBuildError returns an new instance of BuildError. func MakeBuildError(format string, args ...interface{}) *BuildError { return &BuildError{ Inner: fmt.Errorf(format, args...), } } var executorProviders map[string]ExecutorProvider func validateExecutorProvider(provider ExecutorProvider) error { if provider.GetDefaultShell() == "" { return errors.New("default shell not implemented") } if !provider.CanCreate() { return errors.New("cannot create executor") } if err := provider.GetFeatures(&FeaturesInfo{}); err != nil { return fmt.Errorf("cannot get features: %w", err) } return nil } // RegisterExecutorProvider maps an ExecutorProvider to an executor name, i.e. registers it. func RegisterExecutorProvider(executor string, provider ExecutorProvider) { logrus.Debugln("Registering", executor, "executor...") if err := validateExecutorProvider(provider); err != nil { panic("Executor cannot be registered: " + err.Error()) } if executorProviders == nil { executorProviders = make(map[string]ExecutorProvider) } if _, ok := executorProviders[executor]; ok { panic("Executor already exist: " + executor) } executorProviders[executor] = provider } // GetExecutorProvider returns an ExecutorProvider by name from the registered ones. func GetExecutorProvider(executor string) ExecutorProvider { if executorProviders == nil { return nil } provider := executorProviders[executor] return provider } // GetExecutorNames returns a list of all registered executor names. func GetExecutorNames() []string { var names []string for name := range executorProviders { names = append(names, name) } return names } // GetExecutorProviders returns a list of all registered executor providers. func GetExecutorProviders() []ExecutorProvider { var providers []ExecutorProvider for _, executorProvider := range executorProviders { providers = append(providers, executorProvider) } return providers } func NewExecutor(executor string) Executor { provider := GetExecutorProvider(executor) if provider != nil { return provider.Create() } return nil }