Feat: add DockerHub webhook trigger (#3081)

* add dockerhub webhook to update application image

Signed-off-by: chwetion <chwetion@foxmail.com>

* fix e2e webhook response return value

Signed-off-by: chwetion <chwetion@foxmail.com>

* add e2e for dockerhub webhook

Signed-off-by: chwetion <chwetion@foxmail.com>

Co-authored-by: chwetion <chwetion@foxmail.com>
This commit is contained in:
chwetion
2022-01-12 11:37:06 +08:00
committed by GitHub
co-authored by chwetion
parent f41e0a3bb5
commit bfbdb85503
3 changed files with 174 additions and 8 deletions
+43 -1
View File
@@ -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{}
+97 -6
View File
@@ -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
+34 -1
View File
@@ -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"))
})
})