From ffc9929c1a73ffcaa1dd97f07b975f33752aa02b Mon Sep 17 00:00:00 2001 From: Catalin Hofnar Date: Wed, 5 Oct 2022 22:56:41 +0300 Subject: [PATCH] feat(GraphQL): playground, served by zot in specific binary (#753) Signed-off-by: Catalin Hofnar --- examples/config-search.json | 21 ++++++ pkg/api/routes.go | 2 + pkg/debug/constants/consts.go | 6 ++ pkg/debug/gqlplayground/gqlplayground.go | 53 ++++++++++++++ .../gqlplayground/gqlplayground_disabled.go | 19 +++++ pkg/debug/gqlplayground/index.html.tmpl | 67 ++++++++++++++++++ .../search/gql_generated/generated.go | 70 +++++++++++++++++++ .../search/gql_generated/models_gen.go | 9 +++ pkg/extensions/search/schema.graphql | 70 +++++++++++++++++++ 9 files changed, 317 insertions(+) create mode 100644 examples/config-search.json create mode 100644 pkg/debug/constants/consts.go create mode 100644 pkg/debug/gqlplayground/gqlplayground.go create mode 100644 pkg/debug/gqlplayground/gqlplayground_disabled.go create mode 100644 pkg/debug/gqlplayground/index.html.tmpl diff --git a/examples/config-search.json b/examples/config-search.json new file mode 100644 index 00000000..8efd6f5e --- /dev/null +++ b/examples/config-search.json @@ -0,0 +1,21 @@ +{ + "distSpecVersion": "1.0.1-dev", + "storage": { + "rootDirectory": "/tmp/zot" + }, + "http": { + "address": "127.0.0.1", + "port": "5000" + }, + "log": { + "level": "debug" + }, + "extensions": { + "search": { + "enable": true, + "cve": { + "updateInterval": "24h" + } + } + } +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 16b5566a..8794a605 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -27,6 +27,7 @@ import ( artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" + gqlPlayground "zotregistry.io/zot/pkg/debug/gqlplayground" debug "zotregistry.io/zot/pkg/debug/swagger" ext "zotregistry.io/zot/pkg/extensions" "zotregistry.io/zot/pkg/log" @@ -117,6 +118,7 @@ func (rh *RouteHandler) SetupRoutes() { // extended build ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) ext.SetupSearchRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) + gqlPlayground.SetupGQLPlaygroundRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) } } } diff --git a/pkg/debug/constants/consts.go b/pkg/debug/constants/consts.go new file mode 100644 index 00000000..9faa9427 --- /dev/null +++ b/pkg/debug/constants/consts.go @@ -0,0 +1,6 @@ +package constants + +const ( + Debug = "/_zot/debug" + GQLPlaygroundEndpoint = Debug + "/graphql-playground" +) diff --git a/pkg/debug/gqlplayground/gqlplayground.go b/pkg/debug/gqlplayground/gqlplayground.go new file mode 100644 index 00000000..767d5313 --- /dev/null +++ b/pkg/debug/gqlplayground/gqlplayground.go @@ -0,0 +1,53 @@ +//go:build debug +// +build debug + +package debug + +import ( + "embed" + "html/template" + "net/http" + + "github.com/gorilla/mux" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/api/constants" + debugCst "zotregistry.io/zot/pkg/debug/constants" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +//go:embed index.html.tmpl +var playgroundHTML embed.FS + +// SetupGQLPlaygroundRoutes ... +func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router, + storeController storage.StoreController, l log.Logger, +) { + log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()} + log.Info().Msg("setting up graphql playground route") + + templ, err := template.ParseFS(playgroundHTML, "gqlplayground/index.html.tmpl") + if err != nil { + log.Fatal().Err(err) + } + + //nolint:lll + router.PathPrefix(constants.RoutePrefix + debugCst.GQLPlaygroundEndpoint).HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Add("Content-Type", "text/html") + + proto := "" + + if req.TLS == nil { + proto += "http://" + } else { + proto += "https://" + } + + target := proto + req.Host + constants.ExtSearchPrefix + + // respond with the output of template execution + _ = templ.Execute(writer, struct { + Target string + }{Target: target}) + }) +} diff --git a/pkg/debug/gqlplayground/gqlplayground_disabled.go b/pkg/debug/gqlplayground/gqlplayground_disabled.go new file mode 100644 index 00000000..d266ccce --- /dev/null +++ b/pkg/debug/gqlplayground/gqlplayground_disabled.go @@ -0,0 +1,19 @@ +//go:build !debug +// +build !debug + +package debug + +import ( + "github.com/gorilla/mux" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +// SetupGQLPlaygroundRoutes ... +func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router, + storeController storage.StoreController, log log.Logger, +) { + log.Warn().Msg("skipping enabling graphql playground extension because given zot binary" + + "doesn't include this feature, please build a binary that does so") +} diff --git a/pkg/debug/gqlplayground/index.html.tmpl b/pkg/debug/gqlplayground/index.html.tmpl new file mode 100644 index 00000000..f4d17e8b --- /dev/null +++ b/pkg/debug/gqlplayground/index.html.tmpl @@ -0,0 +1,67 @@ + + + + + zot GraphQL playground + + + + + + + + + + + +
Loading...
+ + + + diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 6af532e5..7cb67ba4 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -776,11 +776,17 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var sources = []*ast.Source{ {Name: "../schema.graphql", Input: `scalar Time +""" +Contains the tag of the image and a list of CVEs +""" type CVEResultForImage { Tag: String CVEList: [CVE] } +""" +Contains various details about the CVE and a list of PackageInfo about the affected packages +""" type CVE { Id: String Title: String @@ -789,12 +795,18 @@ type CVE { PackageList: [PackageInfo] } +""" +Contains the name of the package, the current installed version and the version where the CVE was fixed +""" type PackageInfo { Name: String InstalledVersion: String FixedVersion: String } +""" +Contains details about the repo: a list of image summaries and a summary of the repo +""" type RepoInfo { Images: [ImageSummary] Summary: RepoSummary @@ -802,6 +814,9 @@ type RepoInfo { # Search results in all repos/images/layers # There will be other more structures for more detailed information +""" +Search everything. Can search Images, Repos and Layers +""" type GlobalSearchResult { Images: [ImageSummary] Repos: [RepoSummary] @@ -810,6 +825,9 @@ type GlobalSearchResult { # Brief on a specific image to be used in queries returning a list of images # We define an image as a pairing or a repo and a tag belonging to that repo +""" +Contains details about the image +""" type ImageSummary { RepoName: String Tag: String @@ -839,6 +857,9 @@ type ImageVulnerabilitySummary { } # Brief on a specific repo to be used in queries returning a list of repos +""" +Contains details about the repo +""" type RepoSummary { Name: String LastUpdated: Time @@ -854,6 +875,9 @@ type RepoSummary { # Currently the same as LayerInfo, we can refactor later # For detailed information on the layer a ImageListForDigest call can be made +""" +Contains details about the layer +""" type LayerSummary { Size: String # Int64 is not supported. Digest: String @@ -885,22 +909,68 @@ type LayerHistory { HistoryDescription: HistoryDescription } +""" +Contains details about the supported OS and architecture of the image +""" type OsArch { Os: String Arch: String } type Query { + """ + Returns a CVE list for the image specified in the arugment + """ CVEListForImage(image: String!): CVEResultForImage! + + """ + Returns a list of images vulnerable to the CVE of the specified ID + """ ImageListForCVE(id: String!): [ImageSummary!] + + """ + Returns a list of images that are no longer vulnerable to the CVE of the specified ID, from the specified image (repo) + """ ImageListWithCVEFixed(id: String!, image: String!): [ImageSummary!] + + """ + Returns a list of images which contain the specified digest + """ ImageListForDigest(id: String!): [ImageSummary!] + + """ + Returns a list of repos with the newest tag within + """ RepoListWithNewestImage: [RepoSummary!]! # Newest based on created timestamp + + """ + Returns all the images from the specified repo + """ ImageList(repo: String!): [ImageSummary!] + + """ + Returns information about the specified repo + """ ExpandedRepoInfo(repo: String!): RepoInfo! + + """ + Searches within repos, images, and layers + """ GlobalSearch(query: String!): GlobalSearchResult! + + """ + List of images which use the argument image + """ DerivedImageList(image: String!): [ImageSummary!] + + """ + List of images on which the argument image depends on + """ BaseImageList(image: String!): [ImageSummary!] + + """ + Search for a specific image using its name + """ Image(image: String!): ImageSummary } `, BuiltIn: false}, diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 5428b57f..e7a1ea65 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -6,6 +6,7 @@ import ( "time" ) +// Contains various details about the CVE and a list of PackageInfo about the affected packages type Cve struct { ID *string `json:"Id"` Title *string `json:"Title"` @@ -14,11 +15,13 @@ type Cve struct { PackageList []*PackageInfo `json:"PackageList"` } +// Contains the tag of the image and a list of CVEs type CVEResultForImage struct { Tag *string `json:"Tag"` CVEList []*Cve `json:"CVEList"` } +// Search everything. Can search Images, Repos and Layers type GlobalSearchResult struct { Images []*ImageSummary `json:"Images"` Repos []*RepoSummary `json:"Repos"` @@ -37,6 +40,7 @@ type HistoryDescription struct { EmptyLayer *bool `json:"EmptyLayer"` } +// Contains details about the image type ImageSummary struct { RepoName *string `json:"RepoName"` Tag *string `json:"Tag"` @@ -70,28 +74,33 @@ type LayerHistory struct { HistoryDescription *HistoryDescription `json:"HistoryDescription"` } +// Contains details about the layer type LayerSummary struct { Size *string `json:"Size"` Digest *string `json:"Digest"` Score *int `json:"Score"` } +// Contains details about the supported OS and architecture of the image type OsArch struct { Os *string `json:"Os"` Arch *string `json:"Arch"` } +// Contains the name of the package, the current installed version and the version where the CVE was fixed type PackageInfo struct { Name *string `json:"Name"` InstalledVersion *string `json:"InstalledVersion"` FixedVersion *string `json:"FixedVersion"` } +// Contains details about the repo: a list of image summaries and a summary of the repo type RepoInfo struct { Images []*ImageSummary `json:"Images"` Summary *RepoSummary `json:"Summary"` } +// Contains details about the repo type RepoSummary struct { Name *string `json:"Name"` LastUpdated *time.Time `json:"LastUpdated"` diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 91479616..d419f16c 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -1,10 +1,16 @@ scalar Time +""" +Contains the tag of the image and a list of CVEs +""" type CVEResultForImage { Tag: String CVEList: [CVE] } +""" +Contains various details about the CVE and a list of PackageInfo about the affected packages +""" type CVE { Id: String Title: String @@ -13,12 +19,18 @@ type CVE { PackageList: [PackageInfo] } +""" +Contains the name of the package, the current installed version and the version where the CVE was fixed +""" type PackageInfo { Name: String InstalledVersion: String FixedVersion: String } +""" +Contains details about the repo: a list of image summaries and a summary of the repo +""" type RepoInfo { Images: [ImageSummary] Summary: RepoSummary @@ -26,6 +38,9 @@ type RepoInfo { # Search results in all repos/images/layers # There will be other more structures for more detailed information +""" +Search everything. Can search Images, Repos and Layers +""" type GlobalSearchResult { Images: [ImageSummary] Repos: [RepoSummary] @@ -34,6 +49,9 @@ type GlobalSearchResult { # Brief on a specific image to be used in queries returning a list of images # We define an image as a pairing or a repo and a tag belonging to that repo +""" +Contains details about the image +""" type ImageSummary { RepoName: String Tag: String @@ -63,6 +81,9 @@ type ImageVulnerabilitySummary { } # Brief on a specific repo to be used in queries returning a list of repos +""" +Contains details about the repo +""" type RepoSummary { Name: String LastUpdated: Time @@ -78,6 +99,9 @@ type RepoSummary { # Currently the same as LayerInfo, we can refactor later # For detailed information on the layer a ImageListForDigest call can be made +""" +Contains details about the layer +""" type LayerSummary { Size: String # Int64 is not supported. Digest: String @@ -109,21 +133,67 @@ type LayerHistory { HistoryDescription: HistoryDescription } +""" +Contains details about the supported OS and architecture of the image +""" type OsArch { Os: String Arch: String } type Query { + """ + Returns a CVE list for the image specified in the arugment + """ CVEListForImage(image: String!): CVEResultForImage! + + """ + Returns a list of images vulnerable to the CVE of the specified ID + """ ImageListForCVE(id: String!): [ImageSummary!] + + """ + Returns a list of images that are no longer vulnerable to the CVE of the specified ID, from the specified image (repo) + """ ImageListWithCVEFixed(id: String!, image: String!): [ImageSummary!] + + """ + Returns a list of images which contain the specified digest + """ ImageListForDigest(id: String!): [ImageSummary!] + + """ + Returns a list of repos with the newest tag within + """ RepoListWithNewestImage: [RepoSummary!]! # Newest based on created timestamp + + """ + Returns all the images from the specified repo + """ ImageList(repo: String!): [ImageSummary!] + + """ + Returns information about the specified repo + """ ExpandedRepoInfo(repo: String!): RepoInfo! + + """ + Searches within repos, images, and layers + """ GlobalSearch(query: String!): GlobalSearchResult! + + """ + List of images which use the argument image + """ DerivedImageList(image: String!): [ImageSummary!] + + """ + List of images on which the argument image depends on + """ BaseImageList(image: String!): [ImageSummary!] + + """ + Search for a specific image using its name + """ Image(image: String!): ImageSummary }