File: executor.go

package info (click to toggle)
gitlab-ci-multi-runner 14.10.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 31,248 kB
  • sloc: sh: 1,694; makefile: 384; asm: 79; ruby: 68
file content (203 lines) | stat: -rw-r--r-- 6,212 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package common

import (
	"context"
	"errors"
	"fmt"

	"github.com/sirupsen/logrus"
)

// 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
	Trace   JobTrace
	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)
}

// 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
}

// MakeBuildError returns an new instance of BuildError.
func MakeBuildError(format string, args ...interface{}) error {
	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
}