harbor/src/controller/p2p/preheat/controller.go

526 lines
15 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 preheat
import (
"context"
"time"
"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/p2p/preheat/instance"
policyModels "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
providerModels "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
"github.com/goharbor/harbor/src/pkg/p2p/preheat/policy"
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/task"
)
const (
// SchedulerCallback ...
SchedulerCallback = "P2PPreheatCallback"
)
var (
// Ctl is a global preheat controller instance
Ctl = NewController()
)
// ErrorConflict for handling conflicts
var ErrorConflict = errors.New("resource conflict")
// ErrorUnhealthy for unhealthy
var ErrorUnhealthy = errors.New("instance unhealthy")
// Controller defines related top interfaces to handle the workflow of
// the image distribution.
type Controller interface {
// Get all the supported distribution providers
//
// If succeed, an metadata of provider list will be returned.
// Otherwise, a non nil error will be returned
//
GetAvailableProviders() ([]*provider.Metadata, error)
// CountInstance all the setup instances of distribution providers
//
// params *q.Query : parameters for querying
//
// If succeed, matched provider instance count will be returned.
// Otherwise, a non nil error will be returned
//
CountInstance(ctx context.Context, query *q.Query) (int64, error)
// ListInstance all the setup instances of distribution providers
//
// params *q.Query : parameters for querying
//
// If succeed, matched provider instance list will be returned.
// Otherwise, a non nil error will be returned
//
ListInstance(ctx context.Context, query *q.Query) ([]*providerModels.Instance, error)
// GetInstance the metadata of the specified instance
//
// id string : ID of the instance being deleted
//
// If succeed, the metadata with nil error are returned
// Otherwise, a non nil error is returned
//
GetInstance(ctx context.Context, id int64) (*providerModels.Instance, error)
// GetInstance the metadata of the specified instance
GetInstanceByName(ctx context.Context, name string) (*providerModels.Instance, error)
// Create a new instance for the specified provider.
//
// If succeed, the ID of the instance will be returned.
// Any problems met, a non nil error will be returned.
//
CreateInstance(ctx context.Context, instance *providerModels.Instance) (int64, error)
// Delete the specified provider instance.
//
// id string : ID of the instance being deleted
//
// Any problems met, a non nil error will be returned.
//
DeleteInstance(ctx context.Context, id int64) error
// Update the instance with incremental way;
// Including update the enabled flag of the instance.
//
// id string : ID of the instance being updated
// properties ...string : The properties being updated
//
// Any problems met, a non nil error will be returned
//
UpdateInstance(ctx context.Context, instance *providerModels.Instance, properties ...string) error
// do not provide another policy controller, mixed in preheat controller
// CountPolicy returns the total count of the policy.
CountPolicy(ctx context.Context, query *q.Query) (int64, error)
// CreatePolicy creates the policy.
CreatePolicy(ctx context.Context, schema *policyModels.Schema) (int64, error)
// GetPolicy gets the policy by id.
GetPolicy(ctx context.Context, id int64) (*policyModels.Schema, error)
// GetPolicyByName gets the policy by name.
GetPolicyByName(ctx context.Context, projectID int64, name string) (*policyModels.Schema, error)
// UpdatePolicy updates the policy.
UpdatePolicy(ctx context.Context, schema *policyModels.Schema, props ...string) error
// DeletePolicy deletes the policy by id.
DeletePolicy(ctx context.Context, id int64) error
// ListPolicies lists policies by query.
ListPolicies(ctx context.Context, query *q.Query) ([]*policyModels.Schema, error)
// ListPoliciesByProject lists policies by project.
ListPoliciesByProject(ctx context.Context, project int64, query *q.Query) ([]*policyModels.Schema, error)
// CheckHealth checks the instance health, for test connection
CheckHealth(ctx context.Context, instance *providerModels.Instance) error
// DeletePoliciesOfProject delete all policies under one project
DeletePoliciesOfProject(ctx context.Context, project int64) error
}
var _ Controller = (*controller)(nil)
// controller is the default implementation of Controller interface.
type controller struct {
// For instance
iManager instance.Manager
// For policy
pManager policy.Manager
scheduler scheduler.Scheduler
executionMgr task.ExecutionManager
}
// NewController is constructor of controller
func NewController() Controller {
return &controller{
iManager: instance.Mgr,
pManager: policy.Mgr,
scheduler: scheduler.Sched,
executionMgr: task.NewExecutionManager(),
}
}
// GetAvailableProviders implements @Controller.GetAvailableProviders
func (c *controller) GetAvailableProviders() ([]*provider.Metadata, error) {
return provider.ListProviders()
}
// CountInstance implements @Controller.CountInstance
func (c *controller) CountInstance(ctx context.Context, query *q.Query) (int64, error) {
return c.iManager.Count(ctx, query)
}
// ListInstance implements @Controller.ListInstance
func (c *controller) ListInstance(ctx context.Context, query *q.Query) ([]*providerModels.Instance, error) {
return c.iManager.List(ctx, query)
}
// CreateInstance implements @Controller.CreateInstance
func (c *controller) CreateInstance(ctx context.Context, instance *providerModels.Instance) (int64, error) {
if instance == nil {
return 0, errors.New("nil instance object provided")
}
// Avoid duplicated endpoint
var query = &q.Query{
Keywords: map[string]interface{}{
"endpoint": instance.Endpoint,
},
}
num, err := c.iManager.Count(ctx, query)
if err != nil {
return 0, err
}
if num > 0 {
return 0, ErrorConflict
}
// !WARN: We don't check the health of the instance here.
// That is ok because the health of instance will be checked before enforcing the policy each time.
instance.SetupTimestamp = time.Now().Unix()
return c.iManager.Save(ctx, instance)
}
// DeleteInstance implements @Controller.Delete
func (c *controller) DeleteInstance(ctx context.Context, id int64) error {
ins, err := c.GetInstance(ctx, id)
if err != nil {
return err
}
// delete instance should check the instance whether be used by policies
policies, err := c.ListPolicies(ctx, &q.Query{
Keywords: map[string]interface{}{
"provider_id": id,
},
})
if err != nil {
return err
}
if len(policies) > 0 {
return errors.New(nil).
WithCode(errors.PreconditionCode).
WithMessage("Provider [%s] cannot be deleted as some preheat policies are using it", ins.Name)
}
return c.iManager.Delete(ctx, id)
}
// UpdateInstance implements @Controller.Update
func (c *controller) UpdateInstance(ctx context.Context, instance *providerModels.Instance, properties ...string) error {
oldIns, err := c.GetInstance(ctx, instance.ID)
if err != nil {
return err
}
if !instance.Enabled {
// update instance should check the instance whether be used by policies
policies, err := c.ListPolicies(ctx, &q.Query{
Keywords: map[string]interface{}{
"provider_id": instance.ID,
},
})
if err != nil {
return err
}
if len(policies) > 0 {
return errors.New(nil).
WithCode(errors.PreconditionCode).
WithMessage("Provider [%s] cannot be disabled as some preheat policies are using it", oldIns.Name)
}
}
// vendor type does not support change
if oldIns.Vendor != instance.Vendor {
return errors.Errorf("provider [%s] vendor cannot be changed", oldIns.Name)
}
return c.iManager.Update(ctx, instance, properties...)
}
// GetInstance implements @Controller.Get
func (c *controller) GetInstance(ctx context.Context, id int64) (*providerModels.Instance, error) {
return c.iManager.Get(ctx, id)
}
func (c *controller) GetInstanceByName(ctx context.Context, name string) (*providerModels.Instance, error) {
return c.iManager.GetByName(ctx, name)
}
// CountPolicy returns the total count of the policy.
func (c *controller) CountPolicy(ctx context.Context, query *q.Query) (int64, error) {
return c.pManager.Count(ctx, query)
}
// TriggerParam ...
type TriggerParam struct {
PolicyID int64
}
// CreatePolicy creates the policy.
func (c *controller) CreatePolicy(ctx context.Context, schema *policyModels.Schema) (id int64, err error) {
if schema == nil {
return 0, errors.New("nil policy schema provided")
}
// Update timestamps
now := time.Now()
schema.CreatedAt = now
schema.UpdatedTime = now
// Get full model of policy schema
err = schema.Decode()
if err != nil {
return 0, err
}
// valid policy schema
err = schema.ValidatePreheatPolicy()
if err != nil {
return 0, err
}
id, err = c.pManager.Create(ctx, schema)
if err != nil {
return
}
schema.ID = id
if schema.Trigger != nil &&
schema.Trigger.Type == policyModels.TriggerTypeScheduled &&
len(schema.Trigger.Settings.Cron) > 0 {
// schedule and update policy
extras := make(map[string]interface{})
if _, err = c.scheduler.Schedule(ctx, job.P2PPreheatVendorType, id, "", schema.Trigger.Settings.Cron,
SchedulerCallback, TriggerParam{PolicyID: id}, extras); err != nil {
return 0, err
}
if err = schema.Encode(); err == nil {
err = c.pManager.Update(ctx, schema, "trigger")
}
if err != nil {
if e := c.scheduler.UnScheduleByVendor(ctx, job.P2PPreheatVendorType, id); e != nil {
return 0, errors.Wrap(e, err.Error())
}
return 0, err
}
}
return
}
// GetPolicy gets the policy by id.
func (c *controller) GetPolicy(ctx context.Context, id int64) (*policyModels.Schema, error) {
return c.pManager.Get(ctx, id)
}
// GetPolicyByName gets the policy by name.
func (c *controller) GetPolicyByName(ctx context.Context, projectID int64, name string) (*policyModels.Schema, error) {
return c.pManager.GetByName(ctx, projectID, name)
}
// UpdatePolicy updates the policy.
func (c *controller) UpdatePolicy(ctx context.Context, schema *policyModels.Schema, props ...string) error {
if schema == nil {
return errors.New("nil policy schema provided")
}
// Get policy cache
s0, err := c.pManager.Get(ctx, schema.ID)
if err != nil {
return err
}
// Double check trigger
if s0.Trigger == nil {
return errors.Errorf("missing trigger settings in preheat policy %s", s0.Name)
}
// Get full model of updating policy
err = schema.Decode()
if err != nil {
return err
}
// valid policy schema
err = schema.ValidatePreheatPolicy()
if err != nil {
return err
}
var cron = schema.Trigger.Settings.Cron
var oldCron = s0.Trigger.Settings.Cron
var needUn bool
var needSch bool
if s0.Trigger.Type != schema.Trigger.Type {
if s0.Trigger.Type == policyModels.TriggerTypeScheduled && len(oldCron) > 0 {
needUn = true
}
if schema.Trigger.Type == policyModels.TriggerTypeScheduled && len(cron) > 0 {
needSch = true
}
} else {
// not change trigger type
if schema.Trigger.Type == policyModels.TriggerTypeScheduled && oldCron != cron {
// unschedule old
if len(oldCron) > 0 {
needUn = true
}
// schedule new
if len(cron) > 0 {
// valid cron
needSch = true
}
}
}
// unschedule old
if needUn {
err = c.scheduler.UnScheduleByVendor(ctx, job.P2PPreheatVendorType, schema.ID)
if err != nil {
return err
}
}
// schedule new
if needSch {
extras := make(map[string]interface{})
if _, err := c.scheduler.Schedule(ctx, job.P2PPreheatVendorType, schema.ID, "", cron, SchedulerCallback,
TriggerParam{PolicyID: schema.ID}, extras); err != nil {
return err
}
}
// Update timestamp
schema.UpdatedTime = time.Now()
err = c.pManager.Update(ctx, schema, props...)
if (err != nil) && (needSch || needUn) {
return errors.Wrapf(err, "Update failed, but not rollback scheduler")
}
return err
}
// DeletePolicy deletes the policy by id.
func (c *controller) DeletePolicy(ctx context.Context, id int64) error {
s, err := c.pManager.Get(ctx, id)
if err != nil {
return err
}
if s.Trigger != nil && s.Trigger.Type == policyModels.TriggerTypeScheduled && len(s.Trigger.Settings.Cron) > 0 {
err = c.scheduler.UnScheduleByVendor(ctx, job.P2PPreheatVendorType, id)
if err != nil {
return err
}
}
if err = c.deleteExecs(ctx, id); err != nil {
return err
}
return c.pManager.Delete(ctx, id)
}
// DeletePoliciesOfProject deletes all the policy under project.
func (c *controller) DeletePoliciesOfProject(ctx context.Context, project int64) error {
policies, err := c.ListPoliciesByProject(ctx, project, nil)
if err != nil {
return err
}
for _, p := range policies {
if err = c.DeletePolicy(ctx, p.ID); err != nil {
return err
}
}
return nil
}
// deleteExecs delete executions
func (c *controller) deleteExecs(ctx context.Context, vendorID int64) error {
executions, err := c.executionMgr.List(ctx, &q.Query{
Keywords: map[string]interface{}{
"VendorType": job.P2PPreheatVendorType,
"VendorID": vendorID,
},
})
if err != nil {
return err
}
for _, execution := range executions {
if err = c.executionMgr.Delete(ctx, execution.ID); err != nil {
return err
}
}
return nil
}
// ListPolicies lists policies by query.
func (c *controller) ListPolicies(ctx context.Context, query *q.Query) ([]*policyModels.Schema, error) {
return c.pManager.ListPolicies(ctx, query)
}
// ListPoliciesByProject lists policies by project.
func (c *controller) ListPoliciesByProject(ctx context.Context, project int64, query *q.Query) ([]*policyModels.Schema, error) {
return c.pManager.ListPoliciesByProject(ctx, project, query)
}
// CheckHealth checks the instance health, for test connection
func (c *controller) CheckHealth(_ context.Context, instance *providerModels.Instance) error {
if instance == nil {
return errors.New("instance can not be nil")
}
fac, ok := provider.GetProvider(instance.Vendor)
if !ok {
return errors.Errorf("no driver registered for provider %s", instance.Vendor)
}
// Construct driver
driver, err := fac(instance)
if err != nil {
return err
}
// Check health
h, err := driver.GetHealth()
if err != nil {
return err
}
if h.Status != provider.DriverStatusHealthy {
return errors.Errorf("preheat provider instance %s-%s:%s is not healthy", instance.Vendor, instance.Name, instance.Endpoint)
}
return nil
}