From bf1b3916360cc8786f7db2745a308584a1c41418 Mon Sep 17 00:00:00 2001 From: chlins Date: Thu, 12 Sep 2024 15:08:41 +0800 Subject: [PATCH 1/4] feat: add scope for preheat policy The preheat policy adds a new scope field to control and describe the scope of preheat, which currently supports two scopes: single peer and all peers. Signed-off-by: chlins --- api/v2.0/swagger.yaml | 3 + .../postgresql/0150_2.12.0_schema.up.sql | 2 + src/controller/p2p/preheat/enforcer.go | 5 +- src/controller/p2p/preheat/enforcer_test.go | 2 + src/pkg/p2p/preheat/models/policy/policy.go | 76 +++----------- .../p2p/preheat/models/policy/policy_test.go | 98 +++---------------- src/pkg/p2p/preheat/provider/dragonfly.go | 8 +- .../p2p/preheat/provider/dragonfly_test.go | 2 + src/pkg/p2p/preheat/provider/preheat_image.go | 3 + .../add-p2p-policy.component.html | 22 +++++ .../add-p2p-policy.component.ts | 21 ++++ .../p2p-provider/p2p-provider.service.ts | 10 ++ .../p2p-provider/policy/policy.component.ts | 1 + src/portal/src/i18n/lang/de-de-lang.json | 3 + src/portal/src/i18n/lang/en-us-lang.json | 3 + src/portal/src/i18n/lang/es-es-lang.json | 3 + src/portal/src/i18n/lang/fr-fr-lang.json | 3 + src/portal/src/i18n/lang/ko-kr-lang.json | 3 + src/portal/src/i18n/lang/pt-br-lang.json | 3 + src/portal/src/i18n/lang/tr-tr-lang.json | 3 + src/portal/src/i18n/lang/zh-cn-lang.json | 3 + src/portal/src/i18n/lang/zh-tw-lang.json | 3 + src/server/v2.0/handler/preheat.go | 2 + src/server/v2.0/handler/preheat_test.go | 6 +- 24 files changed, 134 insertions(+), 154 deletions(-) diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 48c604b1557..c6ad6db13d4 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -7095,6 +7095,9 @@ definitions: type: boolean description: Whether the preheat policy enabled x-omitempty: false + scope: + type: string + description: The scope of preheat policy creation_time: type: string format: date-time diff --git a/make/migrations/postgresql/0150_2.12.0_schema.up.sql b/make/migrations/postgresql/0150_2.12.0_schema.up.sql index ce167b83ebc..808a00160da 100644 --- a/make/migrations/postgresql/0150_2.12.0_schema.up.sql +++ b/make/migrations/postgresql/0150_2.12.0_schema.up.sql @@ -3,3 +3,5 @@ Add new column creator_ref and creator_type for robot table to record the creato */ ALTER TABLE robot ADD COLUMN IF NOT EXISTS creator_ref integer default 0; ALTER TABLE robot ADD COLUMN IF NOT EXISTS creator_type varchar(255); + +ALTER TABLE p2p_preheat_policy ADD COLUMN IF NOT EXISTS scope varchar(255); \ No newline at end of file diff --git a/src/controller/p2p/preheat/enforcer.go b/src/controller/p2p/preheat/enforcer.go index 2a0cc058535..005c60809f5 100644 --- a/src/controller/p2p/preheat/enforcer.go +++ b/src/controller/p2p/preheat/enforcer.go @@ -402,7 +402,7 @@ func (de *defaultEnforcer) launchExecutions(ctx context.Context, candidates []*s // Start tasks count := 0 for _, c := range candidates { - if _, err = de.startTask(ctx, eid, c, insData); err != nil { + if _, err = de.startTask(ctx, eid, c, insData, pl.Scope); err != nil { // Just log the error and skip log.Errorf("start task error for preheating image: %s/%s:%s@%s", c.Namespace, c.Repository, c.Tags[0], c.Digest) continue @@ -421,7 +421,7 @@ func (de *defaultEnforcer) launchExecutions(ctx context.Context, candidates []*s } // startTask starts the preheat task(job) for the given candidate -func (de *defaultEnforcer) startTask(ctx context.Context, executionID int64, candidate *selector.Candidate, instance string) (int64, error) { +func (de *defaultEnforcer) startTask(ctx context.Context, executionID int64, candidate *selector.Candidate, instance, scope string) (int64, error) { u, err := de.fullURLGetter(candidate) if err != nil { return -1, err @@ -441,6 +441,7 @@ func (de *defaultEnforcer) startTask(ctx context.Context, executionID int64, can ImageName: fmt.Sprintf("%s/%s", candidate.Namespace, candidate.Repository), Tag: candidate.Tags[0], Digest: candidate.Digest, + Scope: scope, } piData, err := pi.ToJSON() diff --git a/src/controller/p2p/preheat/enforcer_test.go b/src/controller/p2p/preheat/enforcer_test.go index 9d6ed5f2434..bbaadc9e607 100644 --- a/src/controller/p2p/preheat/enforcer_test.go +++ b/src/controller/p2p/preheat/enforcer_test.go @@ -210,6 +210,7 @@ func mockPolicies() []*po.Schema { Type: po.TriggerTypeManual, }, Enabled: true, + Scope: "single_peer", CreatedAt: time.Now().UTC(), UpdatedTime: time.Now().UTC(), }, { @@ -235,6 +236,7 @@ func mockPolicies() []*po.Schema { Trigger: &po.Trigger{ Type: po.TriggerTypeEventBased, }, + Scope: "all_peers", Enabled: true, CreatedAt: time.Now().UTC(), UpdatedTime: time.Now().UTC(), diff --git a/src/pkg/p2p/preheat/models/policy/policy.go b/src/pkg/p2p/preheat/models/policy/policy.go index 1a59bdddf40..b9e410b1c25 100644 --- a/src/pkg/p2p/preheat/models/policy/policy.go +++ b/src/pkg/p2p/preheat/models/policy/policy.go @@ -16,12 +16,10 @@ package policy import ( "encoding/json" - "fmt" "strconv" "time" beego_orm "github.com/beego/beego/v2/client/orm" - "github.com/beego/beego/v2/core/validation" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/lib/errors" @@ -32,6 +30,9 @@ func init() { beego_orm.RegisterModel(&Schema{}) } +// ScopeType represents the preheat scope type. +type ScopeType = string + const ( // Filters: // Repository : type=Repository value=name text (double star pattern used) @@ -57,6 +58,11 @@ const ( TriggerTypeScheduled TriggerType = "scheduled" // TriggerTypeEventBased represents the event_based trigger type TriggerTypeEventBased TriggerType = "event_based" + + // ScopeTypeSinglePeer represents preheat image to single peer in p2p cluster. + ScopeTypeSinglePeer ScopeType = "single_peer" + // ScopeTypeAllPeers represents preheat image to all peers in p2p cluster. + ScopeTypeAllPeers ScopeType = "all_peers" ) // Schema defines p2p preheat policy schema @@ -72,8 +78,10 @@ type Schema struct { FiltersStr string `orm:"column(filters)" json:"-"` Trigger *Trigger `orm:"-" json:"trigger"` // Use JSON data format (query by trigger type should be supported) - TriggerStr string `orm:"column(trigger)" json:"-"` - Enabled bool `orm:"column(enabled)" json:"enabled"` + TriggerStr string `orm:"column(trigger)" json:"-"` + Enabled bool `orm:"column(enabled)" json:"enabled"` + // Scope decides the preheat scope. + Scope string `orm:"column(scope)" json:"scope"` CreatedAt time.Time `orm:"column(creation_time)" json:"creation_time"` UpdatedTime time.Time `orm:"column(update_time)" json:"update_time"` } @@ -127,65 +135,13 @@ func (s *Schema) ValidatePreheatPolicy() error { WithMessage("invalid cron string for scheduled preheat: %s, error: %v", s.Trigger.Settings.Cron, err) } } - return nil -} -// Valid the policy -func (s *Schema) Valid(v *validation.Validation) { - if len(s.Name) == 0 { - _ = v.SetError("name", "cannot be empty") + // validate preheat scope + if s.Scope != "" && s.Scope != ScopeTypeSinglePeer && s.Scope != ScopeTypeAllPeers { + return errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid scope for preheat policy: %s", s.Scope) } - // valid the filters - for _, filter := range s.Filters { - switch filter.Type { - case FilterTypeRepository, FilterTypeTag, FilterTypeVulnerability: - _, ok := filter.Value.(string) - if !ok { - _ = v.SetError("filters", "the type of filter value isn't string") - break - } - case FilterTypeSignature: - _, ok := filter.Value.(bool) - if !ok { - _ = v.SetError("filers", "the type of signature filter value isn't bool") - break - } - case FilterTypeLabel: - labels, ok := filter.Value.([]interface{}) - if !ok { - _ = v.SetError("filters", "the type of label filter value isn't string slice") - break - } - for _, label := range labels { - _, ok := label.(string) - if !ok { - _ = v.SetError("filters", "the type of label filter value isn't string slice") - break - } - } - default: - _ = v.SetError("filters", "invalid filter type") - } - } - - // valid trigger - if s.Trigger != nil { - switch s.Trigger.Type { - case TriggerTypeManual, TriggerTypeEventBased: - case TriggerTypeScheduled: - if len(s.Trigger.Settings.Cron) == 0 { - _ = v.SetError("trigger", fmt.Sprintf("the cron string cannot be empty when the trigger type is %s", TriggerTypeScheduled)) - } else { - _, err := utils.CronParser().Parse(s.Trigger.Settings.Cron) - if err != nil { - _ = v.SetError("trigger", fmt.Sprintf("invalid cron string for scheduled trigger: %s", s.Trigger.Settings.Cron)) - } - } - default: - _ = v.SetError("trigger", "invalid trigger type") - } - } + return nil } // Encode encodes policy schema. diff --git a/src/pkg/p2p/preheat/models/policy/policy_test.go b/src/pkg/p2p/preheat/models/policy/policy_test.go index e2310933a6a..71a1e9ca1bd 100644 --- a/src/pkg/p2p/preheat/models/policy/policy_test.go +++ b/src/pkg/p2p/preheat/models/policy/policy_test.go @@ -17,8 +17,6 @@ package policy import ( "testing" - "github.com/beego/beego/v2/core/validation" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -66,92 +64,13 @@ func (p *PolicyTestSuite) TestValidatePreheatPolicy() { // valid cron string p.schema.Trigger.Settings.Cron = "0 0 0 1 1 *" p.NoError(p.schema.ValidatePreheatPolicy()) -} - -// TestValid tests Valid method. -func (p *PolicyTestSuite) TestValid() { - // policy name is empty, should return error - v := &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "no policy name should return one error") - require.Contains(p.T(), v.Errors[0].Error(), "cannot be empty") - - // policy with name but with error filter type - p.schema.Name = "policy-test" - p.schema.Filters = []*Filter{ - { - Type: "invalid-type", - }, - } - v = &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "invalid filter type should return one error") - require.Contains(p.T(), v.Errors[0].Error(), "invalid filter type") - - filterCases := [][]*Filter{ - { - { - Type: FilterTypeSignature, - Value: "invalid-value", - }, - }, - - { - { - Type: FilterTypeTag, - Value: true, - }, - }, - { - { - Type: FilterTypeLabel, - Value: "invalid-value", - }, - }, - } - // with valid filter type but with error value type - for _, filters := range filterCases { - p.schema.Filters = filters - v = &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "invalid filter value type should return one error") - } - // with valid filter but error trigger type - p.schema.Filters = []*Filter{ - { - Type: FilterTypeSignature, - Value: true, - }, - } - p.schema.Trigger = &Trigger{ - Type: "invalid-type", - } - v = &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "invalid trigger type should return one error") - require.Contains(p.T(), v.Errors[0].Error(), "invalid trigger type") - - // with valid filter but error trigger value - p.schema.Trigger = &Trigger{ - Type: TriggerTypeScheduled, - } - v = &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "invalid trigger value should return one error") - require.Contains(p.T(), v.Errors[0].Error(), "the cron string cannot be empty") - // with invalid cron - p.schema.Trigger.Settings.Cron = "1111111111111" - v = &validation.Validation{} - p.schema.Valid(v) - require.True(p.T(), v.HasErrors(), "invalid trigger value should return one error") - require.Contains(p.T(), v.Errors[0].Error(), "invalid cron string for scheduled trigger") - - // all is well - p.schema.Trigger.Settings.Cron = "0/12 * * * * *" - v = &validation.Validation{} - p.schema.Valid(v) - require.False(p.T(), v.HasErrors(), "should return nil error") + // invalid preheat scope + p.schema.Scope = "invalid scope" + p.Error(p.schema.ValidatePreheatPolicy()) + // valid preheat scope + p.schema.Scope = "single_peer" + p.NoError(p.schema.ValidatePreheatPolicy()) } // TestDecode tests decode. @@ -167,11 +86,14 @@ func (p *PolicyTestSuite) TestDecode() { Trigger: nil, TriggerStr: "{\"type\":\"event_based\",\"trigger_setting\":{\"cron\":\"\"}}", Enabled: false, + Scope: "all_peers", } p.NoError(s.Decode()) p.Len(s.Filters, 3) p.NotNil(s.Trigger) + p.Equal(ScopeTypeAllPeers, s.Scope) + // invalid filter or trigger s.FiltersStr = "" s.TriggerStr = "invalid" @@ -210,8 +132,10 @@ func (p *PolicyTestSuite) TestEncode() { }, TriggerStr: "", Enabled: false, + Scope: "single_peer", } p.NoError(s.Encode()) p.Equal(`[{"type":"repository","value":"**"},{"type":"tag","value":"**"},{"type":"label","value":"test"}]`, s.FiltersStr) p.Equal(`{"type":"event_based","trigger_setting":{}}`, s.TriggerStr) + p.Equal(ScopeTypeSinglePeer, s.Scope) } diff --git a/src/pkg/p2p/preheat/provider/dragonfly.go b/src/pkg/p2p/preheat/provider/dragonfly.go index 996594f5a76..6e44abb7666 100644 --- a/src/pkg/p2p/preheat/provider/dragonfly.go +++ b/src/pkg/p2p/preheat/provider/dragonfly.go @@ -59,10 +59,10 @@ func (dd *DragonflyDriver) Self() *Metadata { return &Metadata{ ID: "dragonfly", Name: "Dragonfly", - Icon: "https://raw.githubusercontent.com/alibaba/Dragonfly/master/docs/images/logo.png", - Version: "0.10.1", - Source: "https://github.com/alibaba/Dragonfly", - Maintainers: []string{"Jin Zhang/taiyun.zj@alibaba-inc.com"}, + Icon: "https://raw.githubusercontent.com/dragonflyoss/Dragonfly2/master/docs/images/logo/dragonfly-linear.png", + Version: "2.1.56", + Source: "https://github.com/dragonflyoss/Dragonfly2", + Maintainers: []string{"chlins.zhang@gmail.com", "gaius.qi@gmail.com"}, } } diff --git a/src/pkg/p2p/preheat/provider/dragonfly_test.go b/src/pkg/p2p/preheat/provider/dragonfly_test.go index e7bfa658feb..1657ea73483 100644 --- a/src/pkg/p2p/preheat/provider/dragonfly_test.go +++ b/src/pkg/p2p/preheat/provider/dragonfly_test.go @@ -86,6 +86,7 @@ func (suite *DragonflyTestSuite) TestPreheat() { Tag: "latest", URL: "https://harbor.com", Digest: "sha256:f3c97e3bd1e27393eb853a5c90b1132f2cda84336d5ba5d100c720dc98524c82", + Scope: "single_peer", }) require.NoError(suite.T(), err, "preheat image") suite.Equal("dragonfly-id", st.TaskID, "preheat image result") @@ -97,6 +98,7 @@ func (suite *DragonflyTestSuite) TestPreheat() { Tag: "latest", URL: "https://harbor.com", Digest: "sha256:f3c97e3bd1e27393eb853a5c90b1132f2cda84336d5ba5d100c720dc98524c82", + Scope: "all_peers", }) require.NoError(suite.T(), err, "preheat image") suite.Equal("", st.TaskID, "preheat image result") diff --git a/src/pkg/p2p/preheat/provider/preheat_image.go b/src/pkg/p2p/preheat/provider/preheat_image.go index e690d038544..8b576f96dec 100644 --- a/src/pkg/p2p/preheat/provider/preheat_image.go +++ b/src/pkg/p2p/preheat/provider/preheat_image.go @@ -45,6 +45,9 @@ type PreheatImage struct { // Digest of the preheating image Digest string `json:"digest"` + + // Scope indicates the preheat scope. + Scope string `json:"scope,omitempty"` } // FromJSON build preheating image from the given data. diff --git a/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html b/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html index 4dd78313eef..c3a308936ce 100644 --- a/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html +++ b/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html @@ -457,6 +457,28 @@