diff --git a/pkg/http/http.go b/pkg/http/http.go index 675b64ae..ef3e8d96 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -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") @@ -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") diff --git a/pkg/http/jfrog_webhook_trigger.go b/pkg/http/jfrog_webhook_trigger.go new file mode 100644 index 00000000..c9350adc --- /dev/null +++ b/pkg/http/jfrog_webhook_trigger.go @@ -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 +} diff --git a/pkg/http/jfrog_webhook_trigger_test.go b/pkg/http/jfrog_webhook_trigger_test.go new file mode 100644 index 00000000..83c606a9 --- /dev/null +++ b/pkg/http/jfrog_webhook_trigger_test.go @@ -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) + } +}