Skip to content

Commit

Permalink
Merge pull request #697 from blakebarnett/add_jfrog_webhook_support
Browse files Browse the repository at this point in the history
Add support for JFrog container registry webhooks
  • Loading branch information
rusenask authored Mar 5, 2023
2 parents 5c87dee + cf9c3ae commit c1f2993
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (s *TriggerServer) registerWebhookRoutes(mux *mux.Router) {
if s.authenticatedWebhooks {
mux.HandleFunc("/v1/webhooks/native", s.requireAdminAuthorization(s.nativeHandler)).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/dockerhub", s.requireAdminAuthorization(s.dockerHubHandler)).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/jfrog", s.requireAdminAuthorization(s.jfrogHandler)).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/quay", s.requireAdminAuthorization(s.quayHandler)).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/azure", s.requireAdminAuthorization(s.azureHandler)).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/github", s.requireAdminAuthorization(s.githubHandler)).Methods("POST", "OPTIONS")
Expand All @@ -193,6 +194,7 @@ func (s *TriggerServer) registerWebhookRoutes(mux *mux.Router) {
} else {
mux.HandleFunc("/v1/webhooks/native", s.nativeHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/dockerhub", s.dockerHubHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/jfrog", s.jfrogHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/quay", s.quayHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/azure", s.azureHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/github", s.githubHandler).Methods("POST", "OPTIONS")
Expand Down
136 changes: 136 additions & 0 deletions pkg/http/jfrog_webhook_trigger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package http

import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"

"github.com/keel-hq/keel/types"
"github.com/prometheus/client_golang/prometheus"

log "github.com/sirupsen/logrus"
)

const (
EnvPrivateRegistry = "PRIVATE_REGISTRY"
)

var newJfrogWebhooksCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "jfrog_webhook_requests_total",
Help: "How many /v1/webhooks/jfrog requests processed, partitioned by image.",
},
[]string{"image"},
)

func init() {
prometheus.MustRegister(newJfrogWebhooksCounter)
}

/** Example of jfrog trigger
{
"domain": "docker",
"event_type": "pushed",
"data": {
"repo_key":"docker-remote-cache",
"event_type":"pushed",
"path":"library/ubuntu/latest/list.manifest.json",
"name":"list.manifest.json",
"sha256":"35c4a2c15539c6c1e4e5fa4e554dac323ad0107d8eb5c582d6ff386b383b7dce",
"size":1206,
"image_name":"library/ubuntu",
"tag":"latest",
"platforms":[
{
"architecture":"amd64",
"os":"linux"
},
{
"architecture":"arm",
"os":"linux"
},
{
"architecture":"arm64",
"os":"linux"
},
{
"architecture":"ppc64le",
"os":"linux"
},
{
"architecture":"s390x",
"os":"linux"
}
]
},
"subscription_key": "test",
"jpd_origin": "https://example.jfrog.io",
"source": "jfrog/user@example.com"
}
**/

type jfrogWebhook struct {
Domain string `json:"domain"`
EventType string `json:"event_type"`
Data struct {
RepoKey string `json:"repo_key"`
Path string `json:"path"`
Name string `json:"name"`
Sha256 string `json:"sha256"`
Size int32 `json:"size"`
ImageName string `json:"image_name"`
Tag string `json:"tag"`
Platforms []struct {
Architecture string `json:"architecture"`
Os string `json:"os"`
}
}
SubscriptionKey string `json:"subscription_key"`
JpdOrigin string `json:"jpd_origin"`
Source string `json:"source"`
}

func (s *TriggerServer) jfrogHandler(resp http.ResponseWriter, req *http.Request) {
jw := jfrogWebhook{}
if err := json.NewDecoder(req.Body).Decode(&jw); err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("trigger.jfrogHandler: failed to decode request")
resp.WriteHeader(http.StatusBadRequest)
return
}

if jw.Data.ImageName == "" {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "data.image_name cannot be empty")
return
}

if len(jw.Data.Tag) == 0 {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "tag cannot be empty")
return
}

// for every updated tag generating event
event := types.Event{}
event.CreatedAt = time.Now()
event.TriggerName = "jfrog"
event.Repository.Tag = jw.Data.Tag
event.Repository.Name = jw.Data.ImageName
if privReg, ok := os.LookupEnv(EnvPrivateRegistry); ok {
if len(privReg) >= 3 {
event.Repository.Name = fmt.Sprintf("%s/%s", privReg, jw.Data.ImageName)
}
}

log.Infof("Received jfrog webhook for image: %s:%s", jw.Data.ImageName, jw.Data.Tag)
log.Debug("jfrogWebhook data: ", jw)
s.trigger(event)
newJfrogWebhooksCounter.With(prometheus.Labels{"image": event.Repository.Name}).Inc()

resp.WriteHeader(http.StatusOK)
return
}
90 changes: 90 additions & 0 deletions pkg/http/jfrog_webhook_trigger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package http

import (
"bytes"
"fmt"
"net/http"
"os"

"net/http/httptest"
"testing"
)

var fakeJfrogWebhook = `{
"domain": "docker",
"event_type": "pushed",
"data": {
"repo_key":"docker-remote-cache",
"event_type":"pushed",
"path":"library/ubuntu/latest/list.manifest.json",
"name":"list.manifest.json",
"sha256":"35c4a2c15539c6c1e4e5fa4e554dac323ad0107d8eb5c582d6ff386b383b7dce",
"size":1206,
"image_name":"library/ubuntu",
"tag":"latest",
"platforms":[
{
"architecture":"amd64",
"os":"linux"
},
{
"architecture":"arm",
"os":"linux"
},
{
"architecture":"arm64",
"os":"linux"
},
{
"architecture":"ppc64le",
"os":"linux"
},
{
"architecture":"s390x",
"os":"linux"
}
]
},
"subscription_key": "test",
"jpd_origin": "https://example.jfrog.io",
"source": "jfrog/user@example.com"
}`

func TestJfrogWebhookHandler(t *testing.T) {

fp := &fakeProvider{}
srv, teardown := NewTestingServer(fp)
defer teardown()

req, err := http.NewRequest("POST", "/v1/webhooks/jfrog", bytes.NewBuffer([]byte(fakeJfrogWebhook)))
if err != nil {
t.Fatalf("failed to create req: %s", err)
}

//The response recorder used to record HTTP responses
rec := httptest.NewRecorder()

srv.router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("unexpected status code: %d", rec.Code)

t.Log(rec.Body.String())
}

if len(fp.submitted) != 1 {
t.Fatalf("unexpected number of events submitted: %d", len(fp.submitted))
}

expected_repo_name := "library/ubuntu"
if pr, ok := os.LookupEnv("PRIVATE_REGISTRY"); ok {
expected_repo_name = fmt.Sprintf("%s/%s", pr, "library/ubuntu")
}

if fp.submitted[0].Repository.Name != expected_repo_name {
t.Errorf("expected %s but got %s", expected_repo_name, fp.submitted[0].Repository.Name)
}

if fp.submitted[0].Repository.Tag != "latest" {
t.Errorf("expected latest but got %s", fp.submitted[0].Repository.Tag)
}
}

0 comments on commit c1f2993

Please sign in to comment.