From 05f8a8ebef99c02dd3628422575543dc633860c6 Mon Sep 17 00:00:00 2001
From: Yvonnick Esnault <yvonnick.esnault@corp.ovh.com>
Date: Mon, 4 May 2020 08:51:18 +0200
Subject: [PATCH] feat(hatchery): region prerequisite (#5151)

Signed-off-by: Yvonnick Esnault <yvonnick.esnault@corp.ovh.com>
---
 .../docs/concepts/requirement/_index.md       |  2 ++
 .../requirement/requirement_region.md         | 19 ++++++++++
 engine/api/workermodel/registration.go        | 16 +++++----
 engine/hatchery/local/local.go                | 31 ++++------------
 engine/hatchery/marathon/helper_test.go       |  6 ++--
 engine/hatchery/marathon/marathon.go          |  3 +-
 engine/service/types.go                       | 12 +++----
 engine/worker/internal/requirement.go         |  6 ++++
 sdk/exportentities/pipeline.go                |  7 ++++
 sdk/exportentities/pipeline_test.go           |  8 ++++-
 sdk/hatchery/hatchery.go                      | 35 ++++++++++++-------
 sdk/hatchery/pool.go                          | 26 +++++++-------
 sdk/requirement.go                            | 11 +++---
 tests/06_sc_workflow_with_marathon.yml        |  1 +
 tests/fixtures/ITSCWRKFLW17/pipeline.yml      |  1 +
 tests/test.sh                                 |  4 +++
 ui/src/app/shared/action/action.component.ts  | 11 +++++-
 .../form/requirements.form.component.ts       |  8 +++--
 .../requirements/form/requirements.form.html  |  4 +--
 .../action/form/action.form.component.ts      | 11 +++++-
 ui/src/assets/i18n/en.json                    |  1 +
 ui/src/assets/i18n/fr.json                    |  3 +-
 22 files changed, 146 insertions(+), 80 deletions(-)
 create mode 100644 docs/content/docs/concepts/requirement/requirement_region.md

diff --git a/docs/content/docs/concepts/requirement/_index.md b/docs/content/docs/concepts/requirement/_index.md
index 9af2efb1a7..7eca529647 100644
--- a/docs/content/docs/concepts/requirement/_index.md
+++ b/docs/content/docs/concepts/requirement/_index.md
@@ -15,6 +15,7 @@ Requirement types:
 - [Service]({{< relref "/docs/concepts/requirement/requirement_service.md" >}})
 - [Memory]({{< relref "/docs/concepts/requirement/requirement_memory.md" >}})
 - [OS & Architecture]({{< relref "/docs/concepts/requirement/requirement_os_arch.md" >}})
+- [Region]({{< relref "/docs/concepts/requirement/requirement_region.md" >}})
 
 A [Job]({{< relref "/docs/concepts/job.md" >}}) will be executed by a **worker**.
 
@@ -26,3 +27,4 @@ You can set as many requirements as you want, following these rules:
 - Only one hostname can be set as requirement
 - Only one OS & Architecture requirement can be set at a time
 - Memory and Services requirements are available only on Docker models
+- Only one region can be set as requirement
diff --git a/docs/content/docs/concepts/requirement/requirement_region.md b/docs/content/docs/concepts/requirement/requirement_region.md
new file mode 100644
index 0000000000..6b0776f89a
--- /dev/null
+++ b/docs/content/docs/concepts/requirement/requirement_region.md
@@ -0,0 +1,19 @@
+---
+title: "Region Requirement"
+weight: 4
+---
+
+The `Region` prerequisite allows you to require a worker to have access to a specific region.
+
+A `Region` can be configured on each hatchery. With a free text as `myregion` in hatchery configuration, 
+user can set a prerequisite 'region' with value `myregion` on CDS Job.
+
+Example of job configuration:
+```
+jobs:
+- job: build
+  requirements:
+  - region: myregion
+  steps:
+  ...
+```
diff --git a/engine/api/workermodel/registration.go b/engine/api/workermodel/registration.go
index 7787f5a041..b81ef555af 100644
--- a/engine/api/workermodel/registration.go
+++ b/engine/api/workermodel/registration.go
@@ -2,7 +2,6 @@ package workermodel
 
 import (
 	"context"
-	"errors"
 	"strconv"
 	"strings"
 	"time"
@@ -24,9 +23,7 @@ const (
 // setting flag need_registration to true in DB.
 func ComputeRegistrationNeeds(db gorp.SqlExecutor, allBinaryReqs sdk.RequirementList, reqs sdk.RequirementList) error {
 	log.Debug("ComputeRegistrationNeeds>")
-	var nbModelReq int
-	var nbOSArchReq int
-	var nbHostnameReq int
+	var nbModelReq, nbOSArchReq, nbHostnameReq, nbRegionReq int
 
 	for _, r := range reqs {
 		switch r.Type {
@@ -47,17 +44,22 @@ func ComputeRegistrationNeeds(db gorp.SqlExecutor, allBinaryReqs sdk.Requirement
 			nbModelReq++
 		case sdk.HostnameRequirement:
 			nbHostnameReq++
+		case sdk.RegionRequirement:
+			nbRegionReq++
 		}
 	}
 
 	if nbOSArchReq > 1 {
-		return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid os-architecture requirement usage"))
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid os-architecture requirement usage")
 	}
 	if nbModelReq > 1 {
-		return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid model requirement usage"))
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid model requirement usage")
 	}
 	if nbHostnameReq > 1 {
-		return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid hostname requirement usage"))
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid hostname requirement usage")
+	}
+	if nbRegionReq > 1 {
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid region requirement usage")
 	}
 
 	return nil
diff --git a/engine/hatchery/local/local.go b/engine/hatchery/local/local.go
index d4d1db9b4f..42dff52035 100644
--- a/engine/hatchery/local/local.go
+++ b/engine/hatchery/local/local.go
@@ -178,31 +178,6 @@ func (h *HatcheryLocal) getWorkerBinaryName() string {
 	return workerName
 }
 
-// checkCapabilities checks all requirements, foreach type binary, check if binary is on current host
-// returns an error "Exit status X" if current host misses one requirement
-func (h *HatcheryLocal) checkCapabilities(req []sdk.Requirement) ([]sdk.Requirement, error) {
-	var tmp map[string]sdk.Requirement
-
-	tmp = make(map[string]sdk.Requirement)
-	for _, r := range req {
-		ok, err := h.checkRequirement(r)
-		if err != nil {
-			return nil, err
-		}
-
-		if ok {
-			tmp[r.Name] = r
-		}
-	}
-
-	capa := make([]sdk.Requirement, 0, len(tmp))
-	for _, r := range tmp {
-		capa = append(capa, r)
-	}
-
-	return capa, nil
-}
-
 //Configuration returns Hatchery CommonConfiguration
 func (h *HatcheryLocal) Configuration() service.HatcheryCommonConfiguration {
 	return h.Config.HatcheryCommonConfiguration
@@ -350,6 +325,12 @@ func (h *HatcheryLocal) checkRequirement(r sdk.Requirement) (bool, error) {
 		return true, nil
 	case sdk.PluginRequirement:
 		return true, nil
+	case sdk.RegionRequirement:
+		if r.Value != h.Configuration().Provision.Region {
+			log.Debug("checkRequirement> job with region requirement: cannot spawn. hatchery-region:%s prerequisite:%s", h.Configuration().Provision.Region, r.Value)
+			return false, nil
+		}
+		return true, nil
 	case sdk.OSArchRequirement:
 		osarch := strings.Split(r.Value, "/")
 		if len(osarch) != 2 {
diff --git a/engine/hatchery/marathon/helper_test.go b/engine/hatchery/marathon/helper_test.go
index 6fa2473e71..9cc10340b6 100644
--- a/engine/hatchery/marathon/helper_test.go
+++ b/engine/hatchery/marathon/helper_test.go
@@ -1,10 +1,12 @@
 package marathon
 
 import (
+	"time"
+
 	"github.com/gambol99/go-marathon"
-	"github.com/ovh/cds/sdk/cdsclient"
 	"gopkg.in/h2non/gock.v1"
-	"time"
+
+	"github.com/ovh/cds/sdk/cdsclient"
 )
 
 type marathonJDD struct {
diff --git a/engine/hatchery/marathon/marathon.go b/engine/hatchery/marathon/marathon.go
index a09ccb4024..c0c793973b 100644
--- a/engine/hatchery/marathon/marathon.go
+++ b/engine/hatchery/marathon/marathon.go
@@ -13,14 +13,13 @@ import (
 	"time"
 
 	"github.com/dgrijalva/jwt-go"
-	"github.com/ovh/cds/engine/service"
-
 	"github.com/gambol99/go-marathon"
 	"github.com/gorilla/mux"
 
 	"github.com/ovh/cds/engine/api"
 	"github.com/ovh/cds/engine/api/observability"
 	"github.com/ovh/cds/engine/api/services"
+	"github.com/ovh/cds/engine/service"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/cdsclient"
 	"github.com/ovh/cds/sdk/hatchery"
diff --git a/engine/service/types.go b/engine/service/types.go
index 2c9b16b44a..a905aab252 100644
--- a/engine/service/types.go
+++ b/engine/service/types.go
@@ -40,12 +40,12 @@ type HatcheryCommonConfiguration struct {
 		MaxHeartbeatFailures int    `toml:"maxHeartbeatFailures" default:"10" comment:"Maximum allowed consecutives failures on heatbeat routine" json:"maxHeartbeatFailures"`
 	} `toml:"api" json:"api"`
 	Provision struct {
-		Disabled                  bool `toml:"disabled" default:"false" comment:"Disabled provisioning. Format:true or false" json:"disabled"`
-		RatioService              *int `toml:"ratioService" default:"50" commented:"true" comment:"Percent reserved for spawning worker with service requirement" json:"ratioService,omitempty" mapstructure:"ratioService"`
-		MaxWorker                 int  `toml:"maxWorker" default:"10" comment:"Maximum allowed simultaneous workers" json:"maxWorker"`
-		MaxConcurrentProvisioning int  `toml:"maxConcurrentProvisioning" default:"10" comment:"Maximum allowed simultaneous workers provisioning" json:"maxConcurrentProvisioning"`
-		MaxConcurrentRegistering  int  `toml:"maxConcurrentRegistering" default:"2" comment:"Maximum allowed simultaneous workers registering. -1 to disable registering on this hatchery" json:"maxConcurrentRegistering"`
-		RegisterFrequency         int  `toml:"registerFrequency" default:"60" comment:"Check if some worker model have to be registered each n Seconds" json:"registerFrequency"`
+		RatioService              *int   `toml:"ratioService" default:"50" commented:"true" comment:"Percent reserved for spawning worker with service requirement" json:"ratioService,omitempty" mapstructure:"ratioService"`
+		MaxWorker                 int    `toml:"maxWorker" default:"10" comment:"Maximum allowed simultaneous workers" json:"maxWorker"`
+		MaxConcurrentProvisioning int    `toml:"maxConcurrentProvisioning" default:"10" comment:"Maximum allowed simultaneous workers provisioning" json:"maxConcurrentProvisioning"`
+		MaxConcurrentRegistering  int    `toml:"maxConcurrentRegistering" default:"2" comment:"Maximum allowed simultaneous workers registering. -1 to disable registering on this hatchery" json:"maxConcurrentRegistering"`
+		RegisterFrequency         int    `toml:"registerFrequency" default:"60" comment:"Check if some worker model have to be registered each n Seconds" json:"registerFrequency"`
+		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"`
 		WorkerLogsOptions         struct {
 			Graylog struct {
 				Host       string `toml:"host" comment:"Example: thot.ovh.com" json:"host"`
diff --git a/engine/worker/internal/requirement.go b/engine/worker/internal/requirement.go
index e3bbff1c20..3cacb004e2 100644
--- a/engine/worker/internal/requirement.go
+++ b/engine/worker/internal/requirement.go
@@ -26,6 +26,7 @@ var requirementCheckFuncs = map[string]func(w *CurrentWorker, r sdk.Requirement)
 	sdk.MemoryRequirement:        checkMemoryRequirement,
 	sdk.VolumeRequirement:        checkVolumeRequirement,
 	sdk.OSArchRequirement:        checkOSArchRequirement,
+	sdk.RegionRequirement:        checkRegionRequirement,
 }
 
 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) {
 	return osarch[0] == strings.ToLower(sdk.GOOS) && osarch[1] == strings.ToLower(sdk.GOARCH), nil
 }
 
+// region is checked by hatchery only
+func checkRegionRequirement(w *CurrentWorker, r sdk.Requirement) (bool, error) {
+	return true, nil
+}
+
 // checkPluginDeployment returns true if current job:
 //  - is not linked to a deployment integration
 //  - is linked to a deployement integration, plugin well downloaded (in this func) and
diff --git a/sdk/exportentities/pipeline.go b/sdk/exportentities/pipeline.go
index f620e23926..85bd538e94 100644
--- a/sdk/exportentities/pipeline.go
+++ b/sdk/exportentities/pipeline.go
@@ -60,6 +60,7 @@ type Requirement struct {
 	Service           ServiceRequirement `json:"service,omitempty" yaml:"service,omitempty"`
 	Memory            string             `json:"memory,omitempty" yaml:"memory,omitempty"`
 	OSArchRequirement string             `json:"os-architecture,omitempty" yaml:"os-architecture,omitempty"`
+	RegionRequirement string             `json:"region,omitempty" yaml:"region,omitempty"`
 }
 
 // ServiceRequirement represents an exported sdk.Requirement of type ServiceRequirement
@@ -151,6 +152,8 @@ func newRequirements(req []sdk.Requirement) []Requirement {
 			res = append(res, Requirement{Service: ServiceRequirement{Name: r.Name, Value: r.Value}})
 		case sdk.OSArchRequirement:
 			res = append(res, Requirement{OSArchRequirement: r.Value})
+		case sdk.RegionRequirement:
+			res = append(res, Requirement{RegionRequirement: r.Value})
 		case sdk.MemoryRequirement:
 			res = append(res, Requirement{Memory: r.Value})
 		}
@@ -222,6 +225,10 @@ func computeJobRequirements(req []Requirement) []sdk.Requirement {
 			name = r.OSArchRequirement
 			val = r.OSArchRequirement
 			tpe = sdk.OSArchRequirement
+		} else if r.RegionRequirement != "" {
+			name = "region"
+			val = r.RegionRequirement
+			tpe = sdk.RegionRequirement
 		} else if r.Plugin != "" {
 			name = r.Plugin
 			val = r.Plugin
diff --git a/sdk/exportentities/pipeline_test.go b/sdk/exportentities/pipeline_test.go
index 6d81750762..955af45190 100644
--- a/sdk/exportentities/pipeline_test.go
+++ b/sdk/exportentities/pipeline_test.go
@@ -41,6 +41,11 @@ var (
 										Type:  sdk.OSArchRequirement,
 										Value: "freebsd/amd64",
 									},
+									{
+										Name:  sdk.RegionRequirement,
+										Type:  sdk.RegionRequirement,
+										Value: "graxyz",
+									},
 								},
 								Actions: []sdk.Action{
 									{
@@ -542,6 +547,7 @@ jobs:
   requirements:
   - binary: git
   - os-archicture: freebsd/amd64
+  - region: graxyz
   steps:
   - gitClone:
       branch: '{{.git.branch}}'
@@ -568,7 +574,7 @@ jobs:
 	test.NoError(t, err)
 
 	assert.Len(t, p.Stages[0].Jobs[0].Action.Actions, 3)
-	assert.Len(t, p.Stages[0].Jobs[0].Action.Requirements, 2)
+	assert.Len(t, p.Stages[0].Jobs[0].Action.Requirements, 3)
 	assert.Equal(t, sdk.GitCloneAction, p.Stages[0].Jobs[0].Action.Actions[0].Name)
 	assert.Equal(t, sdk.ArtifactUpload, p.Stages[0].Jobs[0].Action.Actions[1].Name)
 	assert.Equal(t, sdk.ServeStaticFiles, p.Stages[0].Jobs[0].Action.Actions[2].Name)
diff --git a/sdk/hatchery/hatchery.go b/sdk/hatchery/hatchery.go
index 0d085e4403..9d45ddfab3 100644
--- a/sdk/hatchery/hatchery.go
+++ b/sdk/hatchery/hatchery.go
@@ -278,11 +278,17 @@ func canRunJob(ctx context.Context, h Interface, j workerStarterRequest) bool {
 			return false
 		}
 
+		if r.Type == sdk.RegionRequirement && r.Value != h.Configuration().Provision.Region {
+			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)
+			return false
+		}
+
 		// Skip others requirement as we can't check it
 		if r.Type == sdk.PluginRequirement || r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
-			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)
+			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)
 			continue
 		}
+
 	}
 	return h.CanSpawn(ctx, nil, j.id, j.requirements)
 }
@@ -293,18 +299,18 @@ const MemoryRegisterContainer int64 = 128
 
 func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStarterRequest, model *sdk.Model) bool {
 	if model.Type != h.ModelType() {
-		log.Debug("canRunJob> model %s type:%s current hatchery modelType: %s", model.Name, model.Type, h.ModelType())
+		log.Debug("canRunJobWithModel> model %s type:%s current hatchery modelType: %s", model.Name, model.Type, h.ModelType())
 		return false
 	}
 
 	// If the model needs registration, don't spawn for now
 	if h.NeedRegistration(ctx, model) {
-		log.Debug("canRunJob> model %s needs registration", model.Name)
+		log.Debug("canRunJobWithModel> model %s needs registration", model.Name)
 		return false
 	}
 
 	if model.NbSpawnErr > 5 {
-		log.Warning(ctx, "canRunJob> Too many errors on spawn with model %s, please check this worker model", model.Name)
+		log.Warning(ctx, "canRunJobWithModel> Too many errors on spawn with model %s, please check this worker model", model.Name)
 		return false
 	}
 
@@ -317,7 +323,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
 			}
 		}
 		if !checkGroup {
-			log.Debug("canRunJob> job %d - model %s attached to group %d can't run this job", j.id, model.Name, model.GroupID)
+			log.Debug("canRunJobWithModel> job %d - model %s attached to group %d can't run this job", j.id, model.Name, model.GroupID)
 			return false
 		}
 	}
@@ -333,7 +339,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
 	}
 
 	if model.IsDeprecated && !containsModelRequirement {
-		log.Debug("canRunJob> %d - job %d - Cannot launch this model because it is deprecated", j.timestamp, j.id)
+		log.Debug("canRunJobWithModel> %d - job %d - Cannot launch this model because it is deprecated", j.timestamp, j.id)
 		return false
 	}
 
@@ -348,30 +354,35 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
 			isSharedInfraModel := model.Group.Name == sdk.SharedInfraGroupName && modelName == model.Name
 			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.
 			if !isGroupModel && !isSharedInfraModel && !isSameName {
-				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)
+				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)
 				return false
 			}
 		}
 
 		// service and memory requirements are only supported by docker model
 		if model.Type != sdk.Docker && (r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement) {
-			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)
+			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)
 			return false
 		}
 
 		if r.Type == sdk.NetworkAccessRequirement && !sdk.CheckNetworkAccessRequirement(r) {
-			log.Debug("canRunJob> %d - job %d - network requirement failed: %v", j.timestamp, j.id, r.Value)
+			log.Debug("canRunJobWithModel> %d - job %d - network requirement failed: %v", j.timestamp, j.id, r.Value)
 			return false
 		}
 
 		// Skip other requirement as we can't check it
 		if r.Type == sdk.PluginRequirement || r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
