diff --git a/agent/container/api/agent.gen.go b/agent/container/api/agent.gen.go index 9b7b5ec0..11893237 100644 --- a/agent/container/api/agent.gen.go +++ b/agent/container/api/agent.gen.go @@ -22,9 +22,12 @@ type ServerInterface interface { // List of APIs provided by the service // (GET /api-docs) GetApiDocs(w http.ResponseWriter, r *http.Request) - // Post Docker artifactory events - // (POST /event/docker) - PostEventDocker(w http.ResponseWriter, r *http.Request) + // Post github Docker artifactory events + // (POST /event/docker/github) + PostEventDockerGithub(w http.ResponseWriter, r *http.Request) + // Post Dockerhub artifactory events + // (POST /event/docker/hub) + PostEventDockerHub(w http.ResponseWriter, r *http.Request) // Kubernetes readiness and liveness probe endpoint // (GET /status) GetStatus(w http.ResponseWriter, r *http.Request) @@ -54,12 +57,27 @@ func (siw *ServerInterfaceWrapper) GetApiDocs(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r.WithContext(ctx)) } -// PostEventDocker operation middleware -func (siw *ServerInterfaceWrapper) PostEventDocker(w http.ResponseWriter, r *http.Request) { +// PostEventDockerGithub operation middleware +func (siw *ServerInterfaceWrapper) PostEventDockerGithub(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PostEventDocker(w, r) + siw.Handler.PostEventDockerGithub(w, r) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// PostEventDockerHub operation middleware +func (siw *ServerInterfaceWrapper) PostEventDockerHub(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostEventDockerHub(w, r) }) for _, middleware := range siw.HandlerMiddlewares { @@ -201,7 +219,10 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Get(options.BaseURL+"/api-docs", wrapper.GetApiDocs) }) r.Group(func(r chi.Router) { - r.Post(options.BaseURL+"/event/docker", wrapper.PostEventDocker) + r.Post(options.BaseURL+"/event/docker/github", wrapper.PostEventDockerGithub) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/event/docker/hub", wrapper.PostEventDockerHub) }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/status", wrapper.GetStatus) @@ -213,12 +234,13 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5ySz2obQQyHX0XovLWd9ja30IQSUkioews5zM5oHdH1aJC0C8bsu5dZQynpX3IaBD99", - "+jTojEmOVQoVNwznpUMug2A4YyZLytVZCgb8KMUjF1LolfOB4KFSgS+3+69w/XgHVinxwCmu8Q6dfaR/", - "t+1ftc2kdpl3tdltdrh0KJVKrIwBP2x2myvssEZ/aa64jZXfZUlrcSBvj1TSlXaXMeAn8uvKNy3SoZJV", - "KUZr/P1u9+uSD/e4LB3adDxGPWHAz2wOMjRXg6oyc6YM/Qn8hcBIZ07Uto0Hw/CEdepHTvjcIFuaqfg2", - "S/pG2kZVsd8YPor5bUveXIJv0mwQuAAgqvMQk4ueYFWwPwmaR5/++nf7S+J/nGxKicyGaYQfmFeW91NP", - "WsjJQClmLmQGsWQYeaa1qCo9AZVchYv/7K08R6cm3pCk7VAwPJ1x0hEDbnF5Xr4HAAD//4PQ6B3LAgAA", + "H4sIAAAAAAAC/6TSwWrcMBAG4FcZ5uzuOu1Nt9CENKSQ0O0t5CBL492hXknMjA3L4ncvsqGUtLTb5mQE", + "//zzCeuMIR9LTpRM0Z3nBjn1Gd0ZI2kQLsY5ocOPOZnnRAKdcNwTPBZK8OV29xWun+5BCwXuOfgl3qCx", + "DfT3sd2rsYlE131Xm3bT4txgLpR8YXT4YdNurrDB4u1Qrbj1hd/FHJbDnqx+ciFZ2u4jOrwjuy58UyMN", + "CmnJSWmJv2/bXy/5+IDz3KCOx6OXEzr8zGqQ+2pVKJInjhShO4EdCJRk4kD1tn6v6J6xjN3AAV9qyZYm", + "SraNOXwj2e7ZDmNXN5asv4E+ZbXbOnCz5O/W+H+ZaxWs+2BtAy/GvQ+W5QQLSy9C/4v405u4a0cVX05V", + "8zb+8dfv1sQlLB1DINV+HOBHzSvow9iRJDJSEPKRE6mCTxEGnmg5FMkdAaVYMif72S08eaMKr5Uk9Z2j", + "ez7jKAM63OL8Mn8PAAD//+plWf+KAwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/agent/container/cfg.yaml b/agent/container/cfg.yaml index a06beeb5..de91ea73 100644 --- a/agent/container/cfg.yaml +++ b/agent/container/cfg.yaml @@ -3,4 +3,4 @@ generate: chi-server: true models: true embedded-spec: true -output: agent/api/agent.gen.go +output: agent/container/api/agent.gen.go diff --git a/agent/container/openapi.yaml b/agent/container/openapi.yaml index b9642321..6c22238b 100755 --- a/agent/container/openapi.yaml +++ b/agent/container/openapi.yaml @@ -28,11 +28,22 @@ paths: '200': description: OK - /event/docker: + /event/docker/hub: post: tags: - public - summary: Post Docker artifactory events + summary: Post Dockerhub artifactory events responses: '200': description: OK + + /event/docker/github: + post: + tags: + - public + summary: Post github Docker artifactory events + responses: + '200': + description: OK + +# oapi-codegen -config ./cfg.yaml ./openapi.yaml diff --git a/agent/container/pkg/application/application.go b/agent/container/pkg/application/application.go index 2c617e20..1cd52baf 100755 --- a/agent/container/pkg/application/application.go +++ b/agent/container/pkg/application/application.go @@ -40,7 +40,10 @@ func New() *Application { mux := chi.NewMux() apiServer.BindRequest(mux) - + chi.Walk(mux, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + fmt.Printf("[%s]: '%s' has %d middlewares.\n", method, route, len(middlewares)) + return nil + }) httpServer := &http.Server{ // TODO: remove hardcoding // Addr: fmt.Sprintf("0.0.0.0:%d", cfg.Port), diff --git a/agent/container/pkg/handler/docker_event_api_handler.go b/agent/container/pkg/handler/docker_event_api_handler.go deleted file mode 100644 index 317e2e44..00000000 --- a/agent/container/pkg/handler/docker_event_api_handler.go +++ /dev/null @@ -1,20 +0,0 @@ -package handler - -import ( - "io" - "log" - "net/http" -) - -func (ah *APIHandler) PostEventDocker(w http.ResponseWriter, r *http.Request) { - event, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Event body read failed: %v", err) - } - - log.Printf("Received event from docker artifactory: %v", string(event)) - err = ah.conn.Publish(event, "docker registry") - if err != nil { - log.Printf("Publish failed for event: %v, reason: %v", string(event), err) - } -} diff --git a/agent/container/pkg/handler/docker_event_dockerhub.go b/agent/container/pkg/handler/docker_event_dockerhub.go new file mode 100644 index 00000000..0d6d0e08 --- /dev/null +++ b/agent/container/pkg/handler/docker_event_dockerhub.go @@ -0,0 +1,32 @@ +package handler + +import ( + "errors" + "io" + "log" + "net/http" +) + +// parse errors +var ( + ErrReadingBody = errors.New("error reading the request body") + ErrPublishToNats = errors.New("error while publishing to nats") +) + +func (ah *APIHandler) PostEventDockerHub(w http.ResponseWriter, r *http.Request) { + defer func() { + _, _ = io.Copy(io.Discard, r.Body) + _ = r.Body.Close() + }() + payload, err := io.ReadAll(r.Body) + if err != nil || len(payload) == 0 { + log.Printf("%v: %v", ErrReadingBody, err) + return + } + log.Printf("Received event from docker artifactory: %v", string(payload)) + err = ah.conn.Publish(payload, "Dockerhub_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + return + } +} diff --git a/agent/container/pkg/handler/docker_event_github.go b/agent/container/pkg/handler/docker_event_github.go new file mode 100644 index 00000000..1dda3843 --- /dev/null +++ b/agent/container/pkg/handler/docker_event_github.go @@ -0,0 +1,37 @@ +package handler + +import ( + "errors" + "io" + "log" + "net/http" +) + +var ( + ErrMissingGithubEventHeader = errors.New("missing X-GitHub-Event Header") +) + +func (ah *APIHandler) PostEventDockerGithub(w http.ResponseWriter, r *http.Request) { + defer func() { + _, _ = io.Copy(io.Discard, r.Body) + _ = r.Body.Close() + }() + event := r.Header.Get("X-GitHub-Event") + if event == "" { + log.Printf("%v", ErrMissingGithubEventHeader) + return + } + + payload, err := io.ReadAll(r.Body) + if err != nil || len(payload) == 0 { + log.Printf("%v: %v", ErrReadingBody, err) + return + } + + log.Printf("Received docker event from github artifactory: %v", string(payload)) + err = ah.conn.Publish(payload, "Github_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + return + } +} diff --git a/client/pkg/clickhouse/db_client.go b/client/pkg/clickhouse/db_client.go index 13fbc3e1..213facd3 100644 --- a/client/pkg/clickhouse/db_client.go +++ b/client/pkg/clickhouse/db_client.go @@ -28,12 +28,13 @@ type DBInterface interface { InsertDeletedAPI(model.DeletedAPI) InsertKubvizEvent(model.Metrics) InsertGitEvent(string) - InsertContainerEvent(string) InsertKubeScoreMetrics(model.KubeScoreRecommendations) RetriveKetallEvent() ([]model.Resource, error) RetriveOutdatedEvent() ([]model.CheckResultfinal, error) RetriveKubepugEvent() ([]model.Result, error) RetrieveKubvizEvent() ([]model.DbEvent, error) + InsertContainerEventDockerHub(model.DockerHubBuild) + InsertContainerEventGithub(string) Close() } @@ -62,7 +63,7 @@ func NewDBClient(conf *config.Config) (DBInterface, error) { } return nil, err } - tables := []DBStatement{kubvizTable, rakeesTable, kubePugDepricatedTable, kubepugDeletedTable, ketallTable, outdateTable, clickhouseExperimental, containerTable, gitTable, kubescoreTable} + tables := []DBStatement{kubvizTable, rakeesTable, kubePugDepricatedTable, kubepugDeletedTable, ketallTable, outdateTable, clickhouseExperimental, containerDockerhubTable, containerGithubTable, gitTable, kubescoreTable, dockerHubBuildTable} for _, table := range tables { if err = splconn.Exec(context.Background(), string(table)); err != nil { return nil, err @@ -377,3 +378,40 @@ func (c *DBClient) RetrieveKubvizEvent() ([]model.DbEvent, error) { } return events, nil } + +func (c *DBClient) InsertContainerEventDockerHub(build model.DockerHubBuild) { + var ( + tx, _ = c.conn.Begin() + stmt, _ = tx.Prepare(string(InsertDockerHubBuild)) + ) + defer stmt.Close() + if _, err := stmt.Exec( + build.PushedBy, + build.ImageTag, + build.RepositoryName, + build.DateCreated, + build.Owner, + build.Event, + ); err != nil { + log.Fatal(err) + } + if err := tx.Commit(); err != nil { + log.Fatal(err) + } +} + +func (c *DBClient) InsertContainerEventGithub(event string) { + ctx := context.Background() + batch, err := c.splconn.PrepareBatch(ctx, "INSERT INTO container_github") + if err != nil { + log.Fatal(err) + } + + if err = batch.Append(event); err != nil { + log.Fatal(err) + } + + if err = batch.Send(); err != nil { + log.Fatal(err) + } +} diff --git a/client/pkg/clickhouse/statements.go b/client/pkg/clickhouse/statements.go index 30fd17b6..37783859 100644 --- a/client/pkg/clickhouse/statements.go +++ b/client/pkg/clickhouse/statements.go @@ -79,6 +79,18 @@ const kubescoreTable DBStatement = ` recommendations String ) engine=File(TabSeparated) ` + +const dockerHubBuildTable DBStatement = ` + CREATE TABLE IF NOT EXISTS dockerhubbuild ( + PushedBy String, + ImageTag String, + RepositoryName String, + DateCreated String, + Owner String, + Event String + ) engine=File(TabSeparated) + ` +const InsertDockerHubBuild DBStatement = "INSERT INTO dockerhubbuild (PushedBy, ImageTag, RepositoryName, DateCreated, Owner, Event) VALUES (?, ?, ?, ?, ?, ?)" const InsertRakees DBStatement = "INSERT INTO rakkess (ClusterName, Name, Create, Delete, List, Update) VALUES (?, ?, ?, ?, ?, ?)" const InsertKetall DBStatement = "INSERT INTO getall_resources (ClusterName, Namespace, Kind, Resource, Age) VALUES (?, ?, ?, ?, ?)" const InsertOutdated DBStatement = "INSERT INTO outdated_images (ClusterName, Namespace, Pod, CurrentImage, CurrentTag, LatestVersion, VersionsBehind) VALUES (?, ?, ?, ?, ?, ?, ?)" @@ -86,6 +98,7 @@ const InsertDepricatedApi DBStatement = "INSERT INTO DeprecatedAPIs (ClusterName const InsertDeletedApi DBStatement = "INSERT INTO DeletedAPIs (ClusterName, ObjectName, Group, Kind, Version, Name, Deleted, Scope) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" const InsertKubvizEvent DBStatement = "INSERT INTO events (ClusterName, Id, EventTime, OpType, Name, Namespace, Kind, Message, Reason, Host, Event, FirstTime, LastTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" const clickhouseExperimental DBStatement = `SET allow_experimental_object_type=1;` -const containerTable DBStatement = `CREATE table IF NOT EXISTS container_bridge(event JSON) ENGINE = MergeTree ORDER BY tuple();` +const containerDockerhubTable DBStatement = `CREATE table IF NOT EXISTS container_dockerhub(event JSON) ENGINE = MergeTree ORDER BY tuple();` const gitTable DBStatement = `CREATE table IF NOT EXISTS git_json(event JSON) ENGINE = MergeTree ORDER BY tuple();` +const containerGithubTable DBStatement = `CREATE table IF NOT EXISTS container_github(event JSON) ENGINE = MergeTree ORDER BY tuple();` const InsertKubeScore string = "INSERT INTO kubescore (id, namespace, cluster_name, recommendations) VALUES (?, ?, ?, ?)" diff --git a/client/pkg/clients/container_client.go b/client/pkg/clients/container_client.go index e4c7e1e0..59d511ea 100644 --- a/client/pkg/clients/container_client.go +++ b/client/pkg/clients/container_client.go @@ -2,12 +2,19 @@ package clients import ( "encoding/json" + "errors" "log" + "time" "github.com/intelops/kubviz/client/pkg/clickhouse" + "github.com/intelops/kubviz/model" "github.com/nats-io/nats.go" ) +var ( + ErrUnmarshalBuildPayload = errors.New("error while unmarshal the dockerhub build payload") +) + type Container string // constant variables to use with nats stream and @@ -18,61 +25,30 @@ const ( containerConsumer Container = "container-event-consumer" ) -// func (n *NATSContext) SubscribeContainerNats(conn clickhouse.DBInterface) { -// n.stream.Subscribe(string(containerSubject), func(msg *nats.Msg) { -// type events struct { -// Events []json.RawMessage `json:"events"` -// } - -// eventDocker := &events{} -// err := json.Unmarshal(msg.Data, &eventDocker) -// if err == nil { -// log.Println(eventDocker) -// msg.Ack() -// repoName := msg.Header.Get("REPO_NAME") -// type newEvent struct { -// RepoName string `json:"repoName"` -// Event json.RawMessage `json:"event"` -// } - -// for _, event := range eventDocker.Events { -// event := &newEvent{ -// RepoName: repoName, -// Event: event, -// } - -// eventsJSON, err := json.Marshal(event) -// if err != nil { -// log.Printf("Failed to marshall with repo name going ahead with only event, %v", err) -// eventsJSON = msg.Data -// } -// conn.InsertContainerEvent(string(eventsJSON)) -// } -// } else { -// log.Printf("Failed to unmarshal event, %v", err) -// conn.InsertContainerEvent(string(msg.Data)) -// } - -// log.Println("Inserted metrics:", string(msg.Data)) -// }, nats.Durable(string(containerConsumer)), nats.ManualAck()) -// } func (n *NATSContext) SubscribeContainerNats(conn clickhouse.DBInterface) { n.stream.Subscribe(string(containerSubject), func(msg *nats.Msg) { - type pubData struct { - Metrics json.RawMessage `json:"event"` - Repo string `json:"repoName"` - } msg.Ack() repoName := msg.Header.Get("REPO_NAME") - metrics := &pubData{ - Metrics: json.RawMessage(msg.Data), - Repo: repoName, - } - data, err := json.Marshal(metrics) - if err != nil { - log.Fatal(err) + if repoName == "Dockerhub_Registry" { + var pl model.BuildPayload + err := json.Unmarshal(msg.Data, &pl) + if err != nil { + log.Printf("%v", ErrUnmarshalBuildPayload) + return + } + var hub model.DockerHubBuild + t := time.Unix(int64(pl.Repository.DateCreated), 0) + hub.DateCreated = t.Format("2006-01-02 15:04:05") + hub.PushedBy = pl.PushData.Pusher + hub.ImageTag = pl.PushData.Tag + hub.RepositoryName = pl.Repository.Name + hub.Owner = pl.Repository.Owner + hub.Event = string(msg.Data) + conn.InsertContainerEventDockerHub(hub) + log.Println("Inserted DockerHub Container metrics:", string(msg.Data)) + } else if repoName == "Github_Registry" { + conn.InsertContainerEventGithub(string(msg.Data)) + log.Println("Inserted Github Container metrics:", string(msg.Data)) } - conn.InsertContainerEvent(string(data)) - log.Println("Inserted Container metrics:", string(msg.Data)) }, nats.Durable(string(containerConsumer)), nats.ManualAck()) } diff --git a/model/dockerhub.go b/model/dockerhub.go new file mode 100644 index 00000000..e407b135 --- /dev/null +++ b/model/dockerhub.go @@ -0,0 +1,37 @@ +package model + +type BuildPayload struct { + CallbackURL string `json:"callback_url"` + PushData struct { + Images []string `json:"images"` + PushedAt float32 `json:"pushed_at"` + Pusher string `json:"pusher"` + Tag string `json:"tag"` + } `json:"push_data"` + Repository struct { + CommentCount int `json:"comment_count"` + DateCreated float32 `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"` + StarCount int `json:"star_count"` + Status string `json:"status"` + } `json:"repository"` +} + +type DockerHubBuild struct { + PushedBy string + ImageTag string + RepositoryName string + DateCreated string + Owner string + Event string +} diff --git a/model/github_docker.go b/model/github_docker.go new file mode 100644 index 00000000..e3ee2513 --- /dev/null +++ b/model/github_docker.go @@ -0,0 +1,133 @@ +package model + +import "time" + +type PingPayload struct { + HookID int `json:"hook_id"` + Hook struct { + Type string `json:"type"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + Active bool `json:"active"` + Events []string `json:"events"` + AppID int `json:"app_id"` + Config struct { + ContentType string `json:"content_type"` + InsecureSSL string `json:"insecure_ssl"` + Secret string `json:"secret"` + URL string `json:"url"` + } `json:"config"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"hook"` + Repository struct { + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Owner struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"owner"` + Private bool `json:"private"` + HTMLURL string `json:"html_url"` + Description string `json:"description"` + Fork bool `json:"fork"` + URL string `json:"url"` + ForksURL string `json:"forks_url"` + KeysURL string `json:"keys_url"` + CollaboratorsURL string `json:"collaborators_url"` + TeamsURL string `json:"teams_url"` + HooksURL string `json:"hooks_url"` + IssueEventsURL string `json:"issue_events_url"` + EventsURL string `json:"events_url"` + AssigneesURL string `json:"assignees_url"` + BranchesURL string `json:"branches_url"` + TagsURL string `json:"tags_url"` + BlobsURL string `json:"blobs_url"` + GitTagsURL string `json:"git_tags_url"` + GitRefsURL string `json:"git_refs_url"` + TreesURL string `json:"trees_url"` + StatusesURL string `json:"statuses_url"` + LanguagesURL string `json:"languages_url"` + StargazersURL string `json:"stargazers_url"` + ContributorsURL string `json:"contributors_url"` + SubscribersURL string `json:"subscribers_url"` + SubscriptionURL string `json:"subscription_url"` + CommitsURL string `json:"commits_url"` + GitCommitsURL string `json:"git_commits_url"` + CommentsURL string `json:"comments_url"` + IssueCommentURL string `json:"issue_comment_url"` + ContentsURL string `json:"contents_url"` + CompareURL string `json:"compare_url"` + MergesURL string `json:"merges_url"` + ArchiveURL string `json:"archive_url"` + DownloadsURL string `json:"downloads_url"` + IssuesURL string `json:"issues_url"` + PullsURL string `json:"pulls_url"` + MilestonesURL string `json:"milestones_url"` + NotificationsURL string `json:"notifications_url"` + LabelsURL string `json:"labels_url"` + ReleasesURL string `json:"releases_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt time.Time `json:"pushed_at"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + Homepage *string `json:"homepage"` + Size int64 `json:"size"` + StargazersCount int64 `json:"stargazers_count"` + WatchersCount int64 `json:"watchers_count"` + Language *string `json:"language"` + HasIssues bool `json:"has_issues"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + ForksCount int64 `json:"forks_count"` + MirrorURL *string `json:"mirror_url"` + OpenIssuesCount int64 `json:"open_issues_count"` + Forks int64 `json:"forks"` + OpenIssues int64 `json:"open_issues"` + Watchers int64 `json:"watchers"` + DefaultBranch string `json:"default_branch"` + } `json:"repository"` + Sender struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"sender"` +}