harbor/src/server/v2.0/handler/replication.go

569 lines
18 KiB
Go

// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package handler
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/replication"
repctlmodel "github.com/goharbor/harbor/src/controller/replication/model"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/reg/model"
"github.com/goharbor/harbor/src/pkg/task"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/replication"
)
func newReplicationAPI() *replicationAPI {
return &replicationAPI{
ctl: replication.Ctl,
}
}
type replicationAPI struct {
BaseAPI
ctl replication.Controller
}
func (r *replicationAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
return nil
}
func (r *replicationAPI) CreateReplicationPolicy(ctx context.Context, params operation.CreateReplicationPolicyParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplicationPolicy); err != nil {
return r.SendError(ctx, err)
}
sc, err := r.GetSecurityContext(ctx)
if err != nil {
return r.SendError(ctx, err)
}
policy := &repctlmodel.Policy{
Name: params.Policy.Name,
Description: params.Policy.Description,
Creator: sc.GetUsername(),
DestNamespace: params.Policy.DestNamespace,
ReplicateDeletion: params.Policy.Deletion,
Override: params.Policy.Override,
Enabled: params.Policy.Enabled,
}
// Make this field be optional to keep backward compatibility
if params.Policy.DestNamespaceReplaceCount != nil {
policy.DestNamespaceReplaceCount = *params.Policy.DestNamespaceReplaceCount
} else {
policy.DestNamespaceReplaceCount = -1 // -1 mean the legacy mode
}
if params.Policy.SrcRegistry != nil {
policy.SrcRegistry = &model.Registry{
ID: params.Policy.SrcRegistry.ID,
}
}
if params.Policy.DestRegistry != nil {
policy.DestRegistry = &model.Registry{
ID: params.Policy.DestRegistry.ID,
}
}
if len(params.Policy.Filters) > 0 {
for _, filter := range params.Policy.Filters {
policy.Filters = append(policy.Filters, &model.Filter{
Type: filter.Type,
Value: filter.Value,
Decoration: filter.Decoration,
})
}
}
if params.Policy.Trigger != nil {
policy.Trigger = &model.Trigger{
Type: params.Policy.Trigger.Type,
}
if params.Policy.Trigger.TriggerSettings != nil {
policy.Trigger.Settings = &model.TriggerSettings{
Cron: params.Policy.Trigger.TriggerSettings.Cron,
}
}
}
if params.Policy.Speed != nil {
if *params.Policy.Speed < 0 {
*params.Policy.Speed = 0
}
policy.Speed = *params.Policy.Speed
}
if params.Policy.CopyByChunk != nil {
policy.CopyByChunk = *params.Policy.CopyByChunk
}
id, err := r.ctl.CreatePolicy(ctx, policy)
if err != nil {
return r.SendError(ctx, err)
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), id)
return operation.NewCreateReplicationPolicyCreated().WithLocation(location)
}
func (r *replicationAPI) UpdateReplicationPolicy(ctx context.Context, params operation.UpdateReplicationPolicyParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceReplicationPolicy); err != nil {
return r.SendError(ctx, err)
}
policy := &repctlmodel.Policy{
ID: params.ID,
Name: params.Policy.Name,
Description: params.Policy.Description,
DestNamespace: params.Policy.DestNamespace,
ReplicateDeletion: params.Policy.Deletion,
Override: params.Policy.Override,
Enabled: params.Policy.Enabled,
}
// Make this field be optional to keep backward compatibility
if params.Policy.DestNamespaceReplaceCount != nil {
policy.DestNamespaceReplaceCount = *params.Policy.DestNamespaceReplaceCount
} else {
policy.DestNamespaceReplaceCount = -1 // -1 mean the legacy mode
}
if params.Policy.SrcRegistry != nil {
policy.SrcRegistry = &model.Registry{
ID: params.Policy.SrcRegistry.ID,
}
}
if params.Policy.DestRegistry != nil {
policy.DestRegistry = &model.Registry{
ID: params.Policy.DestRegistry.ID,
}
}
if len(params.Policy.Filters) > 0 {
for _, filter := range params.Policy.Filters {
policy.Filters = append(policy.Filters, &model.Filter{
Type: filter.Type,
Value: filter.Value,
Decoration: filter.Decoration,
})
}
}
if params.Policy.Trigger != nil {
policy.Trigger = &model.Trigger{
Type: params.Policy.Trigger.Type,
}
if params.Policy.Trigger.TriggerSettings != nil {
policy.Trigger.Settings = &model.TriggerSettings{
Cron: params.Policy.Trigger.TriggerSettings.Cron,
}
}
}
if params.Policy.Speed != nil {
if *params.Policy.Speed < 0 {
*params.Policy.Speed = 0
}
policy.Speed = *params.Policy.Speed
}
if params.Policy.CopyByChunk != nil {
policy.CopyByChunk = *params.Policy.CopyByChunk
}
if err := r.ctl.UpdatePolicy(ctx, policy); err != nil {
return r.SendError(ctx, err)
}
return operation.NewUpdateReplicationPolicyOK()
}
func (r *replicationAPI) ListReplicationPolicies(ctx context.Context, params operation.ListReplicationPoliciesParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplicationPolicy); err != nil {
return r.SendError(ctx, err)
}
query, err := r.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return r.SendError(ctx, err)
}
if params.Name != nil {
query.Keywords["Name"] = &q.FuzzyMatchValue{
Value: *params.Name,
}
}
total, err := r.ctl.PolicyCount(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
policies, err := r.ctl.ListPolicies(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
var result []*models.ReplicationPolicy
for _, policy := range policies {
result = append(result, convertReplicationPolicy(policy))
}
return operation.NewListReplicationPoliciesOK().
WithXTotalCount(total).
WithLink(r.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(result)
}
func (r *replicationAPI) GetReplicationPolicy(ctx context.Context, params operation.GetReplicationPolicyParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceReplicationPolicy); err != nil {
return r.SendError(ctx, err)
}
policy, err := r.ctl.GetPolicy(ctx, params.ID)
if err != nil {
return r.SendError(ctx, err)
}
return operation.NewGetReplicationPolicyOK().WithPayload(convertReplicationPolicy(policy))
}
func (r *replicationAPI) DeleteReplicationPolicy(ctx context.Context, params operation.DeleteReplicationPolicyParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceReplicationPolicy); err != nil {
return r.SendError(ctx, err)
}
if err := r.ctl.DeletePolicy(ctx, params.ID); err != nil {
return r.SendError(ctx, err)
}
return operation.NewDeleteReplicationPolicyOK()
}
func (r *replicationAPI) StartReplication(ctx context.Context, params operation.StartReplicationParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
policy, err := r.ctl.GetPolicy(ctx, params.Execution.PolicyID)
if err != nil {
return r.SendError(ctx, err)
}
// the legacy replication scheduler job("src/jobservice/job/impl/replication/scheduler.go") calls the start replication API
// to trigger the scheduled replication, a query string "trigger" is added when sending the request
// here is the logic to cover this part
trigger := task.ExecutionTriggerManual
if params.HTTPRequest.URL.Query().Get("trigger") == "scheduled" {
trigger = task.ExecutionTriggerSchedule
}
executionID, err := r.ctl.Start(ctx, policy, nil, trigger)
if err != nil {
return r.SendError(ctx, err)
}
location := strings.TrimSuffix(params.HTTPRequest.URL.Path, "/") + "/" + strconv.FormatInt(executionID, 10)
return operation.NewStartReplicationCreated().WithLocation(location)
}
func (r *replicationAPI) StopReplication(ctx context.Context, params operation.StopReplicationParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
if err := r.ctl.Stop(ctx, params.ID); err != nil {
return r.SendError(ctx, err)
}
return nil
}
func (r *replicationAPI) ListReplicationExecutions(ctx context.Context, params operation.ListReplicationExecutionsParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
query, err := r.BuildQuery(ctx, nil, params.Sort, params.Page, params.PageSize)
if err != nil {
return r.SendError(ctx, err)
}
if params.PolicyID != nil {
query.Keywords["PolicyID"] = *params.PolicyID
}
if params.Status != nil {
status := *params.Status
// as we convert the status when responding requests to keep the backward compatibility,
// here we need to reverse-convert the status
switch status {
case "InProgress":
status = job.RunningStatus.String()
case "Succeed":
status = job.SuccessStatus.String()
case "Stopped":
status = job.StoppedStatus.String()
case "Failed":
status = job.ErrorStatus.String()
}
query.Keywords["Status"] = status
}
if params.Trigger != nil {
trigger := *params.Trigger
// as we convert the trigger when responding requests to keep the backward compatibility,
// here we need to reverse-convert the trigger
switch trigger {
case "manual":
trigger = task.ExecutionTriggerManual
case "scheduled":
trigger = task.ExecutionTriggerSchedule
case "event_based":
trigger = task.ExecutionTriggerEvent
}
query.Keywords["Trigger"] = trigger
}
total, err := r.ctl.ExecutionCount(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
executions, err := r.ctl.ListExecutions(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
var execs []*models.ReplicationExecution
for _, execution := range executions {
execs = append(execs, convertExecution(execution))
}
return operation.NewListReplicationExecutionsOK().
WithXTotalCount(total).
WithLink(r.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(execs)
}
func (r *replicationAPI) GetReplicationExecution(ctx context.Context, params operation.GetReplicationExecutionParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
execution, err := r.ctl.GetExecution(ctx, params.ID)
if err != nil {
return r.SendError(ctx, err)
}
return operation.NewGetReplicationExecutionOK().WithPayload(convertExecution(execution))
}
func (r *replicationAPI) ListReplicationTasks(ctx context.Context, params operation.ListReplicationTasksParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
// check the existence of the replication execution
_, err := r.ctl.GetExecution(ctx, params.ID)
if err != nil {
return r.SendError(ctx, err)
}
query, err := r.BuildQuery(ctx, nil, params.Sort, params.Page, params.PageSize)
if err != nil {
return r.SendError(ctx, err)
}
query.Keywords["ExecutionID"] = params.ID
if params.Status != nil {
var status interface{} = *params.Status
// as we convert the status when responding requests to keep the backward compatibility,
// here we need to reverse-convert the status
// the status "pending" and "stopped" is same with jobservice, no need to convert
switch status {
case "InProgress":
status = &q.OrList{
Values: []interface{}{
job.ScheduledStatus.String(),
job.RunningStatus.String(),
},
}
case "Succeed":
status = job.SuccessStatus.String()
case "Failed":
status = job.ErrorStatus.String()
}
query.Keywords["Status"] = status
}
if params.ResourceType != nil {
query.Keywords["ExtraAttrs.resource_type"] = *params.ResourceType
}
total, err := r.ctl.TaskCount(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
tasks, err := r.ctl.ListTasks(ctx, query)
if err != nil {
return r.SendError(ctx, err)
}
var tks []*models.ReplicationTask
for _, task := range tasks {
tks = append(tks, convertTask(task))
}
return operation.NewListReplicationTasksOK().
WithXTotalCount(total).
WithLink(r.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(tks)
}
func (r *replicationAPI) GetReplicationLog(ctx context.Context, params operation.GetReplicationLogParams) middleware.Responder {
if err := r.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err)
}
execution, err := r.ctl.GetExecution(ctx, params.ID)
if err != nil {
return r.SendError(ctx, err)
}
task, err := r.ctl.GetTask(ctx, params.TaskID)
if err != nil {
return r.SendError(ctx, err)
}
if execution.ID != task.ExecutionID {
return r.SendError(ctx, errors.New(nil).
WithCode(errors.NotFoundCode).
WithMessage("execution %d contains no task with ID %d", params.ID, params.TaskID))
}
log, err := r.ctl.GetTaskLog(ctx, params.TaskID)
if err != nil {
return r.SendError(ctx, err)
}
return operation.NewGetReplicationLogOK().WithContentType("text/plain").WithPayload(string(log))
}
func convertReplicationPolicy(policy *repctlmodel.Policy) *models.ReplicationPolicy {
replaceCount := policy.DestNamespaceReplaceCount
p := &models.ReplicationPolicy{
CreationTime: strfmt.DateTime(policy.CreationTime),
Deletion: policy.ReplicateDeletion,
Description: policy.Description,
DestNamespace: policy.DestNamespace,
DestNamespaceReplaceCount: &replaceCount,
Enabled: policy.Enabled,
ID: policy.ID,
Name: policy.Name,
Override: policy.Override,
ReplicateDeletion: policy.ReplicateDeletion,
Speed: &policy.Speed,
UpdateTime: strfmt.DateTime(policy.UpdateTime),
CopyByChunk: &policy.CopyByChunk,
}
if policy.SrcRegistry != nil {
p.SrcRegistry = convertRegistry(policy.SrcRegistry)
}
if policy.DestRegistry != nil {
p.DestRegistry = convertRegistry(policy.DestRegistry)
}
if len(policy.Filters) > 0 {
for _, filter := range policy.Filters {
p.Filters = append(p.Filters, &models.ReplicationFilter{
Type: string(filter.Type),
Value: filter.Value,
Decoration: filter.Decoration,
})
}
}
if policy.Trigger != nil {
trigger := &models.ReplicationTrigger{
Type: string(policy.Trigger.Type),
}
if policy.Trigger.Settings != nil {
trigger.TriggerSettings = &models.ReplicationTriggerSettings{
Cron: policy.Trigger.Settings.Cron,
}
}
p.Trigger = trigger
}
return p
}
func convertRegistry(registry *model.Registry) *models.Registry {
r := &models.Registry{
CreationTime: strfmt.DateTime(registry.CreationTime),
Description: registry.Description,
ID: registry.ID,
Insecure: registry.Insecure,
Name: registry.Name,
Status: registry.Status,
Type: string(registry.Type),
UpdateTime: strfmt.DateTime(registry.UpdateTime),
URL: registry.URL,
}
if registry.Credential != nil {
credential := &models.RegistryCredential{
AccessKey: registry.Credential.AccessKey,
Type: string(registry.Credential.Type),
}
if len(registry.Credential.AccessSecret) > 0 {
credential.AccessSecret = "*****"
}
r.Credential = credential
}
return r
}
func convertExecution(execution *replication.Execution) *models.ReplicationExecution {
exec := &models.ReplicationExecution{
ID: execution.ID,
PolicyID: execution.PolicyID,
StatusText: execution.StatusMessage,
StartTime: strfmt.DateTime(execution.StartTime),
EndTime: strfmt.DateTime(execution.EndTime),
}
// keep backward compatibility
if execution.Metrics != nil {
exec.Total = execution.Metrics.TaskCount
exec.Succeed = execution.Metrics.SuccessTaskCount
exec.Failed = execution.Metrics.ErrorTaskCount
exec.InProgress = execution.Metrics.PendingTaskCount +
execution.Metrics.ScheduledTaskCount + execution.Metrics.RunningTaskCount
exec.Stopped = execution.Metrics.StoppedTaskCount
}
switch execution.Trigger {
case task.ExecutionTriggerManual:
exec.Trigger = "manual"
case task.ExecutionTriggerSchedule:
exec.Trigger = "scheduled"
case task.ExecutionTriggerEvent:
exec.Trigger = "event_based"
}
switch execution.Status {
case job.RunningStatus.String():
exec.Status = "InProgress"
case job.SuccessStatus.String():
exec.Status = "Succeed"
case job.StoppedStatus.String():
exec.Status = "Stopped"
case job.ErrorStatus.String():
exec.Status = "Failed"
}
return exec
}
func convertTask(task *replication.Task) *models.ReplicationTask {
tk := &models.ReplicationTask{
ID: task.ID,
ExecutionID: task.ExecutionID,
JobID: task.JobID,
Operation: task.Operation,
ResourceType: task.ResourceType,
SrcResource: task.SourceResource,
DstResource: task.DestinationResource,
StartTime: strfmt.DateTime(task.StartTime),
EndTime: strfmt.DateTime(task.EndTime),
}
// keep backward compatibility
switch task.Status {
case job.ScheduledStatus.String(), job.RunningStatus.String():
tk.Status = "InProgress"
case job.SuccessStatus.String():
tk.Status = "Succeed"
case job.ErrorStatus.String():
tk.Status = "Failed"
// the status "pending" and "stopped" is same with jobservice, no need to convert
default:
tk.Status = task.Status
}
return tk
}