diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index c8e8b7905..7778db068 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -430,10 +430,44 @@ type EventData struct { Repository Repository `json:"repository"` } +// HandleApplicationTriggerDockerHubRequest application trigger DockerHub webhook request +type HandleApplicationTriggerDockerHubRequest struct { + CallbackURL string `json:"callback_url"` + PushData DockerHubData `json:"push_data"` + Repository DockerHubRepository `json:"repository"` +} + +// DockerHubData is the push data of dockerhub +type DockerHubData struct { + Images []string `json:"images"` + PushedAt int64 `json:"pushed_at"` + Pusher string `json:"pusher"` + Tag string `json:"tag"` +} + +// DockerHubRepository is the repository of dockerhub +type DockerHubRepository struct { + CommentCount int `json:"comment_count"` + DateCreated int64 `json:"date_created"` + Description string `json:"description"` + Dockerfile string `json:"dockerfile"` + FullDescription string `json:"full_description"` + IsOfficial bool `json:"is_official"` + IsPrivate bool `json:"is_private"` + IsTrusted bool `json:"is_trusted"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Owner string `json:"owner"` + RepoName string `json:"repo_name"` + RepoURL string `json:"repo_url"` + StartCount int `json:"star_count"` + Status string `json:"status"` +} + // EnvBinding application env binding type EnvBinding struct { Name string `json:"name" validate:"checkname"` - //TODO: support componentsPatch + // TODO: support componentsPatch } // EnvBindingTarget the target struct in the envbinding base struct @@ -828,6 +862,14 @@ type ApplicationDeployResponse struct { ApplicationRevisionBase } +// ApplicationDockerhubWebhookResponse dockerhub webhook response body +type ApplicationDockerhubWebhookResponse struct { + State string `json:"state,omitempty"` + Description string `json:"description,omitempty"` + Context string `json:"context,omitempty"` + TargetURL string `json:"target_url,omitempty"` +} + // VelaQLViewResponse query response type VelaQLViewResponse map[string]interface{} diff --git a/pkg/apiserver/rest/usecase/webhook.go b/pkg/apiserver/rest/usecase/webhook.go index 1db8143e4..9dc485f8d 100644 --- a/pkg/apiserver/rest/usecase/webhook.go +++ b/pkg/apiserver/rest/usecase/webhook.go @@ -35,7 +35,7 @@ import ( // WebhookUsecase webhook usecase type WebhookUsecase interface { - HandleApplicationWebhook(ctx context.Context, token string, req *restful.Request) (*apisv1.ApplicationDeployResponse, error) + HandleApplicationWebhook(ctx context.Context, token string, req *restful.Request) (interface{}, error) } type webhookUsecaseImpl struct { @@ -60,11 +60,12 @@ func NewWebhookUsecase(ds datastore.DataStore, func registerHandlers() { new(customHandlerImpl).install() new(acrHandlerImpl).install() + new(dockerHubHandlerImpl).install() new(harborHandlerImpl).install() } type webhookHandler interface { - handle(ctx context.Context, trigger *model.ApplicationTrigger, app *model.Application) (*apisv1.ApplicationDeployResponse, error) + handle(ctx context.Context, trigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) install() } @@ -78,6 +79,11 @@ type acrHandlerImpl struct { w *webhookUsecaseImpl } +type dockerHubHandlerImpl struct { + req apisv1.HandleApplicationTriggerDockerHubRequest + w *webhookUsecaseImpl +} + func (c *webhookUsecaseImpl) newCustomHandler(req *restful.Request) (webhookHandler, error) { var webhookReq apisv1.HandleApplicationTriggerWebhookRequest if err := req.ReadEntity(&webhookReq); err != nil { @@ -100,7 +106,18 @@ func (c *webhookUsecaseImpl) newACRHandler(req *restful.Request) (webhookHandler }, nil } -func (c *webhookUsecaseImpl) HandleApplicationWebhook(ctx context.Context, token string, req *restful.Request) (*apisv1.ApplicationDeployResponse, error) { +func (c *webhookUsecaseImpl) newDockerHubHandler(req *restful.Request) (webhookHandler, error) { + var dockerHubReq apisv1.HandleApplicationTriggerDockerHubRequest + if err := req.ReadEntity(&dockerHubReq); err != nil { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + return &dockerHubHandlerImpl{ + req: dockerHubReq, + w: c, + }, nil +} + +func (c *webhookUsecaseImpl) HandleApplicationWebhook(ctx context.Context, token string, req *restful.Request) (interface{}, error) { webhookTrigger := &model.ApplicationTrigger{ Token: token, } @@ -138,6 +155,11 @@ func (c *webhookUsecaseImpl) HandleApplicationWebhook(ctx context.Context, token if err != nil { return nil, err } + case model.PayloadTypeDockerhub: + handler, err = c.newDockerHubHandler(req) + if err != nil { + return nil, err + } default: return nil, bcode.ErrInvalidWebhookPayloadType } @@ -161,7 +183,7 @@ func (c *webhookUsecaseImpl) patchComponentProperties(ctx context.Context, compo return nil } -func (c *customHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (*apisv1.ApplicationDeployResponse, error) { +func (c *customHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) { for comp, properties := range c.req.Upgrade { component := &model.ApplicationComponent{ AppPrimaryKey: webhookTrigger.AppPrimaryKey, @@ -190,7 +212,7 @@ func (c *customHandlerImpl) install() { WebhookHandlers = append(WebhookHandlers, model.PayloadTypeCustom) } -func (c *acrHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (*apisv1.ApplicationDeployResponse, error) { +func (c *acrHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) { comp := &model.ApplicationComponent{ AppPrimaryKey: webhookTrigger.AppPrimaryKey, } @@ -241,6 +263,75 @@ func (c *acrHandlerImpl) install() { WebhookHandlers = append(WebhookHandlers, model.PayloadTypeACR) } +func (c dockerHubHandlerImpl) handle(ctx context.Context, trigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) { + dockerHubReq := c.req + if dockerHubReq.Repository.Status != "Active" { + log.Logger.Debugf("receive dockerhub webhook but not create event: %v", dockerHubReq) + return &apisv1.ApplicationDockerhubWebhookResponse{ + State: "failed", + Description: "not create event", + }, nil + } + + comp := &model.ApplicationComponent{ + AppPrimaryKey: trigger.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) + image := fmt.Sprintf("docker.io/%s:%s", dockerHubReq.Repository.RepoName, dockerHubReq.PushData.Tag) + if err := c.w.patchComponentProperties(ctx, component, &runtime.RawExtension{ + Raw: []byte(fmt.Sprintf(`{"image": "%s"}`, image)), + }); err != nil { + return nil, err + } + + repositoryType := "public" + if dockerHubReq.Repository.IsPrivate { + repositoryType = "private" + } + + if _, err = c.w.applicationUsecase.Deploy(ctx, app, apisv1.ApplicationDeployRequest{ + WorkflowName: trigger.WorkflowName, + Note: "triggered by webhook dockerhub", + TriggerType: apisv1.TriggerTypeWebhook, + Force: true, + ImageInfo: &model.ImageInfo{ + Type: model.PayloadTypeDockerhub, + Resource: &model.ImageResource{ + Tag: dockerHubReq.PushData.Tag, + URL: image, + CreateTime: time.Unix(dockerHubReq.PushData.PushedAt, 0), + }, + Repository: &model.ImageRepository{ + Name: dockerHubReq.Repository.Name, + Namespace: dockerHubReq.Repository.Namespace, + FullName: dockerHubReq.Repository.RepoName, + Type: repositoryType, + CreateTime: time.Unix(dockerHubReq.Repository.DateCreated, 0), + }, + }, + }); err != nil { + return nil, err + } + + return &apisv1.ApplicationDockerhubWebhookResponse{ + State: "success", + Description: fmt.Sprintf("update application %s/%s success", app.Name, component.Name), + }, nil +} + +func (c dockerHubHandlerImpl) install() { + WebhookHandlers = append(WebhookHandlers, model.PayloadTypeDockerhub) +} + func parseTimeString(t string) time.Time { if t == "" { return time.Time{} @@ -282,7 +373,7 @@ 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) { +func (c *harborHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) { resources := c.req.EventData.Resources if len(resources) < 1 { return nil, bcode.ErrInvalidWebhookPayloadBody diff --git a/pkg/apiserver/rest/usecase/webhook_test.go b/pkg/apiserver/rest/usecase/webhook_test.go index ac30b8908..b973ef3e4 100644 --- a/pkg/apiserver/rest/usecase/webhook_test.go +++ b/pkg/apiserver/rest/usecase/webhook_test.go @@ -136,6 +136,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) res, err := webhookUsecase.HandleApplicationWebhook(context.TODO(), triggers[0].Token, restful.NewRequest(httpreq)) Expect(err).Should(BeNil()) + appDeployRes := res.(*apisv1.ApplicationDeployResponse) comp, err := appUsecase.GetApplicationComponent(context.TODO(), appModel, "component-name-webhook") Expect(err).Should(BeNil()) Expect((*comp.Properties)["image"]).Should(Equal("test-image")) @@ -145,7 +146,7 @@ var _ = Describe("Test application usecase function", func() { revision := &model.ApplicationRevision{ AppPrimaryKey: "test-app-webhook", - Version: res.Version, + Version: appDeployRes.Version, } err = webhookUsecase.ds.Get(context.TODO(), revision) Expect(err).Should(BeNil()) @@ -223,5 +224,37 @@ 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("harbor.server/test-pro/test-repo:test-tag")) + + By("Test HandleApplicationWebhook function with dockerhub payload") + dockerhubTrigger, err := appUsecase.CreateApplicationTrigger(context.TODO(), appModel, apisv1.CreateApplicationTriggerRequest{ + Name: "test-dockerhub", + PayloadType: "dockerhub", + Type: "webhook", + ComponentName: "component-name-webhook", + }) + Expect(err).Should(BeNil()) + + dockerhubBody := apisv1.HandleApplicationTriggerDockerHubRequest{ + PushData: apisv1.DockerHubData{ + Tag: "test-tag", + }, + Repository: apisv1.DockerHubRepository{ + IsPrivate: true, + Name: "test-repo", + Namespace: "test-namespace", + RepoName: "test-namespace/test-repo", + Status: "Active", + }, + } + body, err = json.Marshal(dockerhubBody) + 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(), dockerhubTrigger.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("docker.io/test-namespace/test-repo:test-tag")) }) })