Skip to content

Commit 05f8a8e

Browse files
authored
feat(hatchery): region prerequisite (#5151)
Signed-off-by: Yvonnick Esnault <[email protected]>
1 parent a5e7e0e commit 05f8a8e

File tree

22 files changed

+146
-80
lines changed

22 files changed

+146
-80
lines changed

docs/content/docs/concepts/requirement/_index.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Requirement types:
1515
- [Service]({{< relref "/docs/concepts/requirement/requirement_service.md" >}})
1616
- [Memory]({{< relref "/docs/concepts/requirement/requirement_memory.md" >}})
1717
- [OS & Architecture]({{< relref "/docs/concepts/requirement/requirement_os_arch.md" >}})
18+
- [Region]({{< relref "/docs/concepts/requirement/requirement_region.md" >}})
1819

1920
A [Job]({{< relref "/docs/concepts/job.md" >}}) will be executed by a **worker**.
2021

@@ -26,3 +27,4 @@ You can set as many requirements as you want, following these rules:
2627
- Only one hostname can be set as requirement
2728
- Only one OS & Architecture requirement can be set at a time
2829
- Memory and Services requirements are available only on Docker models
30+
- Only one region can be set as requirement
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: "Region Requirement"
3+
weight: 4
4+
---
5+
6+
The `Region` prerequisite allows you to require a worker to have access to a specific region.
7+
8+
A `Region` can be configured on each hatchery. With a free text as `myregion` in hatchery configuration,
9+
user can set a prerequisite 'region' with value `myregion` on CDS Job.
10+
11+
Example of job configuration:
12+
```
13+
jobs:
14+
- job: build
15+
requirements:
16+
- region: myregion
17+
steps:
18+
...
19+
```

engine/api/workermodel/registration.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package workermodel
22

33
import (
44
"context"
5-
"errors"
65
"strconv"
76
"strings"
87
"time"
@@ -24,9 +23,7 @@ const (
2423
// setting flag need_registration to true in DB.
2524
func ComputeRegistrationNeeds(db gorp.SqlExecutor, allBinaryReqs sdk.RequirementList, reqs sdk.RequirementList) error {
2625
log.Debug("ComputeRegistrationNeeds>")
27-
var nbModelReq int
28-
var nbOSArchReq int
29-
var nbHostnameReq int
26+
var nbModelReq, nbOSArchReq, nbHostnameReq, nbRegionReq int
3027

3128
for _, r := range reqs {
3229
switch r.Type {
@@ -47,17 +44,22 @@ func ComputeRegistrationNeeds(db gorp.SqlExecutor, allBinaryReqs sdk.Requirement
4744
nbModelReq++
4845
case sdk.HostnameRequirement:
4946
nbHostnameReq++
47+
case sdk.RegionRequirement:
48+
nbRegionReq++
5049
}
5150
}
5251

5352
if nbOSArchReq > 1 {
54-
return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid os-architecture requirement usage"))
53+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid os-architecture requirement usage")
5554
}
5655
if nbModelReq > 1 {
57-
return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid model requirement usage"))
56+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid model requirement usage")
5857
}
5958
if nbHostnameReq > 1 {
60-
return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid hostname requirement usage"))
59+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid hostname requirement usage")
60+
}
61+
if nbRegionReq > 1 {
62+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid region requirement usage")
6163
}
6264

6365
return nil

engine/hatchery/local/local.go

+6-25
Original file line numberDiff line numberDiff line change
@@ -178,31 +178,6 @@ func (h *HatcheryLocal) getWorkerBinaryName() string {
178178
return workerName
179179
}
180180

181-
// checkCapabilities checks all requirements, foreach type binary, check if binary is on current host
182-
// returns an error "Exit status X" if current host misses one requirement
183-
func (h *HatcheryLocal) checkCapabilities(req []sdk.Requirement) ([]sdk.Requirement, error) {
184-
var tmp map[string]sdk.Requirement
185-
186-
tmp = make(map[string]sdk.Requirement)
187-
for _, r := range req {
188-
ok, err := h.checkRequirement(r)
189-
if err != nil {
190-
return nil, err
191-
}
192-
193-
if ok {
194-
tmp[r.Name] = r
195-
}
196-
}
197-
198-
capa := make([]sdk.Requirement, 0, len(tmp))
199-
for _, r := range tmp {
200-
capa = append(capa, r)
201-
}
202-
203-
return capa, nil
204-
}
205-
206181
//Configuration returns Hatchery CommonConfiguration
207182
func (h *HatcheryLocal) Configuration() service.HatcheryCommonConfiguration {
208183
return h.Config.HatcheryCommonConfiguration
@@ -350,6 +325,12 @@ func (h *HatcheryLocal) checkRequirement(r sdk.Requirement) (bool, error) {
350325
return true, nil
351326
case sdk.PluginRequirement:
352327
return true, nil
328+
case sdk.RegionRequirement:
329+
if r.Value != h.Configuration().Provision.Region {
330+
log.Debug("checkRequirement> job with region requirement: cannot spawn. hatchery-region:%s prerequisite:%s", h.Configuration().Provision.Region, r.Value)
331+
return false, nil
332+
}
333+
return true, nil
353334
case sdk.OSArchRequirement:
354335
osarch := strings.Split(r.Value, "/")
355336
if len(osarch) != 2 {

engine/hatchery/marathon/helper_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package marathon
22

33
import (
4+
"time"
5+
46
"github.com/gambol99/go-marathon"
5-
"github.com/ovh/cds/sdk/cdsclient"
67
"gopkg.in/h2non/gock.v1"
7-
"time"
8+
9+
"github.com/ovh/cds/sdk/cdsclient"
810
)
911

1012
type marathonJDD struct {

engine/hatchery/marathon/marathon.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ import (
1313
"time"
1414

1515
"github.com/dgrijalva/jwt-go"
16-
"github.com/ovh/cds/engine/service"
17-
1816
"github.com/gambol99/go-marathon"
1917
"github.com/gorilla/mux"
2018

2119
"github.com/ovh/cds/engine/api"
2220
"github.com/ovh/cds/engine/api/observability"
2321
"github.com/ovh/cds/engine/api/services"
22+
"github.com/ovh/cds/engine/service"
2423
"github.com/ovh/cds/sdk"
2524
"github.com/ovh/cds/sdk/cdsclient"
2625
"github.com/ovh/cds/sdk/hatchery"

engine/service/types.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ type HatcheryCommonConfiguration struct {
4040
MaxHeartbeatFailures int `toml:"maxHeartbeatFailures" default:"10" comment:"Maximum allowed consecutives failures on heatbeat routine" json:"maxHeartbeatFailures"`
4141
} `toml:"api" json:"api"`
4242
Provision struct {
43-
Disabled bool `toml:"disabled" default:"false" comment:"Disabled provisioning. Format:true or false" json:"disabled"`
44-
RatioService *int `toml:"ratioService" default:"50" commented:"true" comment:"Percent reserved for spawning worker with service requirement" json:"ratioService,omitempty" mapstructure:"ratioService"`
45-
MaxWorker int `toml:"maxWorker" default:"10" comment:"Maximum allowed simultaneous workers" json:"maxWorker"`
46-
MaxConcurrentProvisioning int `toml:"maxConcurrentProvisioning" default:"10" comment:"Maximum allowed simultaneous workers provisioning" json:"maxConcurrentProvisioning"`
47-
MaxConcurrentRegistering int `toml:"maxConcurrentRegistering" default:"2" comment:"Maximum allowed simultaneous workers registering. -1 to disable registering on this hatchery" json:"maxConcurrentRegistering"`
48-
RegisterFrequency int `toml:"registerFrequency" default:"60" comment:"Check if some worker model have to be registered each n Seconds" json:"registerFrequency"`
43+
RatioService *int `toml:"ratioService" default:"50" commented:"true" comment:"Percent reserved for spawning worker with service requirement" json:"ratioService,omitempty" mapstructure:"ratioService"`
44+
MaxWorker int `toml:"maxWorker" default:"10" comment:"Maximum allowed simultaneous workers" json:"maxWorker"`
45+
MaxConcurrentProvisioning int `toml:"maxConcurrentProvisioning" default:"10" comment:"Maximum allowed simultaneous workers provisioning" json:"maxConcurrentProvisioning"`
46+
MaxConcurrentRegistering int `toml:"maxConcurrentRegistering" default:"2" comment:"Maximum allowed simultaneous workers registering. -1 to disable registering on this hatchery" json:"maxConcurrentRegistering"`
47+
RegisterFrequency int `toml:"registerFrequency" default:"60" comment:"Check if some worker model have to be registered each n Seconds" json:"registerFrequency"`
48+
Region string `toml:"region" default:"" comment:"region of this hatchery - optional. With a free text as 'myregion', user can set a prerequisite 'region' with value 'myregion' on CDS Job" json:"region"`
4949
WorkerLogsOptions struct {
5050
Graylog struct {
5151
Host string `toml:"host" comment:"Example: thot.ovh.com" json:"host"`

engine/worker/internal/requirement.go

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var requirementCheckFuncs = map[string]func(w *CurrentWorker, r sdk.Requirement)
2626
sdk.MemoryRequirement: checkMemoryRequirement,
2727
sdk.VolumeRequirement: checkVolumeRequirement,
2828
sdk.OSArchRequirement: checkOSArchRequirement,
29+
sdk.RegionRequirement: checkRegionRequirement,
2930
}
3031

3132
func checkRequirements(ctx context.Context, w *CurrentWorker, a *sdk.Action) (bool, []sdk.Requirement) {
@@ -209,6 +210,11 @@ func checkOSArchRequirement(w *CurrentWorker, r sdk.Requirement) (bool, error) {
209210
return osarch[0] == strings.ToLower(sdk.GOOS) && osarch[1] == strings.ToLower(sdk.GOARCH), nil
210211
}
211212

213+
// region is checked by hatchery only
214+
func checkRegionRequirement(w *CurrentWorker, r sdk.Requirement) (bool, error) {
215+
return true, nil
216+
}
217+
212218
// checkPluginDeployment returns true if current job:
213219
// - is not linked to a deployment integration
214220
// - is linked to a deployement integration, plugin well downloaded (in this func) and

sdk/exportentities/pipeline.go

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type Requirement struct {
6060
Service ServiceRequirement `json:"service,omitempty" yaml:"service,omitempty"`
6161
Memory string `json:"memory,omitempty" yaml:"memory,omitempty"`
6262
OSArchRequirement string `json:"os-architecture,omitempty" yaml:"os-architecture,omitempty"`
63+
RegionRequirement string `json:"region,omitempty" yaml:"region,omitempty"`
6364
}
6465

6566
// ServiceRequirement represents an exported sdk.Requirement of type ServiceRequirement
@@ -151,6 +152,8 @@ func newRequirements(req []sdk.Requirement) []Requirement {
151152
res = append(res, Requirement{Service: ServiceRequirement{Name: r.Name, Value: r.Value}})
152153
case sdk.OSArchRequirement:
153154
res = append(res, Requirement{OSArchRequirement: r.Value})
155+
case sdk.RegionRequirement:
156+
res = append(res, Requirement{RegionRequirement: r.Value})
154157
case sdk.MemoryRequirement:
155158
res = append(res, Requirement{Memory: r.Value})
156159
}
@@ -222,6 +225,10 @@ func computeJobRequirements(req []Requirement) []sdk.Requirement {
222225
name = r.OSArchRequirement
223226
val = r.OSArchRequirement
224227
tpe = sdk.OSArchRequirement
228+
} else if r.RegionRequirement != "" {
229+
name = "region"
230+
val = r.RegionRequirement
231+
tpe = sdk.RegionRequirement
225232
} else if r.Plugin != "" {
226233
name = r.Plugin
227234
val = r.Plugin

sdk/exportentities/pipeline_test.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ var (
4141
Type: sdk.OSArchRequirement,
4242
Value: "freebsd/amd64",
4343
},
44+
{
45+
Name: sdk.RegionRequirement,
46+
Type: sdk.RegionRequirement,
47+
Value: "graxyz",
48+
},
4449
},
4550
Actions: []sdk.Action{
4651
{
@@ -542,6 +547,7 @@ jobs:
542547
requirements:
543548
- binary: git
544549
- os-archicture: freebsd/amd64
550+
- region: graxyz
545551
steps:
546552
- gitClone:
547553
branch: '{{.git.branch}}'
@@ -568,7 +574,7 @@ jobs:
568574
test.NoError(t, err)
569575

570576
assert.Len(t, p.Stages[0].Jobs[0].Action.Actions, 3)
571-
assert.Len(t, p.Stages[0].Jobs[0].Action.Requirements, 2)
577+
assert.Len(t, p.Stages[0].Jobs[0].Action.Requirements, 3)
572578
assert.Equal(t, sdk.GitCloneAction, p.Stages[0].Jobs[0].Action.Actions[0].Name)
573579
assert.Equal(t, sdk.ArtifactUpload, p.Stages[0].Jobs[0].Action.Actions[1].Name)
574580
assert.Equal(t, sdk.ServeStaticFiles, p.Stages[0].Jobs[0].Action.Actions[2].Name)

sdk/hatchery/hatchery.go

+23-12
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,17 @@ func canRunJob(ctx context.Context, h Interface, j workerStarterRequest) bool {
278278
return false
279279
}
280280

281+
if r.Type == sdk.RegionRequirement && r.Value != h.Configuration().Provision.Region {
282+
log.Debug("canRunJob> %d - job %d - job with region requirement: cannot spawn. hatchery-region:%s prerequisite:%s", j.timestamp, j.id, h.Configuration().Provision.Region, r.Value)
283+
return false
284+
}
285+
281286
// Skip others requirement as we can't check it
282287
if r.Type == sdk.PluginRequirement || r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
283-
log.Debug("canRunJob> %d - job %d - job with service, plugin, network or memory requirement. Skip these check as we can't checkt it on hatchery routine", j.timestamp, j.id)
288+
log.Debug("canRunJob> %d - job %d - job with service, plugin, network or memory requirement. Skip these check as we can't check it on hatchery routine", j.timestamp, j.id)
284289
continue
285290
}
291+
286292
}
287293
return h.CanSpawn(ctx, nil, j.id, j.requirements)
288294
}
@@ -293,18 +299,18 @@ const MemoryRegisterContainer int64 = 128
293299

294300
func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStarterRequest, model *sdk.Model) bool {
295301
if model.Type != h.ModelType() {
296-
log.Debug("canRunJob> model %s type:%s current hatchery modelType: %s", model.Name, model.Type, h.ModelType())
302+
log.Debug("canRunJobWithModel> model %s type:%s current hatchery modelType: %s", model.Name, model.Type, h.ModelType())
297303
return false
298304
}
299305

300306
// If the model needs registration, don't spawn for now
301307
if h.NeedRegistration(ctx, model) {
302-
log.Debug("canRunJob> model %s needs registration", model.Name)
308+
log.Debug("canRunJobWithModel> model %s needs registration", model.Name)
303309
return false
304310
}
305311

306312
if model.NbSpawnErr > 5 {
307-
log.Warning(ctx, "canRunJob> Too many errors on spawn with model %s, please check this worker model", model.Name)
313+
log.Warning(ctx, "canRunJobWithModel> Too many errors on spawn with model %s, please check this worker model", model.Name)
308314
return false
309315
}
310316

@@ -317,7 +323,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
317323
}
318324
}
319325
if !checkGroup {
320-
log.Debug("canRunJob> job %d - model %s attached to group %d can't run this job", j.id, model.Name, model.GroupID)
326+
log.Debug("canRunJobWithModel> job %d - model %s attached to group %d can't run this job", j.id, model.Name, model.GroupID)
321327
return false
322328
}
323329
}
@@ -333,7 +339,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
333339
}
334340

335341
if model.IsDeprecated && !containsModelRequirement {
336-
log.Debug("canRunJob> %d - job %d - Cannot launch this model because it is deprecated", j.timestamp, j.id)
342+
log.Debug("canRunJobWithModel> %d - job %d - Cannot launch this model because it is deprecated", j.timestamp, j.id)
337343
return false
338344
}
339345

@@ -348,30 +354,35 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
348354
isSharedInfraModel := model.Group.Name == sdk.SharedInfraGroupName && modelName == model.Name
349355
isSameName := modelName == model.Name // for backward compatibility with runs, if only the name match we considered that the model can be used, keep this condition until the workflow runs were not migrated.
350356
if !isGroupModel && !isSharedInfraModel && !isSameName {
351-
log.Debug("canRunJob> %d - job %d - model requirement r.Value(%s) do not match model.Name(%s) and model.Group(%s)", j.timestamp, j.id, strings.Split(r.Value, " ")[0], model.Name, model.Group.Name)
357+
log.Debug("canRunJobWithModel> %d - job %d - model requirement r.Value(%s) do not match model.Name(%s) and model.Group(%s)", j.timestamp, j.id, strings.Split(r.Value, " ")[0], model.Name, model.Group.Name)
352358
return false
353359
}
354360
}
355361

356362
// service and memory requirements are only supported by docker model
357363
if model.Type != sdk.Docker && (r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement) {
358-
log.Debug("canRunJob> %d - job %d - job with service requirement or memory requirement: only for model docker. current model:%s", j.timestamp, j.id, model.Type)
364+
log.Debug("canRunJobWithModel> %d - job %d - job with service requirement or memory requirement: only for model docker. current model:%s", j.timestamp, j.id, model.Type)
359365
return false
360366
}
361367

362368
if r.Type == sdk.NetworkAccessRequirement && !sdk.CheckNetworkAccessRequirement(r) {
363-
log.Debug("canRunJob> %d - job %d - network requirement failed: %v", j.timestamp, j.id, r.Value)
369+
log.Debug("canRunJobWithModel> %d - job %d - network requirement failed: %v", j.timestamp, j.id, r.Value)
364370
return false
365371
}
366372

367373
// Skip other requirement as we can't check it
368374
if r.Type == sdk.PluginRequirement || r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
369-
log.Debug("canRunJob> %d - job %d - job with service, plugin, network or memory requirement. Skip these check as we can't check it on hatchery routine", j.timestamp, j.id)
375+
log.Debug("canRunJobWithModel> %d - job %d - job with service, plugin, network or memory requirement. Skip these check as we can't check it on hatchery routine", j.timestamp, j.id)
370376
continue
371377
}
372378

373379
if r.Type == sdk.OSArchRequirement && model.RegisteredOS != "" && model.RegisteredArch != "" && r.Value != (model.RegisteredOS+"/"+model.RegisteredArch) {
374-
log.Debug("canRunJob> %d - job %d - job with OSArch requirement: cannot spawn on this OSArch. current model: %s/%s", j.timestamp, j.id, model.RegisteredOS, model.RegisteredArch)
380+
log.Debug("canRunJobWithModel> %d - job %d - job with OSArch requirement: cannot spawn on this OSArch. current model: %s/%s", j.timestamp, j.id, model.RegisteredOS, model.RegisteredArch)
381+
return false
382+
}
383+
384+
if r.Type == sdk.RegionRequirement && r.Value != h.Configuration().Provision.Region {
385+
log.Debug("canRunJobWithModel> %d - job %d - job with region requirement: cannot spawn. hatchery-region:%s prerequisite:%s", j.timestamp, j.id, h.Configuration().Provision.Region, r.Value)
375386
return false
376387
}
377388

@@ -387,7 +398,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
387398
}
388399

389400
if !found {
390-
log.Debug("canRunJob> %d - job %d - model(%s) does not have binary %s(%s) for this job.", j.timestamp, j.id, model.Name, r.Name, r.Value)
401+
log.Debug("canRunJobWithModel> %d - job %d - model(%s) does not have binary %s(%s) for this job.", j.timestamp, j.id, model.Name, r.Name, r.Value)
391402
return false
392403
}
393404
}

0 commit comments

Comments
 (0)