Repository: ovh/cds
Pull request #4895 (2020-01-16 17:19:25)

fix(hatchery): check hostname prerequisite
Signed-off-by: Yvonnick Esnault <yvonnick.esnault@corp.ovh.com>


Patch:
diff --git a/cli/cdsctl/project.go b/cli/cdsctl/project.go
index 2dd3120b8..d71dd6ec4 100644
--- a/cli/cdsctl/project.go
+++ b/cli/cdsctl/project.go
@@ -158,9 +158,9 @@ func projectDeleteRun(v cli.Values) error {
 	projKey := v.GetString(_ProjectKey)
 	if v.GetBool("force") {
 		// Delete all workflow
-		ws, errW := client.WorkflowList(projKey)
-		if errW != nil && !sdk.ErrorIs(errW, sdk.ErrNoProject) {
-			return errW
+		ws, err := client.WorkflowList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, w := range ws {
 			if err := client.WorkflowDelete(projKey, w.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -169,9 +169,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all apps
-		apps, errA := client.ApplicationList(projKey)
-		if errA != nil && !sdk.ErrorIs(errA, sdk.ErrNoProject) {
-			return errA
+		apps, err := client.ApplicationList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, app := range apps {
 			if err := client.ApplicationDelete(projKey, app.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -180,9 +180,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all pipelines
-		pips, errP := client.PipelineList(projKey)
-		if errP != nil && !sdk.ErrorIs(errP, sdk.ErrNoProject) {
-			return errP
+		pips, err := client.PipelineList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, pip := range pips {
 			if err := client.PipelineDelete(projKey, pip.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -191,9 +191,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all environments
-		envs, errE := client.EnvironmentList(projKey)
-		if errE != nil && !sdk.ErrorIs(errE, sdk.ErrNoProject) {
-			return errE
+		envs, err := client.EnvironmentList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, env := range envs {
 			if err := client.EnvironmentDelete(projKey, env.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
diff --git a/engine/api/router_middleware_auth_permission.go b/engine/api/router_middleware_auth_permission.go
index 094b326fd..1a5be1490 100644
--- a/engine/api/router_middleware_auth_permission.go
+++ b/engine/api/router_middleware_auth_permission.go
@@ -5,6 +5,7 @@ import (
 	"strconv"
 
 	"github.com/ovh/cds/engine/api/authentication"
+	"github.com/ovh/cds/engine/api/project"
 	"github.com/ovh/cds/engine/api/cache"
 	"github.com/ovh/cds/engine/api/observability"
 	"github.com/ovh/cds/engine/api/workflow"
@@ -93,24 +94,29 @@ func (api *API) checkJobIDPermissions(ctx context.Context, jobID string, perm in
 	return nil
 }
 
-func (api *API) checkProjectPermissions(ctx context.Context, projectKey string, perm int, routeVars map[string]string) error {
+func (api *API) checkProjectPermissions(ctx context.Context, projectKey string, requiredPerm int, routeVars map[string]string) error {
 	ctx, end := observability.Span(ctx, "api.checkProjectPermissions")
 	defer end()
 
+	if _, err := project.Load(api.mustDB(), api.Cache, projectKey);  err != nil {
+		return err
+	}
+
 	perms, err := permission.LoadProjectMaxLevelPermission(ctx, api.mustDB(), []string{projectKey}, getAPIConsumer(ctx).GetGroupIDs())
 	if err != nil {
-		return sdk.NewErrorWithStack(err, sdk.WrapError(sdk.ErrForbidden, "not authorized for project %s", projectKey))
+		return sdk.WrapError(err,  "cannot get max project permissions for %s", projectKey)
 	}
 
-	maxLevelPermission := perms.Level(projectKey)
-	if maxLevelPermission < perm { // If the caller based on its group doesn have enough permission level
-		log.Debug("checkProjectPermissions> maxLevelPermission= %d ", maxLevelPermission)
+	callerPermission := perms.Level(projectKey)
+	// If the caller based on its group doesn't have enough permission level
+	if callerPermission < requiredPerm {
+		log.Debug("checkProjectPermissions> callerPermission=%d ", callerPermission)
 		// If it's about READ: we have to check if the user is a maintainer or an admin
-		if perm < sdk.PermissionReadExecute {
+		if requiredPerm == sdk.PermissionRead {
 			if !isMaintainer(ctx) {
 				// The caller doesn't enough permission level from its groups and is neither a maintainer nor an admin
 				log.Debug("checkProjectPermissions> %s(%s) is not authorized to %s", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
-				return sdk.WrapError(sdk.ErrForbidden, "not authorized for workflow %s", projectKey)
+				return sdk.WrapError(sdk.ErrNoProject, "not authorized for project %s", projectKey)
 			}
 			log.Debug("checkProjectPermissions> %s(%s) access granted to %s because is maintainer", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
 			observability.Current(ctx, observability.Tag(observability.TagPermission, "is_maintainer"))
@@ -126,9 +132,8 @@ func (api *API) checkProjectPermissions(ctx context.Context, projectKey string,
 		log.Debug("checkProjectPermissions> %s(%s) access granted to %s because is admin", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
 		observability.Current(ctx, observability.Tag(observability.TagPermission, "is_admin"))
 		return nil
-
 	}
-	log.Debug("checkWorkflowPermissions> %s(%s) access granted to %s because has permission (max permission = %d)", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey, maxLevelPermission)
+	log.Debug("checkWorkflowPermissions> %s(%s) access granted to %s because has permission (max permission = %d)", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey, callerPermission)
 	observability.Current(ctx, observability.Tag(observability.TagPermission, "is_granted"))
 	return nil
 }
diff --git a/engine/api/templates.go b/engine/api/templates.go
index 9ae75c68d..b2ef64434 100644
--- a/engine/api/templates.go
+++ b/engine/api/templates.go
@@ -164,7 +164,7 @@ func (api *API) putTemplateHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		templateSlug := vars["permTemplateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -398,7 +398,7 @@ func (api *API) postTemplateApplyHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -485,7 +485,7 @@ func (api *API) postTemplateBulkHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -637,7 +637,7 @@ func (api *API) getTemplateBulkHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -672,7 +672,7 @@ func (api *API) getTemplateInstancesHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -761,7 +761,7 @@ func (api *API) deleteTemplateInstanceHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -812,7 +812,7 @@ func (api *API) postTemplatePullHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		templateSlug := vars["permTemplateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -855,7 +855,7 @@ func (api *API) postTemplatePushHandler() service.Handler {
 		}
 
 		// check that the user is admin on the given template's group
-		grp, err := group.LoadByName(ctx, api.mustDB(), wt.Group.Name)
+		grp, err := group.LoadByName(ctx, api.mustDB(), wt.Group.Name, group.LoadOptions.WithMembers)
 		if err != nil {
 			return sdk.NewError(sdk.ErrWrongRequest, err)
 		}
@@ -924,7 +924,7 @@ func (api *API) getTemplateUsageHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
diff --git a/engine/api/workflow/dao_node_run_job.go b/engine/api/workflow/dao_node_run_job.go
index 21a894910..f4771730f 100644
--- a/engine/api/workflow/dao_node_run_job.go
+++ b/engine/api/workflow/dao_node_run_job.go
@@ -343,6 +343,13 @@ func DeleteNodeJobRuns(db gorp.SqlExecutor, nodeID int64) error {
 	return err
 }
 
+// DeleteNodeJobRun deletes the given workflow_node_run_job
+func DeleteNodeJobRun(db gorp.SqlExecutor, nodeRunJob int64) error {
+	query := `delete from workflow_node_run_job where id = $1`
+	_, err := db.Exec(query, nodeRunJob)
+	return err
+}
+
 //UpdateNodeJobRun updates a workflow_node_run_job
 func UpdateNodeJobRun(ctx context.Context, db gorp.SqlExecutor, j *sdk.WorkflowNodeJobRun) error {
 	var end func()
diff --git a/engine/api/workflow/heartbeat.go b/engine/api/workflow/heartbeat.go
index 1edf5f757..c71c02d1a 100644
--- a/engine/api/workflow/heartbeat.go
+++ b/engine/api/workflow/heartbeat.go
@@ -35,7 +35,7 @@ func manageDeadJob(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.S
 					continue
 				}
 
-				if err := DeleteNodeJobRuns(tx, deadJob.WorkflowNodeRunID); err != nil {
+				if err := DeleteNodeJobRun(tx, deadJob.ID); err != nil {
 					log.Error(ctx, "manageDeadJob> Cannot delete node run job %d : %v", deadJob.ID, err)
 					_ = tx.Rollback()
 					continue
@@ -48,7 +48,7 @@ func manageDeadJob(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.S
 				}
 			}
 		} else if sdk.StatusIsTerminated(deadJob.Status) {
-			if err := DeleteNodeJobRuns(tx, deadJob.WorkflowNodeRunID); err != nil {
+			if err := DeleteNodeJobRun(tx, deadJob.ID); err != nil {
 				log.Error(ctx, "manageDeadJob> Cannot delete node run job %d : %v", deadJob.ID, err)
 				_ = tx.Rollback()
 				continue
diff --git a/engine/hatchery/kubernetes/kubernetes.go b/engine/hatchery/kubernetes/kubernetes.go
index e5b648500..6cc90163d 100644
--- a/engine/hatchery/kubernetes/kubernetes.go
+++ b/engine/hatchery/kubernetes/kubernetes.go
@@ -228,6 +228,16 @@ func (h *HatcheryKubernetes) WorkerModelsEnabled() ([]sdk.Model, error) {
 // CanSpawn return wether or not hatchery can spawn model.
 // requirements are not supported
 func (h *HatcheryKubernetes) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	// Service and Hostname requirement are not supported
+	for _, r := range requirements {
+		if r.Type == sdk.ServiceRequirement {
+			log.Debug("CanSpawn> Job %d has a service requirement. Kubernetes can't spawn a worker for this job", jobID)
+			return false
+		} else if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Kubernetes can't spawn a worker for this job", jobID)
+			return false
+		}
+	}
 	return true
 }
 
diff --git a/engine/hatchery/local/local.go b/engine/hatchery/local/local.go
index 249f2162b..a09a731c4 100644
--- a/engine/hatchery/local/local.go
+++ b/engine/hatchery/local/local.go
@@ -62,9 +62,13 @@ func (h *HatcheryLocal) ApplyConfiguration(cfg interface{}) error {
 	genname := h.Configuration().Name
 	h.Common.Common.ServiceName = genname
 	h.Common.Common.ServiceType = services.TypeHatchery
+	var err error
+	h.Config.Basedir, err = filepath.Abs(h.Config.Basedir)
+	if err != nil {
+		return fmt.Errorf("unable to get basedir absolute path: %v", err)
+	}
 	h.HTTPURL = h.Config.URL
 	h.MaxHeartbeatFailures = h.Config.API.MaxHeartbeatFailures
-	var err error
 	h.Common.Common.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(h.Config.RSAPrivateKey))
 	if err != nil {
 		return fmt.Errorf("unable to parse RSA private Key: %v", err)
@@ -369,6 +373,12 @@ func (h *HatcheryLocal) checkRequirement(r sdk.Requirement) (bool, error) {
 		}
 		conn.Close()
 		return true, nil
+	case sdk.HostnameRequirement:
+		h, err := os.Hostname()
+		if err != nil {
+			return false, err
+		}
+		return h == r.Value, nil
 	default:
 		log.Debug("checkRequirement> %v don't work on this hatchery", r.Type)
 		return false, nil
diff --git a/engine/hatchery/marathon/marathon.go b/engine/hatchery/marathon/marathon.go
index e6b1931b6..39d087f8e 100644
--- a/engine/hatchery/marathon/marathon.go
+++ b/engine/hatchery/marathon/marathon.go
@@ -166,11 +166,14 @@ func (h *HatcheryMarathon) WorkerModelsEnabled() ([]sdk.Model, error) {
 // CanSpawn return wether or not hatchery can spawn model
 // requirements services are not supported
 func (h *HatcheryMarathon) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
-	//Service requirement are not supported
+	// Service and Hostname requirement are not supported
 	for _, r := range requirements {
 		if r.Type == sdk.ServiceRequirement {
 			log.Debug("CanSpawn> Job %d has a service requirement. Marathon can't spawn a worker for this job", jobID)
 			return false
+		} else if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Marathon can't spawn a worker for this job", jobID)
+			return false
 		}
 	}
 
@@ -309,7 +312,7 @@ func (h *HatcheryMarathon) SpawnWorker(ctx context.Context, spawnArgs hatchery.S
 			Type: "DOCKER",
 		},
 		Env:       &envsWm,
-		CPUs:      2,
+		CPUs:      h.Config.DefaultCPUs,
 		Instances: &instance,
 		Mem:       &mem,
 		Labels:    &h.marathonLabels,
diff --git a/engine/hatchery/marathon/types.go b/engine/hatchery/marathon/types.go
index ea50cd4f5..04289abc8 100644
--- a/engine/hatchery/marathon/types.go
+++ b/engine/hatchery/marathon/types.go
@@ -26,6 +26,9 @@ type HatcheryConfiguration struct {
 	// MarathonLabelsStr "marathon-labels"
 	MarathonLabels string `mapstructure:"labels" toml:"labels" default:"" commented:"false" comment:"Use this option if you want to add labels on workers spawned by this hatchery.\n Format: MarathonLabels = \"A_LABEL=value-of-label,B_LABEL=value-of-label-b\"" json:"labels"`
 
+	// DefaultCPUs
+	DefaultCPUs float64 `mapstructure:"defaultCPUs" toml:"defaultCPUs" default:"1" commented:"false" comment:"Worker default CPUs count" json:"defaultCPUs"`
+
 	// DefaultMemory Worker default memory
 	DefaultMemory int `mapstructure:"defaultMemory" toml:"defaultMemory" default:"1024" commented:"false" comment:"Worker default memory in Mo" json:"defaultMemory"`
 
diff --git a/engine/hatchery/openstack/openstack.go b/engine/hatchery/openstack/openstack.go
index 463d14091..167af86cf 100644
--- a/engine/hatchery/openstack/openstack.go
+++ b/engine/hatchery/openstack/openstack.go
@@ -166,7 +166,7 @@ func (h *HatcheryOpenstack) WorkerModelsEnabled() ([]sdk.Model, error) {
 // requirements are not supported
 func (h *HatcheryOpenstack) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
 	for _, r := range requirements {
-		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
+		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement || r.Type == sdk.HostnameRequirement {
 			return false
 		}
 	}
diff --git a/engine/hatchery/swarm/swarm.go b/engine/hatchery/swarm/swarm.go
index 9dedddc73..f94b1cfb3 100644
--- a/engine/hatchery/swarm/swarm.go
+++ b/engine/hatchery/swarm/swarm.go
@@ -454,6 +454,13 @@ const (
 // CanSpawn checks if the model can be spawned by this hatchery
 // it checks on every docker engine is one of the docker has availability
 func (h *HatcherySwarm) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	// Hostname requirement are not supported
+	for _, r := range requirements {
+		if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Swarm can't spawn a worker for this job", jobID)
+			return false
+		}
+	}
 	for dockerName, dockerClient := range h.dockerClients {
 		//List all containers to check if we can spawn a new one
 		cs, errList := h.getContainers(dockerClient, types.ContainerListOptions{All: true})
@@ -633,6 +640,7 @@ func (h *HatcherySwarm) listAwolWorkers(dockerClientName string, containers []ty
 
 		//If there isn't any worker registered on the API. Kill the container
 		if len(apiworkers) == 0 {
+			log.Debug("hatchery> swarm> listAwolWorkers> no apiworkers returned by api container %s will be deleted", c.Names[0])
 			oldContainers = append(oldContainers, c)
 			continue
 		}
@@ -652,6 +660,7 @@ func (h *HatcherySwarm) listAwolWorkers(dockerClientName string, containers []ty
 		}
 		//If the container doesn't match any worker : Kill it.
 		if !found {
+			log.Debug("hatchery> swarm> listAwolWorkers> container %s not found on apiworkers", c.Names[0])
 			oldContainers = append(oldContainers, c)
 		}
 	}
diff --git a/engine/hatchery/vsphere/vsphere.go b/engine/hatchery/vsphere/vsphere.go
index 9fd214d5a..474a0e677 100644
--- a/engine/hatchery/vsphere/vsphere.go
+++ b/engine/hatchery/vsphere/vsphere.go
@@ -118,7 +118,7 @@ func (h *HatcheryVSphere) CheckConfiguration(cfg interface{}) error {
 // requirements are not supported
 func (h *HatcheryVSphere) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
 	for _, r := range requirements {
-		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
+		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement || r.Type == sdk.HostnameRequirement {
 			return false
 		}
 	}
diff --git a/engine/worker/cmd_upload.go b/engine/worker/cmd_upload.go
index ce935f831..f7f7c94f7 100644
--- a/engine/worker/cmd_upload.go
+++ b/engine/worker/cmd_upload.go
@@ -88,7 +88,7 @@ func uploadCmd() func(cmd *cobra.Command, args []string) {
 			}
 
 			if resp.StatusCode >= 300 {
-				sdk.Exit("cannot artefact upload HTTP %d\n", resp.StatusCode)
+				sdk.Exit("cannot artifact upload HTTP %d\n", resp.StatusCode)
 			}
 		}
 	}
diff --git a/engine/worker/internal/action/builtin_artifact_upload.go b/engine/worker/internal/action/builtin_artifact_upload.go
index 342a67f45..0a5624da3 100644
--- a/engine/worker/internal/action/builtin_artifact_upload.go
+++ b/engine/worker/internal/action/builtin_artifact_upload.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"path"
 	"path/filepath"
 	"strings"
 	"sync"
@@ -42,7 +41,7 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 		abs = workdir.Name()
 	}
 
-	if !path.IsAbs(artifactPath) {
+	if !sdk.PathIsAbs(artifactPath) {
 		artifactPath = filepath.Join(abs, artifactPath)
 	}
 
diff --git a/engine/worker/internal/action/builtin_coverage.go b/engine/worker/internal/action/builtin_coverage.go
index 9580f36e7..9e97c5947 100644
--- a/engine/worker/internal/action/builtin_coverage.go
+++ b/engine/worker/internal/action/builtin_coverage.go
@@ -3,7 +3,6 @@ package action
 import (
 	"context"
 	"fmt"
-	"path"
 	"path/filepath"
 	"strconv"
 
@@ -64,7 +63,7 @@ func RunParseCoverageResultAction(ctx context.Context, wk workerruntime.Runtime,
 		abs = workdir.Name()
 	}
 
-	if !path.IsAbs(p) {
+	if !sdk.PathIsAbs(p) {
 		fpath = filepath.Join(abs, p)
 	} else {
 		fpath = p
diff --git a/engine/worker/internal/action/builtin_junit.go b/engine/worker/internal/action/builtin_junit.go
index 8a461401d..a16277537 100644
--- a/engine/worker/internal/action/builtin_junit.go
+++ b/engine/worker/internal/action/builtin_junit.go
@@ -5,7 +5,6 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
-	"path"
 	"path/filepath"
 
 	"github.com/spf13/afero"
@@ -43,7 +42,7 @@ func RunParseJunitTestResultAction(ctx context.Context, wk workerruntime.Runtime
 		abs = workdir.Name()
 	}
 
-	if !path.IsAbs(p) {
+	if !sdk.PathIsAbs(p) {
 		p = filepath.Join(abs, p)
 	}
 
diff --git a/engine/worker/internal/handler_upload.go b/engine/worker/internal/handler_upload.go
index e4c47435f..8db4e24e9 100644
--- a/engine/worker/internal/handler_upload.go
+++ b/engine/worker/internal/handler_upload.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"path"
 	"path/filepath"
 
 	"github.com/ovh/cds/engine/worker/internal/action"
@@ -31,7 +30,7 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		}
 
 		artifactPath := art.Name
-		if !path.IsAbs(artifactPath) {
+		if !sdk.PathIsAbs(artifactPath) {
 			artifactPath = filepath.Join(art.WorkingDirectory, art.Name)
 		}
 
diff --git a/engine/worker/internal/logger.go b/engine/worker/internal/logger.go
index c3692a3b8..af77575ad 100644
--- a/engine/worker/internal/logger.go
+++ b/engine/worker/internal/logger.go
@@ -76,7 +76,9 @@ func (wk *CurrentWorker) sendHTTPLog(ctx context.Context, jobID int64) {
 
 	for _, l := range logs {
 		log.Debug("LOG: %v", l.Val)
-		if err := wk.Client().QueueSendLogs(wk.currentJob.context, jobID, *l); err != nil {
+		// TODO: stop the worker a nice way,
+		// for the moment we are using context.Background and not the job context
+		if err := wk.Client().QueueSendLogs(context.Background(), jobID, *l); err != nil {
 			log.Error(ctx, "error: cannot send logs: %s", err)
 			continue
 		}
diff --git a/engine/worker/internal/run.go b/engine/worker/internal/run.go
index dceea93b9..c58976a6f 100644
--- a/engine/worker/internal/run.go
+++ b/engine/worker/internal/run.go
@@ -126,8 +126,11 @@ func (w *CurrentWorker) runJob(ctx context.Context, a *sdk.Action, jobID int64,
 			// Check if all newVariables are in currentJob.params
 			// variable can be add in w.currentJob.newVariables by worker command export
 			for _, newVariableFromHandler := range w.currentJob.newVariables {
-				if sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name) == nil {
+				p := sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name)
+				if p == nil {
 					w.currentJob.params = append(w.currentJob.params, newVariableFromHandler.ToParameter(""))
+				} else {
+					p.Value = newVariableFromHandler.Value
 				}
 			}
 
@@ -270,8 +273,11 @@ func (w *CurrentWorker) runSteps(ctx context.Context, steps []sdk.Action, a sdk.
 		// Check if all newVariables are in currentJob.params
 		// variable can be add in w.currentJob.newVariables by worker command export
 		for _, newVariableFromHandler := range w.currentJob.newVariables {
-			if sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name) == nil {
+			p := sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name)
+			if p == nil {
 				w.currentJob.params = append(w.currentJob.params, newVariableFromHandler.ToParameter(""))
+			} else {
+				p.Value = newVariableFromHandler.Value
 			}
 		}
 
diff --git a/engine/worker/internal/take.go b/engine/worker/internal/take.go
index 32ab5bd72..46e43b004 100644
--- a/engine/worker/internal/take.go
+++ b/engine/worker/internal/take.go
@@ -20,6 +20,8 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	t := ""
 	log.Info(ctx, "takeWorkflowJob> Job %d taken%s", job.ID, t)
 
+	ctx, cancel := context.WithCancel(ctx)
+
 	w.currentJob.context = workerruntime.SetJobID(ctx, job.ID)
 	w.currentJob.context = ctx
 
@@ -32,7 +34,6 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	start := time.Now()
 
 	//This goroutine try to get the job every 5 seconds, if it fails, it cancel the build.
-	ctx, cancel := context.WithCancel(ctx)
 	tick := time.NewTicker(5 * time.Second)
 	go func(cancel context.CancelFunc, jobID int64, tick *time.Ticker) {
 		var nbConnrefused int
diff --git a/sdk/cache.go b/sdk/cache.go
index 9afaac51e..d1be77ca3 100644
--- a/sdk/cache.go
+++ b/sdk/cache.go
@@ -7,7 +7,6 @@ import (
 	"io"
 	"net/url"
 	"os"
-	"path"
 	"path/filepath"
 	"strings"
 
@@ -58,7 +57,7 @@ func CreateTarFromPaths(fs afero.Fs, cwd string, paths []string, opts *TarOption
 		// ensure the src actually exists before trying to tar it
 
 		completePath := p
-		if !path.IsAbs(p) {
+		if !PathIsAbs(p) {
 			completePath = filepath.Join(cwd, p)
 		}
 
@@ -85,14 +84,13 @@ func CreateTarFromPaths(fs afero.Fs, cwd string, paths []string, opts *TarOption
 				header.Name = strings.TrimPrefix(strings.TrimPrefix(header.Name, opts.TrimDirName), string(filepath.Separator))
 			}
 			if fi.Mode()&os.ModeSymlink != 0 {
+
 				symlink, errEval := filepath.EvalSymlinks(file)
 				if errEval != nil {
 					return errEval
 				}
-				abs, errAbs := filepath.Abs(header.Name)
-				if errAbs != nil {
-					return errAbs
-				}
+
+				abs := filepath.Dir(filepath.Join(cwd, header.Name))
 
 				symlinkRel, errRel := filepath.Rel(abs, symlink)
 				if errRel != nil {
diff --git a/sdk/common.go b/sdk/common.go
index dfe3fc5da..02f9e31ef 100644
--- a/sdk/common.go
+++ b/sdk/common.go
@@ -11,8 +11,10 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path"
 	"reflect"
 	"regexp"
+	"runtime"
 	"strings"
 	"unicode"
 
@@ -269,3 +271,12 @@ func RemoveNotPrintableChar(in string) string {
 	}
 	return strings.Map(m, in)
 }
+
+var windowsPathRegex = regexp.MustCompile(`^[a-zA-Z]:\\[\\\S|*\S]?.*$`)
+
+func PathIsAbs(s string) bool {
+	if GOOS == "windows" || runtime.GOOS == "windows" {
+		return windowsPathRegex.MatchString(s)
+	}
+	return path.IsAbs(s)
+}
diff --git a/tools/smtpmock/.gitignore b/tools/smtpmock/.gitignore
new file mode 100644
index 000000000..6e021aa29
--- /dev/null
+++ b/tools/smtpmock/.gitignore
@@ -0,0 +1,1 @@
+server/server
\ No newline at end of file
diff --git a/ui/src/app/shared/table/data-table.html b/ui/src/app/shared/table/data-table.html
index 58d70c296..50de03ba4 100644
--- a/ui/src/app/shared/table/data-table.html
+++ b/ui/src/app/shared/table/data-table.html
@@ -76,8 +76,11 @@
                     </ng-container>
                     <ng-container *ngSwitchCase="'time-ago'"> {{c.selector | amTimeAgo}} </ng-container>
                     <ng-container *ngSwitchCase="'date'"> {{c.selector | amCalendar}} </ng-container>
-                    <button class="ui primary button small" *ngSwitchCase="'button'" (click)="c.selector.click()">{{c.selector.title
-                        | translate}}</button>
+                    <button class="ui button" [ngClass]="c.selector.class" *ngSwitchCase="'button'"
+                        (click)="c.selector.click()">
+                        {{c.selector.title | translate}}
+                        <i class="icon" [ngClass]="c.selector.icon" *ngIf="c.selector.icon"></i>
+                    </button>
                     <app-confirm-button *ngSwitchCase="'confirm-button'" [class]="'small'"
                         [color]="c.selector.color ? c.selector.color : 'primary'" (event)="c.selector.click()"
                         [title]="c.selector.title">
@@ -131,4 +134,4 @@
             </td>
         </tr>
     </tfoot>
-</table>
\ No newline at end of file
+</table>


Test patch:
diff --git a/engine/api/ascode_test.go b/engine/api/ascode_test.go
index 1ea17c0e8..a6486d857 100644
--- a/engine/api/ascode_test.go
+++ b/engine/api/ascode_test.go
@@ -124,6 +124,7 @@ func Test_getImportAsCodeHandler(t *testing.T) {
 	api, db, _, end := newTestAPI(t)
 	defer end()
 
+	p := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10))
 	u, pass := assets.InsertAdminUser(t, db)
 
 	_, _ = assets.InsertService(t, db, "Test_getImportAsCodeHandler", services.TypeRepositories)
@@ -164,7 +165,8 @@ func Test_getImportAsCodeHandler(t *testing.T) {
 	)
 
 	uri := api.Router.GetRoute("GET", api.getImportAsCodeHandler, map[string]string{
-		"uuid": UUID,
+		"permProjectKey": p.Key,
+		"uuid":           UUID,
 	})
 	req, err := http.NewRequest("GET", uri, nil)
 	test.NoError(t, err)
diff --git a/engine/hatchery/marathon/helper_test.go b/engine/hatchery/marathon/helper_test.go
index 627c79b48..6fa2473e7 100644
--- a/engine/hatchery/marathon/helper_test.go
+++ b/engine/hatchery/marathon/helper_test.go
@@ -28,6 +28,7 @@ func InitMarathonMarathonTest(opts marathonJDD) *HatcheryMarathon {
 	h.marathonClient, _ = marathon.NewClient(config)
 	h.Config.Provision.MaxConcurrentProvisioning = opts.MaxProvision
 	h.Config.Provision.MaxWorker = opts.MaxWorker
+	h.Config.DefaultCPUs = 1
 	if opts.Prefix != "" {
 		h.Config.MarathonIDPrefix = opts.Prefix
 	}
diff --git a/engine/hatchery/marathon/marathon_test.go b/engine/hatchery/marathon/marathon_test.go
index 9f42c4708..d963824b6 100644
--- a/engine/hatchery/marathon/marathon_test.go
+++ b/engine/hatchery/marathon/marathon_test.go
@@ -296,7 +296,7 @@ func TestSpawnWorkerTimeout(t *testing.T) {
 				assert.NoError(t, json.Unmarshal(bodyContent, &a))
 				assert.Equal(t, "DOCKER", a.Container.Type)
 				assert.Equal(t, "BRIDGE", a.Container.Docker.Network)
-				assert.Equal(t, float64(2), a.CPUs)
+				assert.Equal(t, float64(1), a.CPUs)
 				assert.Equal(t, 1, *a.Instances)
 				assert.Equal(t, "1", (*a.Env)["CDS_BOOKED_WORKFLOW_JOB_ID"])
 				assert.Equal(t, "GroupModel/fake", (*a.Env)["CDS_MODEL_PATH"])
diff --git a/sdk/common_test.go b/sdk/common_test.go
index 595b67a4a..acd62275e 100644
--- a/sdk/common_test.go
+++ b/sdk/common_test.go
@@ -32,3 +32,12 @@ func TestRemoveNotPrintableChar(t *testing.T) {
 		})
 	}
 }
+
+func TestPathIsAbs(t *testing.T) {
+	GOOS = "windows"
+	assert.True(t, PathIsAbs(`C:\Program Files (x86)\Foo`))
+	assert.False(t, PathIsAbs(`Program Files (x86)\Foo`))
+	GOOS = "linux"
+	assert.True(t, PathIsAbs(`/tmp`))
+	assert.False(t, PathIsAbs(`tmp`))
+}
diff --git a/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml b/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
index 9960a9485..d6fcd5273 100644
--- a/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
+++ b/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
@@ -15,6 +15,8 @@ jobs:
       - this a a line in the file, with a CDS variable of type password {{.cds.app.password}}
       - EOF
       - worker check-secret {{.cds.workspace}}/myFile
+    - script:
+      - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: EXIT
@@ -62,6 +64,8 @@ jobs:
       - EOF
       - worker tmpl {{.cds.workspace}}/myFile {{.cds.workspace}}/outputFile
       - cat {{.cds.workspace}}/outputFile
+    - script:
+        - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: UPLOAD
@@ -82,6 +86,8 @@ jobs:
       - set -ex
       - worker key install --env proj-ssh-04scworkflowrunworkercmd --file id_rsa
       - cat id_rsa
+    - script:
+        - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: ARTIFACTLIST
@@ -90,6 +96,7 @@ jobs:
     - script:
       - set -ex
       - worker artifacts
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -100,6 +107,7 @@ jobs:
       - set -ex
       - worker cache pull latest
       - cat fileCache
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -110,6 +118,7 @@ jobs:
       - set -ex
       - worker download
       - cat myFileUploaded
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -119,6 +128,7 @@ jobs:
     - script:
       - set -ex
       - echo "Export value {{.cds.build.foo}}"
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
diff --git a/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts b/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
index be6a764e6..7cb981e1c 100644
--- a/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
+++ b/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
@@ -1,10 +1,10 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core';
-import { Failure, TestCase, Tests } from '../../../../../../model/pipeline.model';
-
 import { ThemeStore } from 'app/service/services.module';
 import { Column, ColumnType, Filter } from 'app/shared/table/data-table.component';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
+import { Failure, TestCase, Tests } from '../../../../../../model/pipeline.model';
+
 
 @Component({
     selector: 'app-workflow-test-table',
@@ -46,7 +46,7 @@ export class WorkflowRunTestTableComponent implements OnInit {
                     tc.fullname = ts.name + ' / ' + tc.name;
                     if (!tc.errors && !tc.failures) {
                         tc.status = 'success';
-                    } else if ( (tc.errors && tc.errors.length > 0) || (tc.failures && tc.failures.length > 0)) {
+                    } else if ((tc.errors && tc.errors.length > 0) || (tc.failures && tc.failures.length > 0)) {
                         tc.status = 'failed';
                     } else {
                         tc.status = 'skipped';