-			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)
+			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)
 			continue
 		}
 
 		if r.Type == sdk.OSArchRequirement && model.RegisteredOS != "" && model.RegisteredArch != "" && r.Value != (model.RegisteredOS+"/"+model.RegisteredArch) {
-			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)
+			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)
+			return false
+		}
+
+		if r.Type == sdk.RegionRequirement && r.Value != h.Configuration().Provision.Region {
+			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)
 			return false
 		}
 
@@ -387,7 +398,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
 				}
 
 				if !found {
-					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)
+					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)
 					return false
 				}
 			}
diff --git a/sdk/hatchery/pool.go b/sdk/hatchery/pool.go
index 19f62dc52c..074195e062 100644
--- a/sdk/hatchery/pool.go
+++ b/sdk/hatchery/pool.go
@@ -14,7 +14,7 @@ import (
 )
 
 // WorkerPool returns all the worker owned by the hatchery h, registered or not on the CDS API
-func WorkerPool(ctx context.Context, h Interface, status ...string) ([]sdk.Worker, error) {
+func WorkerPool(ctx context.Context, h Interface, statusFilter ...string) ([]sdk.Worker, error) {
 	ctx = observability.ContextWithTag(ctx,
 		observability.TagServiceName, h.Name(),
 		observability.TagServiceType, h.Type(),
@@ -30,9 +30,6 @@ func WorkerPool(ctx context.Context, h Interface, status ...string) ([]sdk.Worke
 
 	// Then: get all workers in the orchestrator queue
 	startedWorkers := h.WorkersStarted(ctx)
-	if err != nil {
-		return nil, fmt.Errorf("unable to get started workers: %v", err)
-	}
 
 	// Make the union of the two slices
 	allWorkers := make([]sdk.Worker, 0, len(startedWorkers)+len(registeredWorkers))
@@ -106,17 +103,18 @@ func WorkerPool(ctx context.Context, h Interface, status ...string) ([]sdk.Worke
 	}
 	stats.Record(ctx, measures...)
 
-	// Filter by status
+	// no filter on status, returns the workers list as is.
+	if len(statusFilter) == 0 {
+		return allWorkers, nil
+	}
+
+	// return workers list filtered by status
 	res := make([]sdk.Worker, 0, len(allWorkers))
-	if len(status) == 0 {
-		res = allWorkers
-	} else {
-		for _, w := range allWorkers {
-			for _, s := range status {
-				if s == w.Status {
-					res = append(res, w)
-					break
-				}
+	for _, w := range allWorkers {
+		for _, s := range statusFilter {
+			if s == w.Status {
+				res = append(res, w)
+				break
 			}
 		}
 	}
diff --git a/sdk/requirement.go b/sdk/requirement.go
index 03e566e80e..a1bd972b15 100644
--- a/sdk/requirement.go
+++ b/sdk/requirement.go
@@ -25,6 +25,8 @@ const (
 	VolumeRequirement = "volume"
 	// OSArchRequirement checks the 'dist' of a worker eg {GOOS}/{GOARCH}
 	OSArchRequirement = "os-architecture"
+	// RegionRequirement lets a use to force a job running in a hatchery's region
+	RegionRequirement = "region"
 )
 
 // RequirementList is a list of requirement
@@ -89,14 +91,15 @@ var (
 	// AvailableRequirementsType List of all requirements
 	AvailableRequirementsType = []string{
 		BinaryRequirement,
-		NetworkAccessRequirement,
-		ModelRequirement,
 		HostnameRequirement,
+		MemoryRequirement,
+		ModelRequirement,
+		NetworkAccessRequirement,
+		OSArchRequirement,
 		PluginRequirement,
+		RegionRequirement,
 		ServiceRequirement,
-		MemoryRequirement,
 		VolumeRequirement,
-		OSArchRequirement,
 	}
 
 	// OSArchRequirementValues comes from go tool dist list
diff --git a/tests/06_sc_workflow_with_marathon.yml b/tests/06_sc_workflow_with_marathon.yml
index 7651a6cca7..b2a3830760 100644
--- a/tests/06_sc_workflow_with_marathon.yml
+++ b/tests/06_sc_workflow_with_marathon.yml
@@ -8,6 +8,7 @@ testcases:
   - script: '[ -f ./fixtures/ITSCWRKFLW17/workflow.yml ]' # check file exists
   - script: '[ -z "${CDS_MODEL_REQ}" ] && exit 1 || exit 0' # check that the env variable is set
   - script: '[ -z "${CDS_NETWORK_REQ}" ] && exit 1 || exit 0' # check that the env variable is set
+  - script: '[ -z "${CDS_REGION_REQ}" ] && exit 1 || exit 0' # check that the env variable is set
 
 - name: prepare_tests
   steps:
diff --git a/tests/fixtures/ITSCWRKFLW17/pipeline.yml b/tests/fixtures/ITSCWRKFLW17/pipeline.yml
index c2e36c2cbd..0a49a5f0a0 100644
--- a/tests/fixtures/ITSCWRKFLW17/pipeline.yml
+++ b/tests/fixtures/ITSCWRKFLW17/pipeline.yml
@@ -10,3 +10,4 @@ jobs:
   requirements:
   - model: "${CDS_MODEL_REQ}"
   - network: "${CDS_NETWORK_REQ}"
+  - region: "${CDS_REGION_REQ}"
diff --git a/tests/test.sh b/tests/test.sh
index d5e948c035..6120074517 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -44,6 +44,8 @@ INIT_TOKEN="${INIT_TOKEN:-}"
 CDS_MODEL_REQ="${CDS_MODEL_REQ:-buildpack-deps}"
 # If you want to run some tests with a specific network requirements, set CDS_NETWORK_REQ
 CDS_NETWORK_REQ="${CDS_NETWORK_REQ:-$CDS_API_URL}" 
+# If you want to run some tests with a specific region requirement, set CDS_REGION_REQ
+CDS_REGION_REQ="${CDS_REGION_REQ:-""}" 
 
 # The default values below fit to default minio installation.
 # Run "make minio_start" to start a minio docker container 
@@ -148,6 +150,7 @@ workflow_with_integration_tests() {
 workflow_with_third_parties() {
     if [ -z "$CDS_MODEL_REQ" ]; then echo "missing CDS_MODEL_REQ variable"; exit 1; fi
     if [ -z "$CDS_NETWORK_REQ" ]; then echo "missing CDS_NETWORK_REQ variable"; exit 1; fi
+    if [ -z "$CDS_REGION_REQ" ]; then echo "missing CDS_REGION_REQ variable"; exit 1; fi
     echo "Running Workflow with third parties:"
     for f in $(ls -1 06_*.yml); do
         CMD="${VENOM} run ${VENOM_OPTS} ${f} --var cdsctl=${CDSCTL} --var cdsctl.config=${CDSCTL_CONFIG}"
@@ -182,6 +185,7 @@ for target in $@; do
         workflow_with_third_parties)
             export CDS_MODEL_REQ
             export CDS_NETWORK_REQ
+            export CDS_REGION_REQ
             workflow_with_third_parties;;
         *) echo -e "${RED}Error: unknown target: $target${NOCOLOR}"
             usage
diff --git a/ui/src/app/shared/action/action.component.ts b/ui/src/app/shared/action/action.component.ts
index cc4c2f443f..ef23eeb45c 100644
--- a/ui/src/app/shared/action/action.component.ts
+++ b/ui/src/app/shared/action/action.component.ts
@@ -67,7 +67,7 @@ export class ActionComponent implements OnDestroy, OnInit {
     @Output() actionEvent = new EventEmitter<ActionEvent>();
 
     collapsed = true;
-    configRequirements: { disableModel?: boolean, disableHostname?: boolean } = {};
+    configRequirements: { disableModel?: boolean, disableHostname?: boolean, disableRegion?: boolean } = {};
     workerModels: Array<WorkerModel>;
 
     constructor(
@@ -143,6 +143,9 @@ export class ActionComponent implements OnDestroy, OnInit {
                 if (r.requirement.type === 'hostname') {
                     this.configRequirements.disableHostname = true;
                 }
+                if (r.requirement.type === 'region') {
+                    this.configRequirements.disableRegion = true;
+                }
                 break;
             case 'delete':
                 let indexDelete = this.editableAction.requirements.indexOf(r.requirement);
@@ -155,6 +158,9 @@ export class ActionComponent implements OnDestroy, OnInit {
                 if (r.requirement.type === 'hostname') {
                     this.configRequirements.disableHostname = false;
                 }
+                if (r.requirement.type === 'region') {
+                    this.configRequirements.disableRegion = false;
+                }
                 break;
         }
     }
@@ -177,6 +183,9 @@ export class ActionComponent implements OnDestroy, OnInit {
             if (req.type === 'hostname') {
                 this.configRequirements.disableHostname = true;
             }
+            if (req.type === 'region') {
+                this.configRequirements.disableRegion = true;
+            }
         });
     }
 
diff --git a/ui/src/app/shared/requirements/form/requirements.form.component.ts b/ui/src/app/shared/requirements/form/requirements.form.component.ts
index 5c71b9b996..89e815c317 100644
--- a/ui/src/app/shared/requirements/form/requirements.form.component.ts
+++ b/ui/src/app/shared/requirements/form/requirements.form.component.ts
@@ -44,7 +44,7 @@ export class RequirementsFormComponent implements OnInit {
 
 
     @Input() modal: SemanticModalComponent;
-    @Input() config: { disableModel?: boolean, disableHostname?: boolean };
+    @Input() config: { disableModel?: boolean, disableHostname?: boolean, disableRegion?: boolean };
 
     @Output() event = new EventEmitter<RequirementEvent>();
 
@@ -139,14 +139,18 @@ export class RequirementsFormComponent implements OnInit {
         this.popupText = '';
         let goodModel = this.newRequirement.type !== 'model' || !this.config.disableModel;
         let goodHostname = this.newRequirement.type !== 'hostname' || !this.config.disableHostname;
+        let goodRegion = this.newRequirement.type !== 'region' || !this.config.disableRegion;
         this.isFormValid = (form.valid === true && this.newRequirement.name !== '' && this.newRequirement.value !== '')
-            && goodModel && goodHostname;
+            && goodModel && goodHostname && goodRegion;
         if (!goodModel) {
             this.popupText = this._translate.instant('requirement_error_model');
         }
         if (!goodHostname) {
             this.popupText = this._translate.instant('requirement_error_hostname');
         }
+        if (!goodRegion) {
+            this.popupText = this._translate.instant('requirement_error_region');
+        }
     }
 
     selectType(): void {
diff --git a/ui/src/app/shared/requirements/form/requirements.form.html b/ui/src/app/shared/requirements/form/requirements.form.html
index 0490b8225e..9c5b23354c 100644
--- a/ui/src/app/shared/requirements/form/requirements.form.html
+++ b/ui/src/app/shared/requirements/form/requirements.form.html
@@ -64,14 +64,14 @@
             <div suiPopup
             [popupText]="popupText"
             popupPlacement="top"
-            *ngIf="(newRequirement.type === 'model' && config.disableModel) || (newRequirement.type === 'hostname' && config.disableHostname)">
+            *ngIf="(newRequirement.type === 'model' && config.disableModel) || (newRequirement.type === 'hostname' && config.disableHostname) || (newRequirement.type === 'region' && config.disableRegion)">
                 <button class="ui blue icon button"
                     [disabled]="!isFormValid"
                     (click)="onSubmitAddRequirement(formAddRequirement)">
                     <i class="plus icon"></i>
                 </button>
             </div>
-            <div *ngIf="(newRequirement.type !== 'model' || !config.disableModel) && (newRequirement.type !== 'hostname' || !config.disableHostname)">
+            <div *ngIf="(newRequirement.type !== 'model' || !config.disableModel) && (newRequirement.type !== 'hostname' || !config.disableHostname) && (newRequirement.type !== 'region' || !config.disableRegion)">
                 <button class="ui blue icon button"
                     [disabled]="!isFormValid"
                     (click)="onSubmitAddRequirement(formAddRequirement)">
diff --git a/ui/src/app/views/settings/action/form/action.form.component.ts b/ui/src/app/views/settings/action/form/action.form.component.ts
index 7d3a1e8cf3..4fa5aade3a 100644
--- a/ui/src/app/views/settings/action/form/action.form.component.ts
+++ b/ui/src/app/views/settings/action/form/action.form.component.ts
@@ -63,7 +63,7 @@ export class ActionFormComponent implements OnDestroy {
     steps: Array<Action> = new Array<Action>();
     actions: Array<Action> = new Array<Action>();
     collapsed = true;
-    configRequirements: { disableModel?: boolean, disableHostname?: boolean } = {};
+    configRequirements: { disableModel?: boolean, disableHostname?: boolean, disableRegion?: boolean } = {};
     stepFormExpended: boolean;
     workerModels: Array<WorkerModel>;
 
@@ -146,6 +146,9 @@ export class ActionFormComponent implements OnDestroy {
                 if (r.requirement.type === 'hostname') {
                     this.configRequirements.disableHostname = true;
                 }
+                if (r.requirement.type === 'region') {
+                    this.configRequirements.disableRegion = true;
+                }
                 break;
             case 'delete':
                 let indexDelete = this.action.requirements.indexOf(r.requirement);
@@ -158,6 +161,9 @@ export class ActionFormComponent implements OnDestroy {
                 if (r.requirement.type === 'hostname') {
                     this.configRequirements.disableHostname = false;
                 }
+                if (r.requirement.type === 'region') {
+                    this.configRequirements.disableRegion = false;
+                }
                 break;
         }
     }
@@ -180,6 +186,9 @@ export class ActionFormComponent implements OnDestroy {
             if (req.type === 'hostname') {
                 this.configRequirements.disableHostname = true;
             }
+            if (req.type === 'region') {
+                this.configRequirements.disableRegion = true;
+            }
         });
     }
 
diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json
index e58d8500c5..87e9c0b3e9 100644
--- a/ui/src/assets/i18n/en.json
+++ b/ui/src/assets/i18n/en.json
@@ -599,6 +599,7 @@
   "requirement_documentation": "Documentation about requirements",
   "requirement_error_model": "You can't have multiple requirements of type model",
   "requirement_error_hostname": "You can't have multiple requirements of type hostname",
+  "requirement_error_region": "You can't have multiple requirements of type region",
   "requirement_placeholder_name_os-architecture": "os/arch",
   "requirement_placeholder_name_service": "hostnameOfService",
   "requirement_placeholder_value_model": "Choose a worker model",
diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json
index 4c7db55fdc..b38cd51462 100644
--- a/ui/src/assets/i18n/fr.json
+++ b/ui/src/assets/i18n/fr.json
@@ -578,8 +578,9 @@
   "repoman_verif_msg_ok": "Le gestionnaire de dépôt est désormais connecté à CDS.",
   "requirement_add": "Ajouter un pré-requis",
   "requirement_documentation": "À propos des pré-requis",
-  "requirement_error_hostname": "Vous ne pouvez pas ajouter plusieurs pré-requis de type hostname",
   "requirement_error_model": "Vous ne pouvez pas ajouter plusieurs pré-requis de type modèle",
+  "requirement_error_hostname": "Vous ne pouvez pas ajouter plusieurs pré-requis de type hostname",
+  "requirement_error_region": "Vous ne pouvez pas ajouter plusieurs pré-requis de type region",
   "requirement_help_binary": "Pré-requis type 'binary': CDS choisira un worker possédant ce binaire dans son PATH.",
   "requirement_help_hostname": "Pré-requis type 'hostname': <ul><li>Ce job sera lancé par un worker possédant ce Hostname</li></ul>",
   "requirement_help_memory": "Pré-requis type 'memory': <ul><li>Si vous souhaitez 5Go, entrez la valeur suivante: <b>4096</b></li><li>Le prérequis memory est disponible uniquement avec les <a target=\"_blank\" href=\"https://ovh.github.io/cds/docs/concepts/worker-model/\">Worker Model</a> de type Docker</li></ul>",