mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
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:
@@ -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{}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user