From a90c66d4b79602f5381fd59b4d3ef21b24ffb545 Mon Sep 17 00:00:00 2001 From: Maksym Trofimenko Date: Sat, 19 Oct 2024 20:01:40 +0100 Subject: [PATCH 01/14] add checkConcurrency gocraft/work middleware WIP Signed-off-by: bupd Co-authored-by: Maksym Trofimenko --- src/jobservice/worker/cworker/c_worker.go | 28 ++++++++++++++++++-- src/pkg/retention/policy/rule/index/index.go | 14 ++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/jobservice/worker/cworker/c_worker.go b/src/jobservice/worker/cworker/c_worker.go index 130cd099ea9..15d036d322e 100644 --- a/src/jobservice/worker/cworker/c_worker.go +++ b/src/jobservice/worker/cworker/c_worker.go @@ -16,6 +16,7 @@ package cworker import ( "fmt" + "github.com/davecgh/go-spew/spew" "reflect" "sync" "time" @@ -66,7 +67,9 @@ type basicWorker struct { // workerContext ... // We did not use this context to pass context info so far, just a placeholder. -type workerContext struct{} +type workerContext struct { + client *work.Client +} // log the job func (rpc *workerContext) logJob(job *work.Job, next work.NextMiddlewareFunc) error { @@ -79,6 +82,23 @@ func (rpc *workerContext) logJob(job *work.Job, next work.NextMiddlewareFunc) er return next() } +// log the job +func (rpc *workerContext) checkConcurrency(job *work.Job, next work.NextMiddlewareFunc) error { + jobCopy := *job + + observations, err := rpc.client.WorkerObservations() + if err != nil { + return err + } + + for _, observation := range observations { + fmt.Println("o", observation.JobID) + } + spew.Dump(job.Args) + spew.Dump(jobCopy.Name, jobCopy.ID) + return next() +} + // NewWorker is constructor of worker func NewWorker(ctx *env.Context, namespace string, workerCount uint, redisPool *redis.Pool, ctl lcm.Controller) worker.Interface { wc := defaultWorkerCount @@ -146,9 +166,13 @@ func (w *basicWorker) Start() error { w.pool.Stop() }() + workCtx := workerContext{ + client: w.client, + } // Start the backend worker pool // Add middleware - w.pool.Middleware((*workerContext).logJob) + w.pool.Middleware(workCtx.logJob) + w.pool.Middleware(workCtx.checkConcurrency) // Non blocking call w.pool.Start() logger.Infof("Basic worker is started") diff --git a/src/pkg/retention/policy/rule/index/index.go b/src/pkg/retention/policy/rule/index/index.go index 1555aedd662..044ca41182f 100644 --- a/src/pkg/retention/policy/rule/index/index.go +++ b/src/pkg/retention/policy/rule/index/index.go @@ -163,6 +163,20 @@ func init() { }, }, }, daysps.New, daysps.Valid) + + // Register daysps + Register(&Metadata{ + TemplateID: daysps.TemplateID, + Action: "immutable", + Parameters: []*IndexedParam{ + { + Name: daysps.ParameterN, + Type: "int", + Unit: "days", + Required: true, + }, + }, + }, daysps.New, daysps.Valid) } // Register the rule evaluator with the corresponding rule template From 239595ef5900fc549ca60b5033373919348a9d32 Mon Sep 17 00:00:00 2001 From: Maksym Trofimenko Date: Sun, 17 Nov 2024 20:42:25 +0000 Subject: [PATCH 02/14] add jobs skipper WIP Signed-off-by: bupd Co-authored-by: Maksym Trofimenko --- src/controller/jobmonitor/monitor.go | 4 +- src/controller/replication/execution.go | 37 +++++++++++++++++++ src/controller/replication/flow/copy.go | 12 +++--- .../job/impl/replication/replication.go | 1 - src/jobservice/job/status.go | 2 + src/jobservice/worker/cworker/c_worker.go | 19 ---------- src/pkg/task/execution.go | 13 +++++++ 7 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/controller/jobmonitor/monitor.go b/src/controller/jobmonitor/monitor.go index 04ca93b553c..09ce1a02b95 100644 --- a/src/controller/jobmonitor/monitor.go +++ b/src/controller/jobmonitor/monitor.go @@ -92,13 +92,13 @@ func NewMonitorController() MonitorController { taskManager: task.NewManager(), queueManager: jm.NewQueueClient(), queueStatusManager: queuestatus.Mgr, - monitorClient: jobServiceMonitorClient, + monitorClient: JobServiceMonitorClient, jobServiceRedisClient: jm.JobServiceRedisClient, executionDAO: taskDao.NewExecutionDAO(), } } -func jobServiceMonitorClient() (jm.JobServiceMonitorClient, error) { +func JobServiceMonitorClient() (jm.JobServiceMonitorClient, error) { cfg, err := job.GlobalClient.GetJobServiceConfig() if err != nil { return nil, err diff --git a/src/controller/replication/execution.go b/src/controller/replication/execution.go index 3c92250946f..ee17a775639 100644 --- a/src/controller/replication/execution.go +++ b/src/controller/replication/execution.go @@ -16,7 +16,9 @@ package replication import ( "context" + "encoding/json" "fmt" + "github.com/goharbor/harbor/src/controller/jobmonitor" "time" "github.com/goharbor/harbor/src/controller/event/operator" @@ -109,10 +111,45 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, if op := operator.FromContext(ctx); op != "" { extra["operator"] = op } + + monitorClient, err := jobmonitor.JobServiceMonitorClient() + if err != nil { + return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessage("unable to get job monitor's client: %v", err) + } + + observations, err := monitorClient.WorkerObservations() + if err != nil { + return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessage("unable to get jobs observations: %v", err) + } id, err := c.execMgr.Create(ctx, job.ReplicationVendorType, policy.ID, trigger, extra) if err != nil { return 0, err } + + args := map[string]interface{}{} + + for _, o := range observations { + if o.JobName != job.ReplicationVendorType { + continue + } + if err = json.Unmarshal([]byte(o.ArgsJSON), &args); err != nil { + continue + } + policyID, ok := args["policy_id"].(float64) + if !ok { + continue + } + if int64(policyID) != policy.ID { + continue + } + + err = c.execMgr.MarkSkipped(ctx, id, "task skipped as a duplicate") + if err != nil { + return 0, err + } + return id, nil + } + // start the replication flow in background // as the process runs inside a goroutine, the transaction in the outer ctx // may be submitted already when the process starts, so create an new context diff --git a/src/controller/replication/flow/copy.go b/src/controller/replication/flow/copy.go index 7c6b823bb14..993536163aa 100644 --- a/src/controller/replication/flow/copy.go +++ b/src/controller/replication/flow/copy.go @@ -17,7 +17,6 @@ package flow import ( "context" "encoding/json" - repctlmodel "github.com/goharbor/harbor/src/controller/replication/model" "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/logger" @@ -92,7 +91,7 @@ func (c *copyFlow) Run(ctx context.Context) error { return err } - return c.createTasks(ctx, srcResources, dstResources, c.policy.Speed, c.policy.CopyByChunk) + return c.createTasks(ctx, srcResources, dstResources, c.policy) } func (c *copyFlow) isExecutionStopped(ctx context.Context) (bool, error) { @@ -103,7 +102,7 @@ func (c *copyFlow) isExecutionStopped(ctx context.Context) (bool, error) { return execution.Status == job.StoppedStatus.String(), nil } -func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources []*model.Resource, speed int32, copyByChunk bool) error { +func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources []*model.Resource, policy *repctlmodel.Policy) error { var taskCnt int defer func() { // if no task be created, mark execution done. @@ -139,14 +138,15 @@ func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources [ Parameters: map[string]interface{}{ "src_resource": string(src), "dst_resource": string(dest), - "speed": speed, - "copy_by_chunk": copyByChunk, + "speed": policy.Speed, + "copy_by_chunk": policy.CopyByChunk, + "policy_id": policy.ID, }, } if _, err = c.taskMgr.Create(ctx, c.executionID, job, map[string]interface{}{ "operation": "copy", - "resource_type": string(srcResource.Type), + "resource_type": srcResource.Type, "source_resource": getResourceName(srcResource), "destination_resource": getResourceName(dstResource), "references": getResourceReferences(dstResource)}); err != nil { diff --git a/src/jobservice/job/impl/replication/replication.go b/src/jobservice/job/impl/replication/replication.go index 453fa5e032b..dc0de101284 100644 --- a/src/jobservice/job/impl/replication/replication.go +++ b/src/jobservice/job/impl/replication/replication.go @@ -17,7 +17,6 @@ package replication import ( "encoding/json" "fmt" - "github.com/goharbor/harbor/src/controller/replication/transfer" // import chart transfer _ "github.com/goharbor/harbor/src/controller/replication/transfer/image" diff --git a/src/jobservice/job/status.go b/src/jobservice/job/status.go index 6e38785346a..014dda71831 100644 --- a/src/jobservice/job/status.go +++ b/src/jobservice/job/status.go @@ -29,6 +29,8 @@ const ( SuccessStatus Status = "Success" // ScheduledStatus : job status scheduled ScheduledStatus Status = "Scheduled" + // SkippedStatus : job status skipped + SkippedStatus Status = "Skipped" ) // Status of job diff --git a/src/jobservice/worker/cworker/c_worker.go b/src/jobservice/worker/cworker/c_worker.go index 15d036d322e..088e06431cd 100644 --- a/src/jobservice/worker/cworker/c_worker.go +++ b/src/jobservice/worker/cworker/c_worker.go @@ -16,7 +16,6 @@ package cworker import ( "fmt" - "github.com/davecgh/go-spew/spew" "reflect" "sync" "time" @@ -82,23 +81,6 @@ func (rpc *workerContext) logJob(job *work.Job, next work.NextMiddlewareFunc) er return next() } -// log the job -func (rpc *workerContext) checkConcurrency(job *work.Job, next work.NextMiddlewareFunc) error { - jobCopy := *job - - observations, err := rpc.client.WorkerObservations() - if err != nil { - return err - } - - for _, observation := range observations { - fmt.Println("o", observation.JobID) - } - spew.Dump(job.Args) - spew.Dump(jobCopy.Name, jobCopy.ID) - return next() -} - // NewWorker is constructor of worker func NewWorker(ctx *env.Context, namespace string, workerCount uint, redisPool *redis.Pool, ctl lcm.Controller) worker.Interface { wc := defaultWorkerCount @@ -172,7 +154,6 @@ func (w *basicWorker) Start() error { // Start the backend worker pool // Add middleware w.pool.Middleware(workCtx.logJob) - w.pool.Middleware(workCtx.checkConcurrency) // Non blocking call w.pool.Start() logger.Infof("Basic worker is started") diff --git a/src/pkg/task/execution.go b/src/pkg/task/execution.go index 31c993e36e2..2b7ff2922fc 100644 --- a/src/pkg/task/execution.go +++ b/src/pkg/task/execution.go @@ -49,6 +49,8 @@ type ExecutionManager interface { // In other cases, the execution status can be calculated from the referenced tasks automatically // and no need to update it explicitly MarkDone(ctx context.Context, id int64, message string) (err error) + // MarkSkipped marks the status of the specified execution as skipped. + MarkSkipped(ctx context.Context, id int64, message string) (err error) // MarkError marks the status of the specified execution as error. // It must be called to update the execution status when failed to create tasks. // In other cases, the execution status can be calculated from the referenced tasks automatically @@ -139,6 +141,17 @@ func (e *executionManager) UpdateExtraAttrs(ctx context.Context, id int64, extra return e.executionDAO.Update(ctx, execution, "ExtraAttrs", "UpdateTime") } +func (e *executionManager) MarkSkipped(ctx context.Context, id int64, message string) error { + now := time.Now() + return e.executionDAO.Update(ctx, &dao.Execution{ + ID: id, + Status: job.SkippedStatus.String(), + StatusMessage: message, + UpdateTime: now, + EndTime: now, + }, "Status", "StatusMessage", "UpdateTime", "EndTime") +} + func (e *executionManager) MarkDone(ctx context.Context, id int64, message string) error { now := time.Now() return e.executionDAO.Update(ctx, &dao.Execution{ From ad3881585b8554f587a3ada1bcfbe44d02eb3129 Mon Sep 17 00:00:00 2001 From: bupd Date: Sun, 22 Dec 2024 16:55:39 +0530 Subject: [PATCH 03/14] add skip If Running option in Replication policy * Adds Skip If Runnning to Replication Policy Signed-off-by: bupd --- src/controller/replication/execution.go | 60 ++++++++++++----------- src/controller/replication/flow/copy.go | 15 +++--- src/controller/replication/model/model.go | 3 ++ src/pkg/replication/model/model.go | 1 + src/server/v2.0/handler/replication.go | 9 ++++ 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/controller/replication/execution.go b/src/controller/replication/execution.go index ee17a775639..bee22caee6d 100644 --- a/src/controller/replication/execution.go +++ b/src/controller/replication/execution.go @@ -18,9 +18,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/goharbor/harbor/src/controller/jobmonitor" "time" + "github.com/gocraft/work" + "github.com/goharbor/harbor/src/controller/jobmonitor" + "github.com/goharbor/harbor/src/controller/event/operator" "github.com/goharbor/harbor/src/controller/replication/flow" replicationmodel "github.com/goharbor/harbor/src/controller/replication/model" @@ -112,42 +114,30 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, extra["operator"] = op } - monitorClient, err := jobmonitor.JobServiceMonitorClient() - if err != nil { - return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessage("unable to get job monitor's client: %v", err) - } - - observations, err := monitorClient.WorkerObservations() - if err != nil { - return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessage("unable to get jobs observations: %v", err) - } id, err := c.execMgr.Create(ctx, job.ReplicationVendorType, policy.ID, trigger, extra) if err != nil { return 0, err } - args := map[string]interface{}{} - - for _, o := range observations { - if o.JobName != job.ReplicationVendorType { - continue - } - if err = json.Unmarshal([]byte(o.ArgsJSON), &args); err != nil { - continue - } - policyID, ok := args["policy_id"].(float64) - if !ok { - continue - } - if int64(policyID) != policy.ID { - continue + if policy.SkipIfRunning { + log.Infof("kumar eh policy with ID %v skipped.", policy.ID) + monitorClient, err := jobmonitor.JobServiceMonitorClient() + if err != nil { + return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessagef("unable to get job monitor's client: %v", err) } - - err = c.execMgr.MarkSkipped(ctx, id, "task skipped as a duplicate") + observations, err := monitorClient.WorkerObservations() if err != nil { - return 0, err + return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessagef("unable to get jobs observations: %v", err) + } + for _, o := range observations { + if isDuplicateJob(o, policy.ID) { + err = c.execMgr.MarkSkipped(ctx, id, "task skipped as a duplicate") + if err != nil { + return 0, err + } + return id, nil + } } - return id, nil } // start the replication flow in background @@ -188,6 +178,18 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, return id, nil } +func isDuplicateJob(o *work.WorkerObservation, policyID int64) bool { + if o.JobName != job.ReplicationVendorType { + return false + } + args := map[string]interface{}{} + if err := json.Unmarshal([]byte(o.ArgsJSON), &args); err != nil { + return false + } + policyIDFromArgs, ok := args["policy_id"].(float64) + return ok && int64(policyIDFromArgs) == policyID +} + func (c *controller) markError(ctx context.Context, executionID int64, err error) { logger := log.GetLogger(ctx) // try to stop the execution first in case that some tasks are already created diff --git a/src/controller/replication/flow/copy.go b/src/controller/replication/flow/copy.go index 993536163aa..4512fd323b8 100644 --- a/src/controller/replication/flow/copy.go +++ b/src/controller/replication/flow/copy.go @@ -17,6 +17,7 @@ package flow import ( "context" "encoding/json" + repctlmodel "github.com/goharbor/harbor/src/controller/replication/model" "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/logger" @@ -136,11 +137,12 @@ func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources [ JobKind: job.KindGeneric, }, Parameters: map[string]interface{}{ - "src_resource": string(src), - "dst_resource": string(dest), - "speed": policy.Speed, - "copy_by_chunk": policy.CopyByChunk, - "policy_id": policy.ID, + "src_resource": string(src), + "dst_resource": string(dest), + "speed": policy.Speed, + "copy_by_chunk": policy.CopyByChunk, + "skip_if_running": policy.SkipIfRunning, + "policy_id": policy.ID, }, } @@ -149,7 +151,8 @@ func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources [ "resource_type": srcResource.Type, "source_resource": getResourceName(srcResource), "destination_resource": getResourceName(dstResource), - "references": getResourceReferences(dstResource)}); err != nil { + "references": getResourceReferences(dstResource), + }); err != nil { return err } diff --git a/src/controller/replication/model/model.go b/src/controller/replication/model/model.go index 63202d17973..158c8b5e2a9 100644 --- a/src/controller/replication/model/model.go +++ b/src/controller/replication/model/model.go @@ -47,6 +47,7 @@ type Policy struct { UpdateTime time.Time `json:"update_time"` Speed int32 `json:"speed"` CopyByChunk bool `json:"copy_by_chunk"` + SkipIfRunning bool `json:"skip_if_running"` } // IsScheduledTrigger returns true when the policy is scheduled trigger and enabled @@ -141,6 +142,7 @@ func (p *Policy) From(policy *replicationmodel.Policy) error { p.UpdateTime = policy.UpdateTime p.Speed = policy.Speed p.CopyByChunk = policy.CopyByChunk + p.SkipIfRunning = policy.SkipIfRunning if policy.SrcRegistryID > 0 { p.SrcRegistry = &model.Registry{ @@ -186,6 +188,7 @@ func (p *Policy) To() (*replicationmodel.Policy, error) { UpdateTime: p.UpdateTime, Speed: p.Speed, CopyByChunk: p.CopyByChunk, + SkipIfRunning: p.SkipIfRunning, } if p.SrcRegistry != nil { policy.SrcRegistryID = p.SrcRegistry.ID diff --git a/src/pkg/replication/model/model.go b/src/pkg/replication/model/model.go index 04d17237c8f..063424749f3 100644 --- a/src/pkg/replication/model/model.go +++ b/src/pkg/replication/model/model.go @@ -43,6 +43,7 @@ type Policy struct { UpdateTime time.Time `orm:"column(update_time);auto_now"` Speed int32 `orm:"column(speed_kb)"` CopyByChunk bool `orm:"column(copy_by_chunk)"` + SkipIfRunning bool `orm:"column(skip_if_running)"` } // TableName set table name for ORM diff --git a/src/server/v2.0/handler/replication.go b/src/server/v2.0/handler/replication.go index c5c700f679b..af4366e48e6 100644 --- a/src/server/v2.0/handler/replication.go +++ b/src/server/v2.0/handler/replication.go @@ -113,6 +113,10 @@ func (r *replicationAPI) CreateReplicationPolicy(ctx context.Context, params ope policy.CopyByChunk = *params.Policy.CopyByChunk } + if params.Policy.SkipIfRunning != nil { + policy.SkipIfRunning = *params.Policy.SkipIfRunning + } + id, err := r.ctl.CreatePolicy(ctx, policy) if err != nil { return r.SendError(ctx, err) @@ -181,6 +185,10 @@ func (r *replicationAPI) UpdateReplicationPolicy(ctx context.Context, params ope policy.CopyByChunk = *params.Policy.CopyByChunk } + if params.Policy.SkipIfRunning != nil { + policy.SkipIfRunning = *params.Policy.SkipIfRunning + } + if err := r.ctl.UpdatePolicy(ctx, policy); err != nil { return r.SendError(ctx, err) } @@ -446,6 +454,7 @@ func convertReplicationPolicy(policy *repctlmodel.Policy) *models.ReplicationPol Speed: &policy.Speed, UpdateTime: strfmt.DateTime(policy.UpdateTime), CopyByChunk: &policy.CopyByChunk, + SkipIfRunning: &policy.SkipIfRunning, } if policy.SrcRegistry != nil { p.SrcRegistry = convertRegistry(policy.SrcRegistry) From dd55f50ebb536ddbafd4aefe69e83d6c857322f2 Mon Sep 17 00:00:00 2001 From: bupd Date: Sun, 22 Dec 2024 22:04:20 +0530 Subject: [PATCH 04/14] Add Skip if running checkbox in replication & update swagger * Adds Checkbox in replication policy UI * Updates swagger & sql schema Signed-off-by: bupd --- api/v2.0/swagger.yaml | 4 +++ .../postgresql/0160_2.13.0_schema.up.sql | 2 ++ .../create-edit-rule.component.html | 25 +++++++++++++++++++ .../create-edit-rule.component.ts | 3 +++ src/portal/src/i18n/lang/de-de-lang.json | 2 ++ src/portal/src/i18n/lang/en-us-lang.json | 2 ++ src/portal/src/i18n/lang/es-es-lang.json | 2 ++ src/portal/src/i18n/lang/fr-fr-lang.json | 2 ++ src/portal/src/i18n/lang/ko-kr-lang.json | 2 ++ src/portal/src/i18n/lang/pt-br-lang.json | 2 ++ src/portal/src/i18n/lang/tr-tr-lang.json | 2 ++ src/portal/src/i18n/lang/zh-cn-lang.json | 2 ++ .../apitests/python/test_system_permission.py | 6 +++-- 13 files changed, 54 insertions(+), 2 deletions(-) diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index f69cd8c340b..5932b07aa00 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -7611,6 +7611,10 @@ definitions: type: boolean description: Whether to enable copy by chunk. x-isnullable: true + skip_if_running: + type: boolean + description: Whether to enable skip, if replication already running. + x-isnullable: true # make this field optional to keep backward compatibility ReplicationTrigger: type: object properties: diff --git a/make/migrations/postgresql/0160_2.13.0_schema.up.sql b/make/migrations/postgresql/0160_2.13.0_schema.up.sql index 88efb21b456..07f5e8c0cbf 100644 --- a/make/migrations/postgresql/0160_2.13.0_schema.up.sql +++ b/make/migrations/postgresql/0160_2.13.0_schema.up.sql @@ -21,3 +21,5 @@ CREATE INDEX IF NOT EXISTS idx_audit_log_ext_op_time ON audit_log_ext (op_time); CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_optime ON audit_log_ext (project_id, op_time); CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_resource_type ON audit_log_ext (project_id, resource_type); CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_operation ON audit_log_ext (project_id, operation); + +ALTER TABLE replication_policy ADD COLUMN IF NOT EXISTS skip_if_running boolean; diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html index 3bfcbb21346..f43db885f6a 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html +++ b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html @@ -825,6 +825,31 @@ 'REPLICATION.ENABLED_RULE' | translate }} +
+ + +
diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts index 5b66125146e..bc48a8179e2 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts +++ b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts @@ -334,6 +334,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy { override: true, speed: -1, copy_by_chunk: false, + skip_if_running: false, }); } @@ -367,6 +368,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy { dest_namespace_replace_count: Flatten_Level.FLATTEN_LEVEl_1, speed: -1, copy_by_chunk: false, + skip_if_running: false, }); this.isPushMode = true; this.selectedUnit = BandwidthUnit.KB; @@ -410,6 +412,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy { override: rule.override, speed: speed, copy_by_chunk: rule.copy_by_chunk, + skip_if_running: rule.skip_if_running, }); let filtersArray = this.getFilterArray(rule); this.noSelectedEndpoint = false; diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index b9379409fed..8eb22c6c1f2 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Lade die Ressourcen von der entfernten Registry auf den lokalen Harbor runter.", "DESTINATION_NAMESPACE": "Spezifizieren des Ziel-Namespace. Wenn das Feld leer ist, werden die Ressourcen unter dem gleichen Namespace abgelegt wie in der Quelle.", "OVERRIDE": "Spezifizieren, ob die Ressourcen am Ziel überschrieben werden sollen, falls eine Ressource mit gleichem Namen existiert.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "E-Mail sollte eine gültige E-Mail-Adresse wie name@example.com sein.", "USER_NAME": "Darf keine Sonderzeichen enthalten und sollte kürzer als 255 Zeichen sein.", "FULL_NAME": "Maximale Länge soll 20 Zeichen sein.", @@ -560,6 +561,7 @@ "ALLOWED_CHARACTERS": "Erlaubte Sonderzeichen", "TOTAL": "Gesamt", "OVERRIDE": "Überschreiben", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Aktiviere Regel", "OVERRIDE_INFO": "Überschreiben", "OPERATION": "Operation", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 88f756b027a..72d3700b5eb 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.", "DESTINATION_NAMESPACE": "Specify the destination namespace. If empty, the resources will be put under the same namespace as the source.", "OVERRIDE": "Specify whether to override the resources at the destination if a resource with the same name exists.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "Email should be a valid email address like name@example.com.", "USER_NAME": "Cannot contain special characters and maximum length should be 255 characters.", "FULL_NAME": "Maximum length should be 20 characters.", @@ -560,6 +561,7 @@ "ALLOWED_CHARACTERS": "Allowed special characters", "TOTAL": "Total", "OVERRIDE": "Override", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Enable rule", "OVERRIDE_INFO": "Override", "OPERATION": "Operation", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index b1561fed309..9b975157b79 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Pull the resources from the remote registry to the local Harbor.", "DESTINATION_NAMESPACE": "Specify the destination namespace. If empty, the resources will be put under the same namespace as the source.", "OVERRIDE": "Specify whether to override the resources at the destination if a resource with the same name exists.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "El email debe ser una dirección válida como nombre@ejemplo.com.", "USER_NAME": "Debe tener una longitud máxima de 255 caracteres y no puede contener caracteres especiales.", "FULL_NAME": "La longitud máxima debería ser de 20 caracteres.", @@ -560,6 +561,7 @@ "ALLOWED_CHARACTERS": "Allowed special characters", "TOTAL": "Total", "OVERRIDE": "Override", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Enable rule", "OVERRIDE_INFO": "Override", "CURRENT": "current", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 578026d8b88..d4f6f2c0f2f 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Pull les ressources du registre distant vers le Harbor local.", "DESTINATION_NAMESPACE": "Spécifier l'espace de nom de destination. Si vide, les ressources seront placées sous le même espace de nom que la source.", "OVERRIDE": "Spécifier s'il faut remplacer les ressources dans la destination si une ressource avec le même nom existe.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "L'e-mail doit être une adresse e-mail valide comme name@example.com.", "USER_NAME": "Ne peut pas contenir de caractères spéciaux et la longueur maximale est de 255 caractères.", "FULL_NAME": "La longueur maximale est de 20 caractères.", @@ -560,6 +561,7 @@ "ALLOWED_CHARACTERS": "Caractères spéciaux autorisés", "TOTAL": "Total", "OVERRIDE": "Surcharger", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Activer la règle", "OVERRIDE_INFO": "Surcharger", "OPERATION": "Opération", diff --git a/src/portal/src/i18n/lang/ko-kr-lang.json b/src/portal/src/i18n/lang/ko-kr-lang.json index 777ec5bd3be..641047f5558 100644 --- a/src/portal/src/i18n/lang/ko-kr-lang.json +++ b/src/portal/src/i18n/lang/ko-kr-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "원격 레지스트리의 리소스를 로컬 'Harbor'로 가져옵니다.", "DESTINATION_NAMESPACE": "대상 네임스페이스를 지정합니다. 비어 있으면 리소스는 소스와 동일한 네임스페이스에 배치됩니다.", "OVERRIDE": "동일한 이름의 리소스가 있는 경우 대상의 리소스를 재정의할지 여부를 지정합니다.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "이메일은 name@example.com과 같은 유효한 이메일 주소여야 합니다.", "USER_NAME": "특수 문자를 포함할 수 없으며 최대 길이는 255자입니다.", "FULL_NAME": "최대 길이는 20자입니다.", @@ -557,6 +558,7 @@ "ALLOWED_CHARACTERS": "허용되는 특수 문자", "TOTAL": "총", "OVERRIDE": "Override", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "규칙 활성화", "OVERRIDE_INFO": "Override", "OPERATION": "작업", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 095420e1424..658d303cfbf 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Trazer recursos do repositório remoto para o Harbor local.", "DESTINATION_NAMESPACE": "Especificar o namespace de destino. Se vazio, os recursos serão colocados no mesmo namespace que a fonte.", "OVERRIDE": "Sobrescrever recursos no destino se já existir com o mesmo nome.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "Deve ser um endereço de e-mail válido como nome@exemplo.com.", "USER_NAME": "Não pode conter caracteres especiais. Tamanho máximo de 255 caracteres.", "FULL_NAME": "Tamanho máximo de 20 caracteres.", @@ -558,6 +559,7 @@ "ALLOWED_CHARACTERS": "Símbolos permitidos", "TOTAL": "Total", "OVERRIDE": "Sobrescrever", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Habiltar regra", "OVERRIDE_INFO": "Sobrescrever", "CURRENT": "atual", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 8e30de6eefd..2fe483c3f06 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "Kaynakları uzak kayıt defterinden yerel Harbora çekin.", "DESTINATION_NAMESPACE": "Hedef ad alanını belirtin. Boşsa, kaynaklar, kaynak ile aynı ad alanına yerleştirilir.", "OVERRIDE": "Aynı adı taşıyan bir kaynak varsa, hedefteki kaynakları geçersiz kılmayacağınızı belirtin.", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "E-posta, ad@example.com gibi geçerli bir e-posta adresi olmalıdır.", "USER_NAME": "Özel karakterler içeremez ve maksimum uzunluk 255 karakter olmalıdır.", "FULL_NAME": "Maksimum uzunluk 20 karakter olmalıdır.", @@ -560,6 +561,7 @@ "ALLOWED_CHARACTERS": "İzin verilen özel karakterler", "TOTAL": "Toplam", "OVERRIDE": "Geçersiz Kıl", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "Kuralı etkinleştir", "OVERRIDE_INFO": "Geçersiz Kıl", "OPERATION": "Operasyon", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index ef941a46f34..84507e4e844 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -76,6 +76,7 @@ "PULL_BASED": "把资源由远端仓库拉取到本地Harbor。", "DESTINATION_NAMESPACE": "指定目标名称空间。如果不填,资源会被放到和源相同的名称空间下。", "OVERRIDE": "如果存在具有相同名称的资源,请指定是否覆盖目标上的资源。", + "SKIP_IF_RUNNING": "Specify whether to skip the execution when replication already running.", "EMAIL": "请使用正确的邮箱地址,比如name@example.com。", "USER_NAME": "不能包含特殊字符且长度不能超过255。", "FULL_NAME": "长度不能超过20。", @@ -558,6 +559,7 @@ "ALLOWED_CHARACTERS": "允许的特殊字符", "TOTAL": "总数", "OVERRIDE": "覆盖", + "SKIP_IF_RUNNING": "Skip if running", "ENABLED_RULE": "启用规则", "OVERRIDE_INFO": "覆盖", "CURRENT": "当前仓库", diff --git a/tests/apitests/python/test_system_permission.py b/tests/apitests/python/test_system_permission.py index 4e5cb667bf4..746d1372e8d 100644 --- a/tests/apitests/python/test_system_permission.py +++ b/tests/apitests/python/test_system_permission.py @@ -148,7 +148,8 @@ def call(self): "deletion": False, "override": True, "speed": -1, - "copy_by_chunk": False + "copy_by_chunk": False, + "skip_if_running": False } create_replication_policy = Permission("{}/replication/policies".format(harbor_base_url), "POST", 201, replication_policy_payload, "id", id_from_header=True) list_replication_policy = Permission("{}/replication/policies".format(harbor_base_url), "GET", 200, replication_policy_payload) @@ -201,7 +202,8 @@ def call(self): "deletion": False, "override": True, "speed": -1, - "copy_by_chunk": False + "copy_by_chunk": False, + "skip_if_running": False } response = requests.post("{}/replication/policies".format(harbor_base_url), data=json.dumps(replication_policy_payload), verify=False, auth=(admin_user_name, admin_password), headers={"Content-Type": "application/json"}) replication_policy_id = int(response.headers["Location"].split("/")[-1]) From cef59849d3de937afffbdd7c5009f67451abb80a Mon Sep 17 00:00:00 2001 From: bupd Date: Sun, 22 Dec 2024 22:44:02 +0530 Subject: [PATCH 05/14] update mocks & lint Signed-off-by: bupd --- src/controller/replication/execution.go | 3 +-- .../job/impl/replication/replication.go | 1 + src/server/v2.0/handler/replication.go | 2 +- src/testing/pkg/task/execution_manager.go | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/controller/replication/execution.go b/src/controller/replication/execution.go index bee22caee6d..8f6d9ae509d 100644 --- a/src/controller/replication/execution.go +++ b/src/controller/replication/execution.go @@ -21,9 +21,9 @@ import ( "time" "github.com/gocraft/work" - "github.com/goharbor/harbor/src/controller/jobmonitor" "github.com/goharbor/harbor/src/controller/event/operator" + "github.com/goharbor/harbor/src/controller/jobmonitor" "github.com/goharbor/harbor/src/controller/replication/flow" replicationmodel "github.com/goharbor/harbor/src/controller/replication/model" "github.com/goharbor/harbor/src/jobservice/job" @@ -120,7 +120,6 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, } if policy.SkipIfRunning { - log.Infof("kumar eh policy with ID %v skipped.", policy.ID) monitorClient, err := jobmonitor.JobServiceMonitorClient() if err != nil { return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessagef("unable to get job monitor's client: %v", err) diff --git a/src/jobservice/job/impl/replication/replication.go b/src/jobservice/job/impl/replication/replication.go index dc0de101284..453fa5e032b 100644 --- a/src/jobservice/job/impl/replication/replication.go +++ b/src/jobservice/job/impl/replication/replication.go @@ -17,6 +17,7 @@ package replication import ( "encoding/json" "fmt" + "github.com/goharbor/harbor/src/controller/replication/transfer" // import chart transfer _ "github.com/goharbor/harbor/src/controller/replication/transfer/image" diff --git a/src/server/v2.0/handler/replication.go b/src/server/v2.0/handler/replication.go index af4366e48e6..452e73d8171 100644 --- a/src/server/v2.0/handler/replication.go +++ b/src/server/v2.0/handler/replication.go @@ -454,7 +454,7 @@ func convertReplicationPolicy(policy *repctlmodel.Policy) *models.ReplicationPol Speed: &policy.Speed, UpdateTime: strfmt.DateTime(policy.UpdateTime), CopyByChunk: &policy.CopyByChunk, - SkipIfRunning: &policy.SkipIfRunning, + SkipIfRunning: &policy.SkipIfRunning, } if policy.SrcRegistry != nil { p.SrcRegistry = convertRegistry(policy.SrcRegistry) diff --git a/src/testing/pkg/task/execution_manager.go b/src/testing/pkg/task/execution_manager.go index bd2b7988831..058dc14ee10 100644 --- a/src/testing/pkg/task/execution_manager.go +++ b/src/testing/pkg/task/execution_manager.go @@ -213,6 +213,24 @@ func (_m *ExecutionManager) MarkError(ctx context.Context, id int64, message str return r0 } +// MarkSkipped provides a mock function with given fields: ctx, id, message +func (_m *ExecutionManager) MarkSkipped(ctx context.Context, id int64, message string) error { + ret := _m.Called(ctx, id, message) + + if len(ret) == 0 { + panic("no return value specified for MarkSkipped") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { + r0 = rf(ctx, id, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Stop provides a mock function with given fields: ctx, id func (_m *ExecutionManager) Stop(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) From f50e1c23b0fe81593dd3c6fa30f260c4096af062 Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 8 Jan 2025 06:05:30 +0530 Subject: [PATCH 06/14] rename skip if running to single active replication * renamed all occurences to single active replication * updated description and tests Signed-off-by: bupd --- api/v2.0/swagger.yaml | 6 ++++-- make/migrations/postgresql/0160_2.13.0_schema.up.sql | 2 +- src/controller/replication/execution.go | 4 ++-- src/controller/replication/flow/copy.go | 12 ++++++------ src/controller/replication/model/model.go | 6 +++--- src/pkg/replication/model/model.go | 2 +- .../create-edit-rule/create-edit-rule.component.html | 10 +++++----- .../create-edit-rule/create-edit-rule.component.ts | 6 +++--- src/portal/src/i18n/lang/de-de-lang.json | 4 ++-- src/portal/src/i18n/lang/en-us-lang.json | 4 ++-- src/portal/src/i18n/lang/es-es-lang.json | 4 ++-- src/portal/src/i18n/lang/fr-fr-lang.json | 4 ++-- src/portal/src/i18n/lang/ko-kr-lang.json | 4 ++-- src/portal/src/i18n/lang/pt-br-lang.json | 4 ++-- src/portal/src/i18n/lang/tr-tr-lang.json | 4 ++-- src/portal/src/i18n/lang/zh-cn-lang.json | 4 ++-- src/server/v2.0/handler/replication.go | 10 +++++----- tests/apitests/python/test_system_permission.py | 4 ++-- 18 files changed, 48 insertions(+), 46 deletions(-) diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 5932b07aa00..48838f8dec0 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -7611,9 +7611,11 @@ definitions: type: boolean description: Whether to enable copy by chunk. x-isnullable: true - skip_if_running: + single_active_replication: type: boolean - description: Whether to enable skip, if replication already running. + description: |- + Whether to Defer execution until the previous active execution finishes, + avoiding the execution of the same replication rules multiple times in parallel. x-isnullable: true # make this field optional to keep backward compatibility ReplicationTrigger: type: object diff --git a/make/migrations/postgresql/0160_2.13.0_schema.up.sql b/make/migrations/postgresql/0160_2.13.0_schema.up.sql index 07f5e8c0cbf..77df1028a8e 100644 --- a/make/migrations/postgresql/0160_2.13.0_schema.up.sql +++ b/make/migrations/postgresql/0160_2.13.0_schema.up.sql @@ -22,4 +22,4 @@ CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_optime ON audit_log_ext CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_resource_type ON audit_log_ext (project_id, resource_type); CREATE INDEX IF NOT EXISTS idx_audit_log_ext_project_id_operation ON audit_log_ext (project_id, operation); -ALTER TABLE replication_policy ADD COLUMN IF NOT EXISTS skip_if_running boolean; +ALTER TABLE replication_policy ADD COLUMN IF NOT EXISTS single_active_replication boolean; diff --git a/src/controller/replication/execution.go b/src/controller/replication/execution.go index 8f6d9ae509d..6a4d6bdec57 100644 --- a/src/controller/replication/execution.go +++ b/src/controller/replication/execution.go @@ -119,7 +119,7 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, return 0, err } - if policy.SkipIfRunning { + if policy.SingleActiveReplication { monitorClient, err := jobmonitor.JobServiceMonitorClient() if err != nil { return 0, errors.New(nil).WithCode(errors.PreconditionCode).WithMessagef("unable to get job monitor's client: %v", err) @@ -130,7 +130,7 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy, } for _, o := range observations { if isDuplicateJob(o, policy.ID) { - err = c.execMgr.MarkSkipped(ctx, id, "task skipped as a duplicate") + err = c.execMgr.MarkSkipped(ctx, id, "Execution deferred: active replication still in progress.") if err != nil { return 0, err } diff --git a/src/controller/replication/flow/copy.go b/src/controller/replication/flow/copy.go index 4512fd323b8..8cb087bcd7e 100644 --- a/src/controller/replication/flow/copy.go +++ b/src/controller/replication/flow/copy.go @@ -137,12 +137,12 @@ func (c *copyFlow) createTasks(ctx context.Context, srcResources, dstResources [ JobKind: job.KindGeneric, }, Parameters: map[string]interface{}{ - "src_resource": string(src), - "dst_resource": string(dest), - "speed": policy.Speed, - "copy_by_chunk": policy.CopyByChunk, - "skip_if_running": policy.SkipIfRunning, - "policy_id": policy.ID, + "src_resource": string(src), + "dst_resource": string(dest), + "speed": policy.Speed, + "copy_by_chunk": policy.CopyByChunk, + "single_active_replication": policy.SingleActiveReplication, + "policy_id": policy.ID, }, } diff --git a/src/controller/replication/model/model.go b/src/controller/replication/model/model.go index 158c8b5e2a9..70d98916a65 100644 --- a/src/controller/replication/model/model.go +++ b/src/controller/replication/model/model.go @@ -47,7 +47,7 @@ type Policy struct { UpdateTime time.Time `json:"update_time"` Speed int32 `json:"speed"` CopyByChunk bool `json:"copy_by_chunk"` - SkipIfRunning bool `json:"skip_if_running"` + SingleActiveReplication bool `json:"single_active_replication"` } // IsScheduledTrigger returns true when the policy is scheduled trigger and enabled @@ -142,7 +142,7 @@ func (p *Policy) From(policy *replicationmodel.Policy) error { p.UpdateTime = policy.UpdateTime p.Speed = policy.Speed p.CopyByChunk = policy.CopyByChunk - p.SkipIfRunning = policy.SkipIfRunning + p.SingleActiveReplication = policy.SingleActiveReplication if policy.SrcRegistryID > 0 { p.SrcRegistry = &model.Registry{ @@ -188,7 +188,7 @@ func (p *Policy) To() (*replicationmodel.Policy, error) { UpdateTime: p.UpdateTime, Speed: p.Speed, CopyByChunk: p.CopyByChunk, - SkipIfRunning: p.SkipIfRunning, + SingleActiveReplication: p.SingleActiveReplication, } if p.SrcRegistry != nil { policy.SrcRegistryID = p.SrcRegistry.ID diff --git a/src/pkg/replication/model/model.go b/src/pkg/replication/model/model.go index 063424749f3..6a4915cde55 100644 --- a/src/pkg/replication/model/model.go +++ b/src/pkg/replication/model/model.go @@ -43,7 +43,7 @@ type Policy struct { UpdateTime time.Time `orm:"column(update_time);auto_now"` Speed int32 `orm:"column(speed_kb)"` CopyByChunk bool `orm:"column(copy_by_chunk)"` - SkipIfRunning bool `orm:"column(skip_if_running)"` + SingleActiveReplication bool `orm:"column(single_active_replication)"` } // TableName set table name for ORM diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html index f43db885f6a..0b3c401c233 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html +++ b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html @@ -830,10 +830,10 @@ type="checkbox" class="clr-checkbox" [checked]="true" - id="skipIfRunning" - formControlName="skip_if_running" /> -