From ceb95229cd0e3fb82b82d7a73a0d376bc0bfd239 Mon Sep 17 00:00:00 2001 From: Kingram Date: Mon, 10 Jan 2022 21:26:43 +0800 Subject: [PATCH] Feat: add Harbor webhook trigger (#3065) * Feat: add harbor webhook trigger (#3029) Signed-off-by: kingram * resolve comments Signed-off-by: kingram * fix: add comments Signed-off-by: kingram --- pkg/apiserver/model/application.go | 7 ++ pkg/apiserver/rest/apis/v1/types.go | 32 ++++++++- pkg/apiserver/rest/usecase/application.go | 3 - pkg/apiserver/rest/usecase/webhook.go | 80 ++++++++++++++++++++++ pkg/apiserver/rest/usecase/webhook_test.go | 44 ++++++++++-- pkg/apiserver/rest/webservice/validate.go | 16 +++++ 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 008759436..31e2f82cd 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -322,6 +322,8 @@ const ( PayloadTypeDockerhub = "dockerhub" // PayloadTypeACR is the payload type acr PayloadTypeACR = "acr" + // PayloadTypeHarbor is the payload type harbor + PayloadTypeHarbor = "harbor" // ComponentTypeWebservice is the component type webservice ComponentTypeWebservice = "webservice" @@ -331,6 +333,11 @@ const ( ComponentTypeTask = "task" ) +const ( + // HarborEventTypePushArtifact is the event type PUSH_ARTIFACT + HarborEventTypePushArtifact = "PUSH_ARTIFACT" +) + // TableName return custom table name func (w *ApplicationTrigger) TableName() string { return tableNamePrefix + "trigger" diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 57832d8f1..c8e8b7905 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -346,7 +346,7 @@ type CreateApplicationTriggerRequest struct { Description string `json:"description" optional:"true"` WorkflowName string `json:"workflowName"` Type string `json:"type" validate:"oneof=webhook"` - PayloadType string `json:"payloadType" validate:"oneof=custom acr"` + PayloadType string `json:"payloadType" validate:"checkpayloadtype"` ComponentName string `json:"componentName,omitempty" optional:"true"` } @@ -400,6 +400,36 @@ type ACRRepository struct { RepoType string `json:"repo_type"` } +// HandleApplicationHarborReq handles application trigger harbor request +type HandleApplicationHarborReq struct { + Type string `json:"type"` + OccurAt int64 `json:"occur_at"` + Operator string `json:"operator"` + EventData EventData `json:"event_data"` +} + +// Resources is the image info of harbor +type Resources struct { + Digest string `json:"digest"` + Tag string `json:"tag"` + ResourceURL string `json:"resource_url"` +} + +// Repository is the repository of harbor +type Repository struct { + DateCreated int64 `json:"date_created"` + Name string `json:"name"` + Namespace string `json:"namespace"` + RepoFullName string `json:"repo_full_name"` + RepoType string `json:"repo_type"` +} + +// EventData is the event info of harbor +type EventData struct { + Resources []Resources `json:"resources"` + Repository Repository `json:"repository"` +} + // EnvBinding application env binding type EnvBinding struct { Name string `json:"name" validate:"checkname"` diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 9f44e037e..ab0baa779 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -370,9 +370,6 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis // CreateApplicationTrigger create application trigger func (c *applicationUsecaseImpl) CreateApplicationTrigger(ctx context.Context, app *model.Application, req apisv1.CreateApplicationTriggerRequest) (*apisv1.ApplicationTriggerBase, error) { - if (req.PayloadType == model.PayloadTypeACR || req.PayloadType == model.PayloadTypeDockerhub) && req.ComponentName == "" { - return nil, bcode.ErrApplicationComponetNotExist - } trigger := &model.ApplicationTrigger{ AppPrimaryKey: app.Name, WorkflowName: req.WorkflowName, diff --git a/pkg/apiserver/rest/usecase/webhook.go b/pkg/apiserver/rest/usecase/webhook.go index ba4443cb6..1db8143e4 100644 --- a/pkg/apiserver/rest/usecase/webhook.go +++ b/pkg/apiserver/rest/usecase/webhook.go @@ -60,6 +60,7 @@ func NewWebhookUsecase(ds datastore.DataStore, func registerHandlers() { new(customHandlerImpl).install() new(acrHandlerImpl).install() + new(harborHandlerImpl).install() } type webhookHandler interface { @@ -132,6 +133,11 @@ func (c *webhookUsecaseImpl) HandleApplicationWebhook(ctx context.Context, token if err != nil { return nil, err } + case model.PayloadTypeHarbor: + handler, err = c.newHarborHandler(req) + if err != nil { + return nil, err + } default: return nil, bcode.ErrInvalidWebhookPayloadType } @@ -252,3 +258,77 @@ func parseTimeString(t string) time.Time { } return parsedTime } + +type harborHandlerImpl struct { + req apisv1.HandleApplicationHarborReq + w *webhookUsecaseImpl +} + +func (c *webhookUsecaseImpl) newHarborHandler(req *restful.Request) (webhookHandler, error) { + var harborReq apisv1.HandleApplicationHarborReq + if err := req.ReadEntity(&harborReq); err != nil { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + if harborReq.Type != model.HarborEventTypePushArtifact { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + return &harborHandlerImpl{ + req: harborReq, + w: c, + }, nil +} + +func (c *harborHandlerImpl) install() { + WebhookHandlers = append(WebhookHandlers, model.PayloadTypeHarbor) +} + +func (c *harborHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (*apisv1.ApplicationDeployResponse, error) { + resources := c.req.EventData.Resources + if len(resources) < 1 { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + imageURL := resources[0].ResourceURL + digest := resources[0].Digest + tag := resources[0].Tag + comp := &model.ApplicationComponent{ + AppPrimaryKey: webhookTrigger.AppPrimaryKey, + } + comps, err := c.w.ds.List(ctx, comp, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if len(comps) == 0 { + return nil, bcode.ErrApplicationComponetNotExist + } + + // use the first component as the target component + component := comps[0].(*model.ApplicationComponent) + harborReq := c.req + if err := c.w.patchComponentProperties(ctx, component, &runtime.RawExtension{ + Raw: []byte(fmt.Sprintf(`{"image": "%s"}`, imageURL)), + }); err != nil { + return nil, err + } + return c.w.applicationUsecase.Deploy(ctx, app, apisv1.ApplicationDeployRequest{ + WorkflowName: webhookTrigger.WorkflowName, + Note: "triggered by webhook harbor", + TriggerType: apisv1.TriggerTypeWebhook, + Force: true, + ImageInfo: &model.ImageInfo{ + Type: model.PayloadTypeHarbor, + Resource: &model.ImageResource{ + Digest: digest, + Tag: tag, + URL: imageURL, + CreateTime: time.Unix(harborReq.OccurAt, 0), + }, + Repository: &model.ImageRepository{ + Name: harborReq.EventData.Repository.Name, + Namespace: harborReq.EventData.Repository.Namespace, + FullName: harborReq.EventData.Repository.RepoFullName, + Type: harborReq.EventData.Repository.RepoType, + CreateTime: time.Unix(harborReq.EventData.Repository.DateCreated, 0), + }, + }, + }) +} diff --git a/pkg/apiserver/rest/usecase/webhook_test.go b/pkg/apiserver/rest/usecase/webhook_test.go index dd976ad86..ac30b8908 100644 --- a/pkg/apiserver/rest/usecase/webhook_test.go +++ b/pkg/apiserver/rest/usecase/webhook_test.go @@ -154,12 +154,6 @@ var _ = Describe("Test application usecase function", func() { Expect(revision.CodeInfo.User).Should(Equal("test-user")) By("Test HandleApplicationWebhook function with ACR payload") - _, err = appUsecase.CreateApplicationTrigger(context.TODO(), appModel, apisv1.CreateApplicationTriggerRequest{ - Name: "test-acr", - PayloadType: "acr", - Type: "webhook", - }) - Expect(err).Should(Equal(bcode.ErrApplicationComponetNotExist)) acrTrigger, err := appUsecase.CreateApplicationTrigger(context.TODO(), appModel, apisv1.CreateApplicationTriggerRequest{ Name: "test-acr", PayloadType: "acr", @@ -191,5 +185,43 @@ var _ = Describe("Test application usecase function", func() { comp, err = appUsecase.GetApplicationComponent(context.TODO(), appModel, "component-name-webhook") Expect(err).Should(BeNil()) Expect((*comp.Properties)["image"]).Should(Equal("registry.test-region.aliyuncs.com/test-namespace/test-repo:test-tag")) + + By("Test HandleApplicationWebhook function with harbor payload") + harborTrigger, err := appUsecase.CreateApplicationTrigger(context.TODO(), appModel, apisv1.CreateApplicationTriggerRequest{ + Name: "test-harbor", + PayloadType: "harbor", + Type: "webhook", + ComponentName: "component-name-webhook", + }) + Expect(err).Should(BeNil()) + + harborBody := apisv1.HandleApplicationHarborReq{ + Type: model.HarborEventTypePushArtifact, + EventData: apisv1.EventData{ + Resources: []apisv1.Resources{ + { + Digest: "test-digest", + Tag: "test-tag", + ResourceURL: "harbor.server/test-pro/test-repo:test-tag", + }, + }, + Repository: apisv1.Repository{ + Name: "test-repo", + Namespace: "test-namespace", + RepoFullName: "test-pro/test-repo", + RepoType: "public", + }, + }, + } + body, err = json.Marshal(harborBody) + Expect(err).Should(BeNil()) + httpreq, err = http.NewRequest("post", "/", bytes.NewBuffer(body)) + httpreq.Header.Add(restful.HEADER_ContentType, "application/json") + Expect(err).Should(BeNil()) + _, err = webhookUsecase.HandleApplicationWebhook(context.TODO(), harborTrigger.Token, restful.NewRequest(httpreq)) + Expect(err).Should(BeNil()) + comp, err = appUsecase.GetApplicationComponent(context.TODO(), appModel, "component-name-webhook") + Expect(err).Should(BeNil()) + Expect((*comp.Properties)["image"]).Should(Equal("harbor.server/test-pro/test-repo:test-tag")) }) }) diff --git a/pkg/apiserver/rest/webservice/validate.go b/pkg/apiserver/rest/webservice/validate.go index f5714e80b..9a4691293 100644 --- a/pkg/apiserver/rest/webservice/validate.go +++ b/pkg/apiserver/rest/webservice/validate.go @@ -20,6 +20,8 @@ import ( "regexp" "github.com/go-playground/validator/v10" + + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" ) var validate = validator.New() @@ -38,6 +40,20 @@ func init() { if err := validate.RegisterValidation("checkalias", ValidateAlias); err != nil { panic(err) } + if err := validate.RegisterValidation("checkpayloadtype", ValidatePayloadType); err != nil { + panic(err) + } +} + +// ValidatePayloadType check PayloadType +func ValidatePayloadType(fl validator.FieldLevel) bool { + value := fl.Field().String() + for _, v := range usecase.WebhookHandlers { + if v == value { + return true + } + } + return false } // ValidateName custom check name field