diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 31e2f82cd..bfbee6b2e 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -324,6 +324,8 @@ const ( PayloadTypeACR = "acr" // PayloadTypeHarbor is the payload type harbor PayloadTypeHarbor = "harbor" + // PayloadTypeJFrog is the payload type jfrog + PayloadTypeJFrog = "jfrog" // ComponentTypeWebservice is the component type webservice ComponentTypeWebservice = "webservice" @@ -336,6 +338,10 @@ const ( const ( // HarborEventTypePushArtifact is the event type PUSH_ARTIFACT HarborEventTypePushArtifact = "PUSH_ARTIFACT" + // JFrogEventTypePush is push event type of jfrog webhook + JFrogEventTypePush = "pushed" + // JFrogDomainDocker is webhook domain of jfrog docker + JFrogDomainDocker = "docker" ) // TableName return custom table name diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 126bb9e41..b122bf60d 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -464,6 +464,24 @@ type DockerHubRepository struct { Status string `json:"status"` } +// HandleApplicationTriggerJFrogRequest application trigger JFrog webhook request +type HandleApplicationTriggerJFrogRequest struct { + Domain string `json:"domain"` + EventType string `json:"event_type"` + Data JFrogWebhookData `json:"data"` +} + +// JFrogWebhookData is the data of JFrog webhook request +type JFrogWebhookData struct { + URL string + ImageName string `json:"image_name"` + Name string `json:"name"` + Path string `json:"path"` + RepoKey string `json:"repo_key"` + Digest string `json:"sha256"` + Tag string `json:"tag"` +} + // EnvBinding application env binding type EnvBinding struct { Name string `json:"name" validate:"checkname"` diff --git a/pkg/apiserver/rest/usecase/webhook.go b/pkg/apiserver/rest/usecase/webhook.go index 9dc485f8d..1a9101e50 100644 --- a/pkg/apiserver/rest/usecase/webhook.go +++ b/pkg/apiserver/rest/usecase/webhook.go @@ -62,6 +62,7 @@ func registerHandlers() { new(acrHandlerImpl).install() new(dockerHubHandlerImpl).install() new(harborHandlerImpl).install() + new(jfrogHandlerImpl).install() } type webhookHandler interface { @@ -160,6 +161,11 @@ func (c *webhookUsecaseImpl) HandleApplicationWebhook(ctx context.Context, token if err != nil { return nil, err } + case model.PayloadTypeJFrog: + handler, err = c.newJFrogHandler(req) + if err != nil { + return nil, err + } default: return nil, bcode.ErrInvalidWebhookPayloadType } @@ -423,3 +429,74 @@ func (c *harborHandlerImpl) handle(ctx context.Context, webhookTrigger *model.Ap }, }) } + +type jfrogHandlerImpl struct { + req apisv1.HandleApplicationTriggerJFrogRequest + w *webhookUsecaseImpl +} + +func (c *webhookUsecaseImpl) newJFrogHandler(req *restful.Request) (webhookHandler, error) { + var jfrogReq apisv1.HandleApplicationTriggerJFrogRequest + if err := req.ReadEntity(&jfrogReq); err != nil { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + if jfrogReq.Domain != model.JFrogDomainDocker || jfrogReq.EventType != model.JFrogEventTypePush { + return nil, bcode.ErrInvalidWebhookPayloadBody + } + // jfrog should use request header to give URL, it is not exist in request body + jfrogReq.Data.URL = req.HeaderParameter("imageURL") + return &jfrogHandlerImpl{ + req: jfrogReq, + w: c, + }, nil +} + +func (j *jfrogHandlerImpl) handle(ctx context.Context, webhookTrigger *model.ApplicationTrigger, app *model.Application) (interface{}, error) { + jfrogReq := j.req + comp := &model.ApplicationComponent{ + AppPrimaryKey: webhookTrigger.AppPrimaryKey, + } + comps, err := j.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("%s/%s:%s", jfrogReq.Data.RepoKey, jfrogReq.Data.ImageName, jfrogReq.Data.Tag) + if jfrogReq.Data.URL != "" { + image = fmt.Sprintf("%s/%s", jfrogReq.Data.URL, image) + } + if err := j.w.patchComponentProperties(ctx, component, &runtime.RawExtension{ + Raw: []byte(fmt.Sprintf(`{"image": "%s"}`, image)), + }); err != nil { + return nil, err + } + + return j.w.applicationUsecase.Deploy(ctx, app, apisv1.ApplicationDeployRequest{ + WorkflowName: webhookTrigger.WorkflowName, + Note: "triggered by webhook jfrog", + TriggerType: apisv1.TriggerTypeWebhook, + Force: true, + ImageInfo: &model.ImageInfo{ + Type: model.PayloadTypeHarbor, + Resource: &model.ImageResource{ + Digest: jfrogReq.Data.Digest, + Tag: jfrogReq.Data.Tag, + URL: image, + }, + Repository: &model.ImageRepository{ + Name: jfrogReq.Data.ImageName, + Namespace: jfrogReq.Data.RepoKey, + FullName: fmt.Sprintf("%s/%s", jfrogReq.Data.RepoKey, jfrogReq.Data.ImageName), + }, + }, + }) +} + +func (j *jfrogHandlerImpl) install() { + WebhookHandlers = append(WebhookHandlers, model.PayloadTypeJFrog) +} diff --git a/pkg/apiserver/rest/usecase/webhook_test.go b/pkg/apiserver/rest/usecase/webhook_test.go index b973ef3e4..01df4812e 100644 --- a/pkg/apiserver/rest/usecase/webhook_test.go +++ b/pkg/apiserver/rest/usecase/webhook_test.go @@ -256,5 +256,45 @@ 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("docker.io/test-namespace/test-repo:test-tag")) + + By("Test HandleApplicationWebhook function with jfrog payload without header of imageURL") + jfrogTrigger, err := appUsecase.CreateApplicationTrigger(context.TODO(), appModel, apisv1.CreateApplicationTriggerRequest{ + Name: "test-jfrog", + PayloadType: "jfrog", + Type: "webhook", + ComponentName: "component-name-webhook", + }) + Expect(err).Should(BeNil()) + jfrogBody := apisv1.HandleApplicationTriggerJFrogRequest{ + Domain: "docker", + EventType: "pushed", + Data: apisv1.JFrogWebhookData{ + ImageName: "test-image", + RepoKey: "test-repo", + Digest: "test-digest", + Tag: "test-tag", + }, + } + body, err = json.Marshal(jfrogBody) + 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(), jfrogTrigger.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("test-repo/test-image:test-tag")) + + By("Test HandleApplicationWebhook function with jfrog payload with header of imageURL") + httpreq, err = http.NewRequest("post", "/", bytes.NewBuffer(body)) + Expect(err).Should(BeNil()) + httpreq.Header.Add(restful.HEADER_ContentType, "application/json") + httpreq.Header.Add("imageURL", "test-addr") + _, err = webhookUsecase.HandleApplicationWebhook(context.TODO(), jfrogTrigger.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("test-addr/test-repo/test-image:test-tag")) }) })