diff --git a/api/api.go b/api/api.go index cf1a6a6d..41a4c396 100644 --- a/api/api.go +++ b/api/api.go @@ -35,11 +35,9 @@ const timeoutResponse = `{"Error":{"Message":"Clair failed to respond within the // Config is the configuration for the API service. type Config struct { - Port int GrpcPort int HealthPort int Timeout time.Duration - PaginationKey string CertFile, KeyFile, CAFile string } @@ -51,40 +49,7 @@ func RunV2(cfg *Config, store database.Datastore) { if tlsConfig != nil { log.Info("main API configured with client certificate authentication") } - v2.Run(cfg.GrpcPort, tlsConfig, cfg.PaginationKey, cfg.CertFile, cfg.KeyFile, store) -} - -func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) { - defer st.End() - - // Do not run the API service if there is no config. - if cfg == nil { - log.Info("main API service is disabled.") - return - } - log.WithField("port", cfg.Port).Info("starting main API") - - tlsConfig, err := tlsClientConfig(cfg.CAFile) - if err != nil { - log.WithError(err).Fatal("could not initialize client cert authentication") - } - if tlsConfig != nil { - log.Info("main API configured with client certificate authentication") - } - - srv := &graceful.Server{ - Timeout: 0, // Already handled by our TimeOut middleware - NoSignalHandling: true, // We want to use our own Stopper - Server: &http.Server{ - Addr: ":" + strconv.Itoa(cfg.Port), - TLSConfig: tlsConfig, - Handler: http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse), - }, - } - - listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile) - - log.Info("main API stopped") + v2.Run(cfg.GrpcPort, tlsConfig, cfg.CertFile, cfg.KeyFile, store) } func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) { diff --git a/api/router.go b/api/router.go index 59ebf96b..c3bd3b41 100644 --- a/api/router.go +++ b/api/router.go @@ -16,13 +16,9 @@ package api import ( "net/http" - "strings" "github.com/julienschmidt/httprouter" - log "github.com/sirupsen/logrus" - "github.com/coreos/clair/api/httputil" - "github.com/coreos/clair/api/v1" "github.com/coreos/clair/database" ) @@ -30,34 +26,6 @@ import ( // depending on the API version specified in the request URI. type router map[string]*httprouter.Router -// Let's hope we never have more than 99 API versions. -const apiVersionLength = len("v99") - -func newAPIHandler(cfg *Config, store database.Datastore) http.Handler { - router := make(router) - router["/v1"] = v1.NewRouter(store, cfg.PaginationKey) - return router -} - -func (rtr router) ServeHTTP(w http.ResponseWriter, r *http.Request) { - urlStr := r.URL.String() - var version string - if len(urlStr) >= apiVersionLength { - version = urlStr[:apiVersionLength] - } - - if router, _ := rtr[version]; router != nil { - // Remove the version number from the request path to let the router do its - // job but do not update the RequestURI - r.URL.Path = strings.Replace(r.URL.Path, version, "", 1) - router.ServeHTTP(w, r) - return - } - - log.WithFields(log.Fields{"status": http.StatusNotFound, "method": r.Method, "request uri": r.RequestURI, "remote addr": httputil.GetClientAddr(r)}).Info("Served HTTP request") - http.NotFound(w, r) -} - func newHealthHandler(store database.Datastore) http.Handler { router := httprouter.New() router.GET("/health", healthHandler(store)) diff --git a/api/v1/models.go b/api/v1/models.go deleted file mode 100644 index 2a1b8065..00000000 --- a/api/v1/models.go +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2017 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v1 - -import ( - "fmt" - - "github.com/coreos/clair/api/token" - "github.com/coreos/clair/database" - "github.com/coreos/clair/ext/versionfmt" -) - -type Error struct { - Message string `json:"Message,omitempty"` -} - -type Layer struct { - Name string `json:"Name,omitempty"` - NamespaceNames []string `json:"NamespaceNames,omitempty"` - Path string `json:"Path,omitempty"` - Headers map[string]string `json:"Headers,omitempty"` - ParentName string `json:"ParentName,omitempty"` - Format string `json:"Format,omitempty"` - IndexedByVersion int `json:"IndexedByVersion,omitempty"` - Features []Feature `json:"Features,omitempty"` -} - -func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabilities bool) Layer { - layer := Layer{ - Name: dbLayer.Name, - IndexedByVersion: dbLayer.EngineVersion, - } - - if dbLayer.Parent != nil { - layer.ParentName = dbLayer.Parent.Name - } - - for _, ns := range dbLayer.Namespaces { - layer.NamespaceNames = append(layer.NamespaceNames, ns.Name) - } - - if withFeatures || withVulnerabilities && dbLayer.Features != nil { - for _, dbFeatureVersion := range dbLayer.Features { - feature := Feature{ - Name: dbFeatureVersion.Feature.Name, - NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat, - Version: dbFeatureVersion.Version, - AddedBy: dbFeatureVersion.AddedBy.Name, - } - - for _, dbVuln := range dbFeatureVersion.AffectedBy { - vuln := Vulnerability{ - Name: dbVuln.Name, - NamespaceName: dbVuln.Namespace.Name, - Description: dbVuln.Description, - Link: dbVuln.Link, - Severity: string(dbVuln.Severity), - Metadata: dbVuln.Metadata, - } - - if dbVuln.FixedBy != versionfmt.MaxVersion { - vuln.FixedBy = dbVuln.FixedBy - } - feature.Vulnerabilities = append(feature.Vulnerabilities, vuln) - } - layer.Features = append(layer.Features, feature) - } - } - - return layer -} - -type Namespace struct { - Name string `json:"Name,omitempty"` - VersionFormat string `json:"VersionFormat,omitempty"` -} - -type Vulnerability struct { - Name string `json:"Name,omitempty"` - NamespaceName string `json:"NamespaceName,omitempty"` - Description string `json:"Description,omitempty"` - Link string `json:"Link,omitempty"` - Severity string `json:"Severity,omitempty"` - Metadata map[string]interface{} `json:"Metadata,omitempty"` - FixedBy string `json:"FixedBy,omitempty"` - FixedIn []Feature `json:"FixedIn,omitempty"` -} - -func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) { - severity, err := database.NewSeverity(v.Severity) - if err != nil { - return database.Vulnerability{}, err - } - - var dbFeatures []database.FeatureVersion - for _, feature := range v.FixedIn { - dbFeature, err := feature.DatabaseModel() - if err != nil { - return database.Vulnerability{}, err - } - - dbFeatures = append(dbFeatures, dbFeature) - } - - return database.Vulnerability{ - Name: v.Name, - Namespace: database.Namespace{Name: v.NamespaceName}, - Description: v.Description, - Link: v.Link, - Severity: severity, - Metadata: v.Metadata, - FixedIn: dbFeatures, - }, nil -} - -func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn bool) Vulnerability { - vuln := Vulnerability{ - Name: dbVuln.Name, - NamespaceName: dbVuln.Namespace.Name, - Description: dbVuln.Description, - Link: dbVuln.Link, - Severity: string(dbVuln.Severity), - Metadata: dbVuln.Metadata, - } - - if withFixedIn { - for _, dbFeatureVersion := range dbVuln.FixedIn { - vuln.FixedIn = append(vuln.FixedIn, FeatureFromDatabaseModel(dbFeatureVersion)) - } - } - - return vuln -} - -type Feature struct { - Name string `json:"Name,omitempty"` - NamespaceName string `json:"NamespaceName,omitempty"` - VersionFormat string `json:"VersionFormat,omitempty"` - Version string `json:"Version,omitempty"` - Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` - AddedBy string `json:"AddedBy,omitempty"` -} - -func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature { - version := dbFeatureVersion.Version - if version == versionfmt.MaxVersion { - version = "None" - } - - return Feature{ - Name: dbFeatureVersion.Feature.Name, - NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat, - Version: version, - AddedBy: dbFeatureVersion.AddedBy.Name, - } -} - -func (f Feature) DatabaseModel() (fv database.FeatureVersion, err error) { - var version string - if f.Version == "None" { - version = versionfmt.MaxVersion - } else { - err = versionfmt.Valid(f.VersionFormat, f.Version) - if err != nil { - return - } - version = f.Version - } - - fv = database.FeatureVersion{ - Feature: database.Feature{ - Name: f.Name, - Namespace: database.Namespace{ - Name: f.NamespaceName, - VersionFormat: f.VersionFormat, - }, - }, - Version: version, - } - - return -} - -type Notification struct { - Name string `json:"Name,omitempty"` - Created string `json:"Created,omitempty"` - Notified string `json:"Notified,omitempty"` - Deleted string `json:"Deleted,omitempty"` - Limit int `json:"Limit,omitempty"` - Page string `json:"Page,omitempty"` - NextPage string `json:"NextPage,omitempty"` - Old *VulnerabilityWithLayers `json:"Old,omitempty"` - New *VulnerabilityWithLayers `json:"New,omitempty"` -} - -func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, pageToken string, nextPage database.VulnerabilityNotificationPageNumber, key string) Notification { - var oldVuln *VulnerabilityWithLayers - if dbNotification.OldVulnerability != nil { - v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability) - oldVuln = &v - } - - var newVuln *VulnerabilityWithLayers - if dbNotification.NewVulnerability != nil { - v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.NewVulnerability) - newVuln = &v - } - - var nextPageStr string - if nextPage != database.NoVulnerabilityNotificationPage { - nextPageBytes, _ := token.Marshal(nextPage, key) - nextPageStr = string(nextPageBytes) - } - - var created, notified, deleted string - if !dbNotification.Created.IsZero() { - created = fmt.Sprintf("%d", dbNotification.Created.Unix()) - } - if !dbNotification.Notified.IsZero() { - notified = fmt.Sprintf("%d", dbNotification.Notified.Unix()) - } - if !dbNotification.Deleted.IsZero() { - deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix()) - } - - // TODO(jzelinskie): implement "changed" key - fmt.Println(dbNotification.Deleted.IsZero()) - return Notification{ - Name: dbNotification.Name, - Created: created, - Notified: notified, - Deleted: deleted, - Limit: limit, - Page: pageToken, - NextPage: nextPageStr, - Old: oldVuln, - New: newVuln, - } -} - -type VulnerabilityWithLayers struct { - Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` - - // This field is guaranteed to be in order only for pagination. - // Indices from different notifications may not be comparable. - OrderedLayersIntroducingVulnerability []OrderedLayerName `json:"OrderedLayersIntroducingVulnerability,omitempty"` - - // This field is deprecated. - LayersIntroducingVulnerability []string `json:"LayersIntroducingVulnerability,omitempty"` -} - -type OrderedLayerName struct { - Index int `json:"Index"` - LayerName string `json:"LayerName"` -} - -func VulnerabilityWithLayersFromDatabaseModel(dbVuln database.Vulnerability) VulnerabilityWithLayers { - vuln := VulnerabilityFromDatabaseModel(dbVuln, true) - - var layers []string - var orderedLayers []OrderedLayerName - for _, layer := range dbVuln.LayersIntroducingVulnerability { - layers = append(layers, layer.Name) - orderedLayers = append(orderedLayers, OrderedLayerName{ - Index: layer.ID, - LayerName: layer.Name, - }) - } - - return VulnerabilityWithLayers{ - Vulnerability: &vuln, - OrderedLayersIntroducingVulnerability: orderedLayers, - LayersIntroducingVulnerability: layers, - } -} - -type LayerEnvelope struct { - Layer *Layer `json:"Layer,omitempty"` - Error *Error `json:"Error,omitempty"` -} - -type NamespaceEnvelope struct { - Namespaces *[]Namespace `json:"Namespaces,omitempty"` - Error *Error `json:"Error,omitempty"` -} - -type VulnerabilityEnvelope struct { - Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` - Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"` - NextPage string `json:"NextPage,omitempty"` - Error *Error `json:"Error,omitempty"` -} - -type NotificationEnvelope struct { - Notification *Notification `json:"Notification,omitempty"` - Error *Error `json:"Error,omitempty"` -} - -type FeatureEnvelope struct { - Feature *Feature `json:"Feature,omitempty"` - Features *[]Feature `json:"Features,omitempty"` - Error *Error `json:"Error,omitempty"` -} diff --git a/api/v1/router.go b/api/v1/router.go deleted file mode 100644 index d5e93eeb..00000000 --- a/api/v1/router.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package v1 implements the first version of the Clair API. -package v1 - -import ( - "net/http" - "strconv" - "time" - - "github.com/julienschmidt/httprouter" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - - "github.com/coreos/clair/api/httputil" - "github.com/coreos/clair/database" -) - -var ( - promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Name: "clair_api_response_duration_milliseconds", - Help: "The duration of time it takes to receieve and write a response to an API request", - Buckets: prometheus.ExponentialBuckets(9.375, 2, 10), - }, []string{"route", "code"}) -) - -func init() { - prometheus.MustRegister(promResponseDurationMilliseconds) -} - -type handler func(http.ResponseWriter, *http.Request, httprouter.Params, *context) (route string, status int) - -func httpHandler(h handler, ctx *context) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - start := time.Now() - route, status := h(w, r, p, ctx) - statusStr := strconv.Itoa(status) - if status == 0 { - statusStr = "???" - } - - promResponseDurationMilliseconds. - WithLabelValues(route, statusStr). - Observe(float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond)) - - log.WithFields(log.Fields{"remote addr": httputil.GetClientAddr(r), "method": r.Method, "request uri": r.RequestURI, "status": statusStr, "elapsed time": time.Since(start)}).Info("Handled HTTP request") - } -} - -type context struct { - Store database.Datastore - PaginationKey string -} - -// NewRouter creates an HTTP router for version 1 of the Clair API. -func NewRouter(store database.Datastore, paginationKey string) *httprouter.Router { - router := httprouter.New() - ctx := &context{store, paginationKey} - - // Layers - router.POST("/layers", httpHandler(postLayer, ctx)) - router.GET("/layers/:layerName", httpHandler(getLayer, ctx)) - router.DELETE("/layers/:layerName", httpHandler(deleteLayer, ctx)) - - // Namespaces - router.GET("/namespaces", httpHandler(getNamespaces, ctx)) - - // Vulnerabilities - router.GET("/namespaces/:namespaceName/vulnerabilities", httpHandler(getVulnerabilities, ctx)) - router.POST("/namespaces/:namespaceName/vulnerabilities", httpHandler(postVulnerability, ctx)) - router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(getVulnerability, ctx)) - router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(putVulnerability, ctx)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", httpHandler(deleteVulnerability, ctx)) - - // Fixes - router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", httpHandler(getFixes, ctx)) - router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(putFix, ctx)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", httpHandler(deleteFix, ctx)) - - // Notifications - router.GET("/notifications/:notificationName", httpHandler(getNotification, ctx)) - router.DELETE("/notifications/:notificationName", httpHandler(deleteNotification, ctx)) - - // Metrics - router.GET("/metrics", httpHandler(getMetrics, ctx)) - - return router -} diff --git a/api/v1/routes.go b/api/v1/routes.go deleted file mode 100644 index 9a5f6bb3..00000000 --- a/api/v1/routes.go +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v1 - -import ( - "compress/gzip" - "encoding/json" - "io" - "net/http" - "strconv" - "strings" - - "github.com/julienschmidt/httprouter" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - - "github.com/coreos/clair" - "github.com/coreos/clair/api/token" - "github.com/coreos/clair/database" - "github.com/coreos/clair/pkg/commonerr" - "github.com/coreos/clair/pkg/tarutil" -) - -const ( - // These are the route identifiers for prometheus. - postLayerRoute = "v1/postLayer" - getLayerRoute = "v1/getLayer" - deleteLayerRoute = "v1/deleteLayer" - getNamespacesRoute = "v1/getNamespaces" - getVulnerabilitiesRoute = "v1/getVulnerabilities" - postVulnerabilityRoute = "v1/postVulnerability" - getVulnerabilityRoute = "v1/getVulnerability" - putVulnerabilityRoute = "v1/putVulnerability" - deleteVulnerabilityRoute = "v1/deleteVulnerability" - getFixesRoute = "v1/getFixes" - putFixRoute = "v1/putFix" - deleteFixRoute = "v1/deleteFix" - getNotificationRoute = "v1/getNotification" - deleteNotificationRoute = "v1/deleteNotification" - getMetricsRoute = "v1/getMetrics" - - // maxBodySize restricts client request bodies to 1MiB. - maxBodySize int64 = 1048576 - - // statusUnprocessableEntity represents the 422 (Unprocessable Entity) status code, which means - // the server understands the content type of the request entity - // (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the - // request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was - // unable to process the contained instructions. - statusUnprocessableEntity = 422 -) - -func decodeJSON(r *http.Request, v interface{}) error { - defer r.Body.Close() - return json.NewDecoder(io.LimitReader(r.Body, maxBodySize)).Decode(v) -} - -func writeResponse(w http.ResponseWriter, r *http.Request, status int, resp interface{}) { - // Headers must be written before the response. - header := w.Header() - header.Set("Content-Type", "application/json;charset=utf-8") - header.Set("Server", "clair") - - // Gzip the response if the client supports it. - var writer io.Writer = w - if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - gzipWriter := gzip.NewWriter(w) - defer gzipWriter.Close() - writer = gzipWriter - - header.Set("Content-Encoding", "gzip") - } - - // Write the response. - w.WriteHeader(status) - err := json.NewEncoder(writer).Encode(resp) - - if err != nil { - switch err.(type) { - case *json.MarshalerError, *json.UnsupportedTypeError, *json.UnsupportedValueError: - panic("v1: failed to marshal response: " + err.Error()) - default: - log.WithError(err).Warning("failed to write response") - } - } -} - -func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - request := LayerEnvelope{} - err := decodeJSON(r, &request) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}}) - return postLayerRoute, http.StatusBadRequest - } - - if request.Layer == nil { - writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{"failed to provide layer"}}) - return postLayerRoute, http.StatusBadRequest - } - - err = clair.ProcessLayer(ctx.Store, request.Layer.Format, request.Layer.Name, request.Layer.ParentName, request.Layer.Path, request.Layer.Headers) - if err != nil { - if err == tarutil.ErrCouldNotExtract || - err == tarutil.ErrExtractedFileTooBig || - err == clair.ErrUnsupported { - writeResponse(w, r, statusUnprocessableEntity, LayerEnvelope{Error: &Error{err.Error()}}) - return postLayerRoute, statusUnprocessableEntity - } - - if _, badreq := err.(*commonerr.ErrBadRequest); badreq { - writeResponse(w, r, http.StatusBadRequest, LayerEnvelope{Error: &Error{err.Error()}}) - return postLayerRoute, http.StatusBadRequest - } - - writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}}) - return postLayerRoute, http.StatusInternalServerError - } - - writeResponse(w, r, http.StatusCreated, LayerEnvelope{Layer: &Layer{ - Name: request.Layer.Name, - ParentName: request.Layer.ParentName, - Path: request.Layer.Path, - Headers: request.Layer.Headers, - Format: request.Layer.Format, - IndexedByVersion: clair.Version, - }}) - return postLayerRoute, http.StatusCreated -} - -func getLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - _, withFeatures := r.URL.Query()["features"] - _, withVulnerabilities := r.URL.Query()["vulnerabilities"] - - dbLayer, err := ctx.Store.FindLayer(p.ByName("layerName"), withFeatures, withVulnerabilities) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}}) - return getLayerRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}}) - return getLayerRoute, http.StatusInternalServerError - } - - layer := LayerFromDatabaseModel(dbLayer, withFeatures, withVulnerabilities) - - writeResponse(w, r, http.StatusOK, LayerEnvelope{Layer: &layer}) - return getLayerRoute, http.StatusOK -} - -func deleteLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - err := ctx.Store.DeleteLayer(p.ByName("layerName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, LayerEnvelope{Error: &Error{err.Error()}}) - return deleteLayerRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, LayerEnvelope{Error: &Error{err.Error()}}) - return deleteLayerRoute, http.StatusInternalServerError - } - - w.WriteHeader(http.StatusOK) - return deleteLayerRoute, http.StatusOK -} - -func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - dbNamespaces, err := ctx.Store.ListNamespaces() - if err != nil { - writeResponse(w, r, http.StatusInternalServerError, NamespaceEnvelope{Error: &Error{err.Error()}}) - return getNamespacesRoute, http.StatusInternalServerError - } - var namespaces []Namespace - for _, dbNamespace := range dbNamespaces { - namespaces = append(namespaces, Namespace{ - Name: dbNamespace.Name, - VersionFormat: dbNamespace.VersionFormat, - }) - } - - writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces}) - return getNamespacesRoute, http.StatusOK -} - -func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - query := r.URL.Query() - - limitStrs, limitExists := query["limit"] - if !limitExists { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide limit query parameter"}}) - return getVulnerabilitiesRoute, http.StatusBadRequest - } - limit, err := strconv.Atoi(limitStrs[0]) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid limit format: " + err.Error()}}) - return getVulnerabilitiesRoute, http.StatusBadRequest - } else if limit < 0 { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"limit value should not be less than zero"}}) - return getVulnerabilitiesRoute, http.StatusBadRequest - } - - page := 0 - pageStrs, pageExists := query["page"] - if pageExists { - err = token.Unmarshal(pageStrs[0], ctx.PaginationKey, &page) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) - return getNotificationRoute, http.StatusBadRequest - } - } - - namespace := p.ByName("namespaceName") - if namespace == "" { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"namespace should not be empty"}}) - return getNotificationRoute, http.StatusBadRequest - } - - dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespace, limit, page) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return getVulnerabilityRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return getVulnerabilitiesRoute, http.StatusInternalServerError - } - - var vulns []Vulnerability - for _, dbVuln := range dbVulns { - vuln := VulnerabilityFromDatabaseModel(dbVuln, false) - vulns = append(vulns, vuln) - } - - var nextPageStr string - if nextPage != -1 { - nextPageBytes, err := token.Marshal(nextPage, ctx.PaginationKey) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) - return getNotificationRoute, http.StatusBadRequest - } - nextPageStr = string(nextPageBytes) - } - - writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns, NextPage: nextPageStr}) - return getVulnerabilitiesRoute, http.StatusOK -} - -func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - request := VulnerabilityEnvelope{} - err := decodeJSON(r, &request) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return postVulnerabilityRoute, http.StatusBadRequest - } - - if request.Vulnerability == nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}}) - return postVulnerabilityRoute, http.StatusBadRequest - } - - vuln, err := request.Vulnerability.DatabaseModel() - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return postVulnerabilityRoute, http.StatusBadRequest - } - - err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true) - if err != nil { - switch err.(type) { - case *commonerr.ErrBadRequest: - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return postVulnerabilityRoute, http.StatusBadRequest - default: - writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return postVulnerabilityRoute, http.StatusInternalServerError - } - } - - writeResponse(w, r, http.StatusCreated, VulnerabilityEnvelope{Vulnerability: request.Vulnerability}) - return postVulnerabilityRoute, http.StatusCreated -} - -func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - _, withFixedIn := r.URL.Query()["fixedIn"] - - dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return getVulnerabilityRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return getVulnerabilityRoute, http.StatusInternalServerError - } - - vuln := VulnerabilityFromDatabaseModel(dbVuln, withFixedIn) - - writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: &vuln}) - return getVulnerabilityRoute, http.StatusOK -} - -func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - request := VulnerabilityEnvelope{} - err := decodeJSON(r, &request) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return putVulnerabilityRoute, http.StatusBadRequest - } - - if request.Vulnerability == nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to provide vulnerability"}}) - return putVulnerabilityRoute, http.StatusBadRequest - } - - if len(request.Vulnerability.FixedIn) != 0 { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"Vulnerability.FixedIn must be empty"}}) - return putVulnerabilityRoute, http.StatusBadRequest - } - - vuln, err := request.Vulnerability.DatabaseModel() - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return putVulnerabilityRoute, http.StatusBadRequest - } - - vuln.Namespace.Name = p.ByName("namespaceName") - vuln.Name = p.ByName("vulnerabilityName") - - err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true) - if err != nil { - switch err.(type) { - case *commonerr.ErrBadRequest: - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return putVulnerabilityRoute, http.StatusBadRequest - default: - writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return putVulnerabilityRoute, http.StatusInternalServerError - } - } - - writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerability: request.Vulnerability}) - return putVulnerabilityRoute, http.StatusOK -} - -func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - err := ctx.Store.DeleteVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return deleteVulnerabilityRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) - return deleteVulnerabilityRoute, http.StatusInternalServerError - } - - w.WriteHeader(http.StatusOK) - return deleteVulnerabilityRoute, http.StatusOK -} - -func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}}) - return getFixesRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}}) - return getFixesRoute, http.StatusInternalServerError - } - - vuln := VulnerabilityFromDatabaseModel(dbVuln, true) - writeResponse(w, r, http.StatusOK, FeatureEnvelope{Features: &vuln.FixedIn}) - return getFixesRoute, http.StatusOK -} - -func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - request := FeatureEnvelope{} - err := decodeJSON(r, &request) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}}) - return putFixRoute, http.StatusBadRequest - } - - if request.Feature == nil { - writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"failed to provide feature"}}) - return putFixRoute, http.StatusBadRequest - } - - if request.Feature.Name != p.ByName("fixName") { - writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"feature name in URL and JSON do not match"}}) - return putFixRoute, http.StatusBadRequest - } - - dbFix, err := request.Feature.DatabaseModel() - if err != nil { - writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}}) - return putFixRoute, http.StatusBadRequest - } - - err = ctx.Store.InsertVulnerabilityFixes(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), []database.FeatureVersion{dbFix}) - if err != nil { - switch err.(type) { - case *commonerr.ErrBadRequest: - writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{err.Error()}}) - return putFixRoute, http.StatusBadRequest - default: - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}}) - return putFixRoute, http.StatusNotFound - } - writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}}) - return putFixRoute, http.StatusInternalServerError - } - } - - writeResponse(w, r, http.StatusOK, FeatureEnvelope{Feature: request.Feature}) - return putFixRoute, http.StatusOK -} - -func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - err := ctx.Store.DeleteVulnerabilityFix(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), p.ByName("fixName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}}) - return deleteFixRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, FeatureEnvelope{Error: &Error{err.Error()}}) - return deleteFixRoute, http.StatusInternalServerError - } - - w.WriteHeader(http.StatusOK) - return deleteFixRoute, http.StatusOK -} - -func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - query := r.URL.Query() - - limitStrs, limitExists := query["limit"] - if !limitExists { - writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"must provide limit query parameter"}}) - return getNotificationRoute, http.StatusBadRequest - } - limit, err := strconv.Atoi(limitStrs[0]) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid limit format: " + err.Error()}}) - return getNotificationRoute, http.StatusBadRequest - } - - var pageToken string - page := database.VulnerabilityNotificationFirstPage - pageStrs, pageExists := query["page"] - if pageExists { - err := token.Unmarshal(pageStrs[0], ctx.PaginationKey, &page) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) - return getNotificationRoute, http.StatusBadRequest - } - pageToken = pageStrs[0] - } else { - pageTokenBytes, err := token.Marshal(page, ctx.PaginationKey) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) - return getNotificationRoute, http.StatusBadRequest - } - pageToken = string(pageTokenBytes) - } - - dbNotification, nextPage, err := ctx.Store.GetNotification(p.ByName("notificationName"), limit, page) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}}) - return deleteNotificationRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}}) - return getNotificationRoute, http.StatusInternalServerError - } - - notification := NotificationFromDatabaseModel(dbNotification, limit, pageToken, nextPage, ctx.PaginationKey) - - writeResponse(w, r, http.StatusOK, NotificationEnvelope{Notification: ¬ification}) - return getNotificationRoute, http.StatusOK -} - -func deleteNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - err := ctx.Store.DeleteNotification(p.ByName("notificationName")) - if err == commonerr.ErrNotFound { - writeResponse(w, r, http.StatusNotFound, NotificationEnvelope{Error: &Error{err.Error()}}) - return deleteNotificationRoute, http.StatusNotFound - } else if err != nil { - writeResponse(w, r, http.StatusInternalServerError, NotificationEnvelope{Error: &Error{err.Error()}}) - return deleteNotificationRoute, http.StatusInternalServerError - } - - w.WriteHeader(http.StatusOK) - return deleteNotificationRoute, http.StatusOK -} - -func getMetrics(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { - prometheus.Handler().ServeHTTP(w, r) - return getMetricsRoute, 0 -} diff --git a/api/v2/clairpb/clair.pb.go b/api/v2/clairpb/clair.pb.go index 6e5255d4..19816099 100644 --- a/api/v2/clairpb/clair.pb.go +++ b/api/v2/clairpb/clair.pb.go @@ -9,20 +9,20 @@ It is generated from these files: It has these top-level messages: Vulnerability + ClairStatus Feature Ancestry - LayersIntroducingVulnerabilty - OrderedLayerName Layer Notification - Page + IndexedAncestryName + PagedVulnerableAncestries PostAncestryRequest PostAncestryResponse GetAncestryRequest GetAncestryResponse GetNotificationRequest GetNotificationResponse - DeleteNotificationRequest + MarkNotificationAsReadRequest */ package clairpb @@ -31,6 +31,7 @@ import fmt "fmt" import math "math" import _ "google.golang.org/genproto/googleapis/api/annotations" import google_protobuf1 "github.com/golang/protobuf/ptypes/empty" +import google_protobuf2 "github.com/golang/protobuf/ptypes/timestamp" import ( context "golang.org/x/net/context" @@ -49,14 +50,16 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Vulnerability struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - NamespaceName string `protobuf:"bytes,2,opt,name=namespace_name,json=namespaceName" json:"namespace_name,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"` - Link string `protobuf:"bytes,4,opt,name=link" json:"link,omitempty"` - Severity string `protobuf:"bytes,5,opt,name=severity" json:"severity,omitempty"` - Metadata string `protobuf:"bytes,6,opt,name=metadata" json:"metadata,omitempty"` - FixedBy string `protobuf:"bytes,7,opt,name=fixed_by,json=fixedBy" json:"fixed_by,omitempty"` - FixedInFeatures []*Feature `protobuf:"bytes,8,rep,name=fixed_in_features,json=fixedInFeatures" json:"fixed_in_features,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + NamespaceName string `protobuf:"bytes,2,opt,name=namespace_name,json=namespaceName" json:"namespace_name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"` + Link string `protobuf:"bytes,4,opt,name=link" json:"link,omitempty"` + Severity string `protobuf:"bytes,5,opt,name=severity" json:"severity,omitempty"` + Metadata string `protobuf:"bytes,6,opt,name=metadata" json:"metadata,omitempty"` + // fixed_by exists when vulnerability is under feature. + FixedBy string `protobuf:"bytes,7,opt,name=fixed_by,json=fixedBy" json:"fixed_by,omitempty"` + // affected_versions exists when vulnerability is under notification. + AffectedVersions []*Feature `protobuf:"bytes,8,rep,name=affected_versions,json=affectedVersions" json:"affected_versions,omitempty"` } func (m *Vulnerability) Reset() { *m = Vulnerability{} } @@ -113,9 +116,43 @@ func (m *Vulnerability) GetFixedBy() string { return "" } -func (m *Vulnerability) GetFixedInFeatures() []*Feature { +func (m *Vulnerability) GetAffectedVersions() []*Feature { if m != nil { - return m.FixedInFeatures + return m.AffectedVersions + } + return nil +} + +type ClairStatus struct { + // listers and detectors are processors implemented in this Clair and used to + // scan ancestries + Listers []string `protobuf:"bytes,1,rep,name=listers" json:"listers,omitempty"` + Detectors []string `protobuf:"bytes,2,rep,name=detectors" json:"detectors,omitempty"` + LastUpdateTime *google_protobuf2.Timestamp `protobuf:"bytes,3,opt,name=last_update_time,json=lastUpdateTime" json:"last_update_time,omitempty"` +} + +func (m *ClairStatus) Reset() { *m = ClairStatus{} } +func (m *ClairStatus) String() string { return proto.CompactTextString(m) } +func (*ClairStatus) ProtoMessage() {} +func (*ClairStatus) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *ClairStatus) GetListers() []string { + if m != nil { + return m.Listers + } + return nil +} + +func (m *ClairStatus) GetDetectors() []string { + if m != nil { + return m.Detectors + } + return nil +} + +func (m *ClairStatus) GetLastUpdateTime() *google_protobuf2.Timestamp { + if m != nil { + return m.LastUpdateTime } return nil } @@ -125,14 +162,13 @@ type Feature struct { NamespaceName string `protobuf:"bytes,2,opt,name=namespace_name,json=namespaceName" json:"namespace_name,omitempty"` Version string `protobuf:"bytes,3,opt,name=version" json:"version,omitempty"` VersionFormat string `protobuf:"bytes,4,opt,name=version_format,json=versionFormat" json:"version_format,omitempty"` - AddedBy string `protobuf:"bytes,5,opt,name=added_by,json=addedBy" json:"added_by,omitempty"` - Vulnerabilities []*Vulnerability `protobuf:"bytes,6,rep,name=vulnerabilities" json:"vulnerabilities,omitempty"` + Vulnerabilities []*Vulnerability `protobuf:"bytes,5,rep,name=vulnerabilities" json:"vulnerabilities,omitempty"` } func (m *Feature) Reset() { *m = Feature{} } func (m *Feature) String() string { return proto.CompactTextString(m) } func (*Feature) ProtoMessage() {} -func (*Feature) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } +func (*Feature) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (m *Feature) GetName() string { if m != nil { @@ -162,13 +198,6 @@ func (m *Feature) GetVersionFormat() string { return "" } -func (m *Feature) GetAddedBy() string { - if m != nil { - return m.AddedBy - } - return "" -} - func (m *Feature) GetVulnerabilities() []*Vulnerability { if m != nil { return m.Vulnerabilities @@ -177,15 +206,20 @@ func (m *Feature) GetVulnerabilities() []*Vulnerability { } type Ancestry struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - EngineVersion int32 `protobuf:"varint,2,opt,name=engine_version,json=engineVersion" json:"engine_version,omitempty"` - Layers []*Layer `protobuf:"bytes,3,rep,name=layers" json:"layers,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Features []*Feature `protobuf:"bytes,2,rep,name=features" json:"features,omitempty"` + Layers []*Layer `protobuf:"bytes,3,rep,name=layers" json:"layers,omitempty"` + // scanned_listers and scanned_detectors are used to scan this ancestry, it + // may be different from listers and detectors in ClairStatus since the + // ancestry could be scanned by previous version of Clair. + ScannedListers []string `protobuf:"bytes,4,rep,name=scanned_listers,json=scannedListers" json:"scanned_listers,omitempty"` + ScannedDetectors []string `protobuf:"bytes,5,rep,name=scanned_detectors,json=scannedDetectors" json:"scanned_detectors,omitempty"` } func (m *Ancestry) Reset() { *m = Ancestry{} } func (m *Ancestry) String() string { return proto.CompactTextString(m) } func (*Ancestry) ProtoMessage() {} -func (*Ancestry) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } +func (*Ancestry) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } func (m *Ancestry) GetName() string { if m != nil { @@ -194,11 +228,11 @@ func (m *Ancestry) GetName() string { return "" } -func (m *Ancestry) GetEngineVersion() int32 { +func (m *Ancestry) GetFeatures() []*Feature { if m != nil { - return m.EngineVersion + return m.Features } - return 0 + return nil } func (m *Ancestry) GetLayers() []*Layer { @@ -208,91 +242,49 @@ func (m *Ancestry) GetLayers() []*Layer { return nil } -type LayersIntroducingVulnerabilty struct { - Vulnerability *Vulnerability `protobuf:"bytes,1,opt,name=vulnerability" json:"vulnerability,omitempty"` - Layers []*OrderedLayerName `protobuf:"bytes,2,rep,name=layers" json:"layers,omitempty"` -} - -func (m *LayersIntroducingVulnerabilty) Reset() { *m = LayersIntroducingVulnerabilty{} } -func (m *LayersIntroducingVulnerabilty) String() string { return proto.CompactTextString(m) } -func (*LayersIntroducingVulnerabilty) ProtoMessage() {} -func (*LayersIntroducingVulnerabilty) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } - -func (m *LayersIntroducingVulnerabilty) GetVulnerability() *Vulnerability { +func (m *Ancestry) GetScannedListers() []string { if m != nil { - return m.Vulnerability + return m.ScannedListers } return nil } -func (m *LayersIntroducingVulnerabilty) GetLayers() []*OrderedLayerName { +func (m *Ancestry) GetScannedDetectors() []string { if m != nil { - return m.Layers + return m.ScannedDetectors } return nil } -type OrderedLayerName struct { - Index int32 `protobuf:"varint,1,opt,name=index" json:"index,omitempty"` - LayerName string `protobuf:"bytes,2,opt,name=layer_name,json=layerName" json:"layer_name,omitempty"` -} - -func (m *OrderedLayerName) Reset() { *m = OrderedLayerName{} } -func (m *OrderedLayerName) String() string { return proto.CompactTextString(m) } -func (*OrderedLayerName) ProtoMessage() {} -func (*OrderedLayerName) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } - -func (m *OrderedLayerName) GetIndex() int32 { - if m != nil { - return m.Index - } - return 0 -} - -func (m *OrderedLayerName) GetLayerName() string { - if m != nil { - return m.LayerName - } - return "" -} - type Layer struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - NamespaceNames []string `protobuf:"bytes,2,rep,name=namespace_names,json=namespaceNames" json:"namespace_names,omitempty"` + Hash string `protobuf:"bytes,1,opt,name=hash" json:"hash,omitempty"` } func (m *Layer) Reset() { *m = Layer{} } func (m *Layer) String() string { return proto.CompactTextString(m) } func (*Layer) ProtoMessage() {} -func (*Layer) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } +func (*Layer) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } -func (m *Layer) GetName() string { +func (m *Layer) GetHash() string { if m != nil { - return m.Name + return m.Hash } return "" } -func (m *Layer) GetNamespaceNames() []string { - if m != nil { - return m.NamespaceNames - } - return nil -} - type Notification struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - Created string `protobuf:"bytes,2,opt,name=created" json:"created,omitempty"` - Notified string `protobuf:"bytes,3,opt,name=notified" json:"notified,omitempty"` - Deleted string `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"` - Limit int32 `protobuf:"varint,5,opt,name=limit" json:"limit,omitempty"` - Page *Page `protobuf:"bytes,6,opt,name=page" json:"page,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Created string `protobuf:"bytes,2,opt,name=created" json:"created,omitempty"` + Notified string `protobuf:"bytes,3,opt,name=notified" json:"notified,omitempty"` + Deleted string `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"` + Old *PagedVulnerableAncestries `protobuf:"bytes,5,opt,name=old" json:"old,omitempty"` + New *PagedVulnerableAncestries `protobuf:"bytes,6,opt,name=new" json:"new,omitempty"` } func (m *Notification) Reset() { *m = Notification{} } func (m *Notification) String() string { return proto.CompactTextString(m) } func (*Notification) ProtoMessage() {} -func (*Notification) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } +func (*Notification) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } func (m *Notification) GetName() string { if m != nil { @@ -322,56 +314,91 @@ func (m *Notification) GetDeleted() string { return "" } -func (m *Notification) GetLimit() int32 { +func (m *Notification) GetOld() *PagedVulnerableAncestries { if m != nil { - return m.Limit + return m.Old } - return 0 + return nil } -func (m *Notification) GetPage() *Page { +func (m *Notification) GetNew() *PagedVulnerableAncestries { if m != nil { - return m.Page + return m.New } return nil } -type Page struct { - ThisToken string `protobuf:"bytes,1,opt,name=this_token,json=thisToken" json:"this_token,omitempty"` - NextToken string `protobuf:"bytes,2,opt,name=next_token,json=nextToken" json:"next_token,omitempty"` - Old *LayersIntroducingVulnerabilty `protobuf:"bytes,3,opt,name=old" json:"old,omitempty"` - New *LayersIntroducingVulnerabilty `protobuf:"bytes,4,opt,name=new" json:"new,omitempty"` +type IndexedAncestryName struct { + // index is unique to name in all streams simultaneously streamed, increasing + // and larger than all indexes in previous page in same stream. + Index int32 `protobuf:"varint,1,opt,name=index" json:"index,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` } -func (m *Page) Reset() { *m = Page{} } -func (m *Page) String() string { return proto.CompactTextString(m) } -func (*Page) ProtoMessage() {} -func (*Page) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } +func (m *IndexedAncestryName) Reset() { *m = IndexedAncestryName{} } +func (m *IndexedAncestryName) String() string { return proto.CompactTextString(m) } +func (*IndexedAncestryName) ProtoMessage() {} +func (*IndexedAncestryName) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } -func (m *Page) GetThisToken() string { +func (m *IndexedAncestryName) GetIndex() int32 { if m != nil { - return m.ThisToken + return m.Index + } + return 0 +} + +func (m *IndexedAncestryName) GetName() string { + if m != nil { + return m.Name } return "" } -func (m *Page) GetNextToken() string { +type PagedVulnerableAncestries struct { + CurrentPage string `protobuf:"bytes,1,opt,name=current_page,json=currentPage" json:"current_page,omitempty"` + // if next_page is empty, it signals the end of all pages. + NextPage string `protobuf:"bytes,2,opt,name=next_page,json=nextPage" json:"next_page,omitempty"` + Limit int32 `protobuf:"varint,3,opt,name=limit" json:"limit,omitempty"` + Vulnerability *Vulnerability `protobuf:"bytes,4,opt,name=vulnerability" json:"vulnerability,omitempty"` + Ancestries []*IndexedAncestryName `protobuf:"bytes,5,rep,name=ancestries" json:"ancestries,omitempty"` +} + +func (m *PagedVulnerableAncestries) Reset() { *m = PagedVulnerableAncestries{} } +func (m *PagedVulnerableAncestries) String() string { return proto.CompactTextString(m) } +func (*PagedVulnerableAncestries) ProtoMessage() {} +func (*PagedVulnerableAncestries) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } + +func (m *PagedVulnerableAncestries) GetCurrentPage() string { if m != nil { - return m.NextToken + return m.CurrentPage } return "" } -func (m *Page) GetOld() *LayersIntroducingVulnerabilty { +func (m *PagedVulnerableAncestries) GetNextPage() string { if m != nil { - return m.Old + return m.NextPage + } + return "" +} + +func (m *PagedVulnerableAncestries) GetLimit() int32 { + if m != nil { + return m.Limit + } + return 0 +} + +func (m *PagedVulnerableAncestries) GetVulnerability() *Vulnerability { + if m != nil { + return m.Vulnerability } return nil } -func (m *Page) GetNew() *LayersIntroducingVulnerabilty { +func (m *PagedVulnerableAncestries) GetAncestries() []*IndexedAncestryName { if m != nil { - return m.New + return m.Ancestries } return nil } @@ -409,7 +436,7 @@ func (m *PostAncestryRequest) GetLayers() []*PostAncestryRequest_PostLayer { } type PostAncestryRequest_PostLayer struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Hash string `protobuf:"bytes,1,opt,name=hash" json:"hash,omitempty"` Path string `protobuf:"bytes,2,opt,name=path" json:"path,omitempty"` Headers map[string]string `protobuf:"bytes,3,rep,name=headers" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` } @@ -421,9 +448,9 @@ func (*PostAncestryRequest_PostLayer) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8, 0} } -func (m *PostAncestryRequest_PostLayer) GetName() string { +func (m *PostAncestryRequest_PostLayer) GetHash() string { if m != nil { - return m.Name + return m.Hash } return "" } @@ -443,7 +470,7 @@ func (m *PostAncestryRequest_PostLayer) GetHeaders() map[string]string { } type PostAncestryResponse struct { - EngineVersion int32 `protobuf:"varint,1,opt,name=engine_version,json=engineVersion" json:"engine_version,omitempty"` + Status *ClairStatus `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"` } func (m *PostAncestryResponse) Reset() { *m = PostAncestryResponse{} } @@ -451,11 +478,11 @@ func (m *PostAncestryResponse) String() string { return proto.Compact func (*PostAncestryResponse) ProtoMessage() {} func (*PostAncestryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } -func (m *PostAncestryResponse) GetEngineVersion() int32 { +func (m *PostAncestryResponse) GetStatus() *ClairStatus { if m != nil { - return m.EngineVersion + return m.Status } - return 0 + return nil } type GetAncestryRequest struct { @@ -491,8 +518,8 @@ func (m *GetAncestryRequest) GetWithFeatures() bool { } type GetAncestryResponse struct { - Ancestry *Ancestry `protobuf:"bytes,1,opt,name=ancestry" json:"ancestry,omitempty"` - Features []*Feature `protobuf:"bytes,2,rep,name=features" json:"features,omitempty"` + Ancestry *Ancestry `protobuf:"bytes,1,opt,name=ancestry" json:"ancestry,omitempty"` + Status *ClairStatus `protobuf:"bytes,2,opt,name=status" json:"status,omitempty"` } func (m *GetAncestryResponse) Reset() { *m = GetAncestryResponse{} } @@ -507,17 +534,19 @@ func (m *GetAncestryResponse) GetAncestry() *Ancestry { return nil } -func (m *GetAncestryResponse) GetFeatures() []*Feature { +func (m *GetAncestryResponse) GetStatus() *ClairStatus { if m != nil { - return m.Features + return m.Status } return nil } type GetNotificationRequest struct { - Page string `protobuf:"bytes,1,opt,name=page" json:"page,omitempty"` - Limit int32 `protobuf:"varint,2,opt,name=limit" json:"limit,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"` + // if the vulnerability_page is empty, it implies the first page. + OldVulnerabilityPage string `protobuf:"bytes,1,opt,name=old_vulnerability_page,json=oldVulnerabilityPage" json:"old_vulnerability_page,omitempty"` + NewVulnerabilityPage string `protobuf:"bytes,2,opt,name=new_vulnerability_page,json=newVulnerabilityPage" json:"new_vulnerability_page,omitempty"` + Limit int32 `protobuf:"varint,3,opt,name=limit" json:"limit,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name" json:"name,omitempty"` } func (m *GetNotificationRequest) Reset() { *m = GetNotificationRequest{} } @@ -525,9 +554,16 @@ func (m *GetNotificationRequest) String() string { return proto.Compa func (*GetNotificationRequest) ProtoMessage() {} func (*GetNotificationRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } -func (m *GetNotificationRequest) GetPage() string { +func (m *GetNotificationRequest) GetOldVulnerabilityPage() string { + if m != nil { + return m.OldVulnerabilityPage + } + return "" +} + +func (m *GetNotificationRequest) GetNewVulnerabilityPage() string { if m != nil { - return m.Page + return m.NewVulnerabilityPage } return "" } @@ -562,16 +598,16 @@ func (m *GetNotificationResponse) GetNotification() *Notification { return nil } -type DeleteNotificationRequest struct { +type MarkNotificationAsReadRequest struct { Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` } -func (m *DeleteNotificationRequest) Reset() { *m = DeleteNotificationRequest{} } -func (m *DeleteNotificationRequest) String() string { return proto.CompactTextString(m) } -func (*DeleteNotificationRequest) ProtoMessage() {} -func (*DeleteNotificationRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } +func (m *MarkNotificationAsReadRequest) Reset() { *m = MarkNotificationAsReadRequest{} } +func (m *MarkNotificationAsReadRequest) String() string { return proto.CompactTextString(m) } +func (*MarkNotificationAsReadRequest) ProtoMessage() {} +func (*MarkNotificationAsReadRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } -func (m *DeleteNotificationRequest) GetName() string { +func (m *MarkNotificationAsReadRequest) GetName() string { if m != nil { return m.Name } @@ -580,13 +616,13 @@ func (m *DeleteNotificationRequest) GetName() string { func init() { proto.RegisterType((*Vulnerability)(nil), "clairpb.Vulnerability") + proto.RegisterType((*ClairStatus)(nil), "clairpb.ClairStatus") proto.RegisterType((*Feature)(nil), "clairpb.Feature") proto.RegisterType((*Ancestry)(nil), "clairpb.Ancestry") - proto.RegisterType((*LayersIntroducingVulnerabilty)(nil), "clairpb.LayersIntroducingVulnerabilty") - proto.RegisterType((*OrderedLayerName)(nil), "clairpb.OrderedLayerName") proto.RegisterType((*Layer)(nil), "clairpb.Layer") proto.RegisterType((*Notification)(nil), "clairpb.Notification") - proto.RegisterType((*Page)(nil), "clairpb.Page") + proto.RegisterType((*IndexedAncestryName)(nil), "clairpb.IndexedAncestryName") + proto.RegisterType((*PagedVulnerableAncestries)(nil), "clairpb.PagedVulnerableAncestries") proto.RegisterType((*PostAncestryRequest)(nil), "clairpb.PostAncestryRequest") proto.RegisterType((*PostAncestryRequest_PostLayer)(nil), "clairpb.PostAncestryRequest.PostLayer") proto.RegisterType((*PostAncestryResponse)(nil), "clairpb.PostAncestryResponse") @@ -594,7 +630,7 @@ func init() { proto.RegisterType((*GetAncestryResponse)(nil), "clairpb.GetAncestryResponse") proto.RegisterType((*GetNotificationRequest)(nil), "clairpb.GetNotificationRequest") proto.RegisterType((*GetNotificationResponse)(nil), "clairpb.GetNotificationResponse") - proto.RegisterType((*DeleteNotificationRequest)(nil), "clairpb.DeleteNotificationRequest") + proto.RegisterType((*MarkNotificationAsReadRequest)(nil), "clairpb.MarkNotificationAsReadRequest") } // Reference imports to suppress errors if they are not otherwise used. @@ -706,7 +742,7 @@ var _AncestryService_serviceDesc = grpc.ServiceDesc{ type NotificationServiceClient interface { GetNotification(ctx context.Context, in *GetNotificationRequest, opts ...grpc.CallOption) (*GetNotificationResponse, error) - DeleteNotification(ctx context.Context, in *DeleteNotificationRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) + MarkNotificationAsRead(ctx context.Context, in *MarkNotificationAsReadRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) } type notificationServiceClient struct { @@ -726,9 +762,9 @@ func (c *notificationServiceClient) GetNotification(ctx context.Context, in *Get return out, nil } -func (c *notificationServiceClient) DeleteNotification(ctx context.Context, in *DeleteNotificationRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) { +func (c *notificationServiceClient) MarkNotificationAsRead(ctx context.Context, in *MarkNotificationAsReadRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) { out := new(google_protobuf1.Empty) - err := grpc.Invoke(ctx, "/clairpb.NotificationService/DeleteNotification", in, out, c.cc, opts...) + err := grpc.Invoke(ctx, "/clairpb.NotificationService/MarkNotificationAsRead", in, out, c.cc, opts...) if err != nil { return nil, err } @@ -739,7 +775,7 @@ func (c *notificationServiceClient) DeleteNotification(ctx context.Context, in * type NotificationServiceServer interface { GetNotification(context.Context, *GetNotificationRequest) (*GetNotificationResponse, error) - DeleteNotification(context.Context, *DeleteNotificationRequest) (*google_protobuf1.Empty, error) + MarkNotificationAsRead(context.Context, *MarkNotificationAsReadRequest) (*google_protobuf1.Empty, error) } func RegisterNotificationServiceServer(s *grpc.Server, srv NotificationServiceServer) { @@ -764,20 +800,20 @@ func _NotificationService_GetNotification_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } -func _NotificationService_DeleteNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteNotificationRequest) +func _NotificationService_MarkNotificationAsRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MarkNotificationAsReadRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(NotificationServiceServer).DeleteNotification(ctx, in) + return srv.(NotificationServiceServer).MarkNotificationAsRead(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/clairpb.NotificationService/DeleteNotification", + FullMethod: "/clairpb.NotificationService/MarkNotificationAsRead", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NotificationServiceServer).DeleteNotification(ctx, req.(*DeleteNotificationRequest)) + return srv.(NotificationServiceServer).MarkNotificationAsRead(ctx, req.(*MarkNotificationAsReadRequest)) } return interceptor(ctx, in, info, handler) } @@ -791,8 +827,8 @@ var _NotificationService_serviceDesc = grpc.ServiceDesc{ Handler: _NotificationService_GetNotification_Handler, }, { - MethodName: "DeleteNotification", - Handler: _NotificationService_DeleteNotification_Handler, + MethodName: "MarkNotificationAsRead", + Handler: _NotificationService_MarkNotificationAsRead_Handler, }, }, Streams: []grpc.StreamDesc{}, @@ -802,71 +838,78 @@ var _NotificationService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("clair.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 1042 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0xdd, 0x6e, 0x1b, 0x45, - 0x14, 0xd6, 0xda, 0x71, 0x6c, 0x1f, 0xdb, 0x49, 0x3a, 0x49, 0xd3, 0x8d, 0x93, 0x88, 0x74, 0x11, - 0xa5, 0xaa, 0xc0, 0x56, 0xd3, 0x9b, 0x12, 0x01, 0x82, 0xa8, 0x6d, 0xa8, 0x04, 0xa5, 0x5a, 0xaa, - 0x5c, 0x70, 0x63, 0x4d, 0xbc, 0x27, 0xce, 0x28, 0xeb, 0x59, 0xb3, 0x3b, 0x76, 0x62, 0x55, 0xdc, - 0xf0, 0x04, 0x54, 0x3c, 0x06, 0x2f, 0xc0, 0x15, 0x2f, 0xd1, 0x27, 0x00, 0xf1, 0x16, 0xdc, 0xa0, - 0xf9, 0xf5, 0xae, 0x63, 0x23, 0x7e, 0xae, 0x3c, 0xe7, 0x7c, 0xe7, 0xe7, 0x3b, 0x3f, 0x33, 0x5e, - 0x68, 0xf4, 0x63, 0xca, 0xd2, 0xce, 0x28, 0x4d, 0x44, 0x42, 0xaa, 0x4a, 0x18, 0x9d, 0xb5, 0xf7, - 0x06, 0x49, 0x32, 0x88, 0xb1, 0x4b, 0x47, 0xac, 0x4b, 0x39, 0x4f, 0x04, 0x15, 0x2c, 0xe1, 0x99, - 0x36, 0x6b, 0xef, 0x1a, 0x54, 0x49, 0x67, 0xe3, 0xf3, 0x2e, 0x0e, 0x47, 0x62, 0xaa, 0xc1, 0xe0, - 0x4d, 0x09, 0x5a, 0xa7, 0xe3, 0x98, 0x63, 0x4a, 0xcf, 0x58, 0xcc, 0xc4, 0x94, 0x10, 0x58, 0xe1, - 0x74, 0x88, 0xbe, 0x77, 0xe0, 0xdd, 0xaf, 0x87, 0xea, 0x4c, 0xde, 0x83, 0x35, 0xf9, 0x9b, 0x8d, - 0x68, 0x1f, 0x7b, 0x0a, 0x2d, 0x29, 0xb4, 0xe5, 0xb4, 0x2f, 0xa4, 0xd9, 0x01, 0x34, 0x22, 0xcc, - 0xfa, 0x29, 0x1b, 0xc9, 0xfc, 0x7e, 0x59, 0xd9, 0xe4, 0x55, 0x32, 0x78, 0xcc, 0xf8, 0xa5, 0xbf, - 0xa2, 0x83, 0xcb, 0x33, 0x69, 0x43, 0x2d, 0xc3, 0x09, 0xa6, 0x4c, 0x4c, 0xfd, 0x8a, 0xd2, 0x3b, - 0x59, 0x62, 0x43, 0x14, 0x34, 0xa2, 0x82, 0xfa, 0xab, 0x1a, 0xb3, 0x32, 0xd9, 0x81, 0xda, 0x39, - 0xbb, 0xc6, 0xa8, 0x77, 0x36, 0xf5, 0xab, 0x0a, 0xab, 0x2a, 0xf9, 0x78, 0x4a, 0x3e, 0x86, 0x5b, - 0x1a, 0x62, 0xbc, 0x77, 0x8e, 0x54, 0x8c, 0x53, 0xcc, 0xfc, 0xda, 0x41, 0xf9, 0x7e, 0xe3, 0x70, - 0xa3, 0x63, 0xba, 0xd6, 0x79, 0xa6, 0x81, 0x70, 0x5d, 0x99, 0x3e, 0xe7, 0x46, 0xce, 0x82, 0xdf, - 0x3d, 0xa8, 0x1a, 0xe1, 0xff, 0x74, 0xc3, 0x87, 0xea, 0x04, 0xd3, 0x6c, 0xd6, 0x09, 0x2b, 0xca, - 0x00, 0xe6, 0xd8, 0x3b, 0x4f, 0xd2, 0x21, 0x15, 0xa6, 0x1f, 0x2d, 0xa3, 0x7d, 0xa6, 0x94, 0xb2, - 0x40, 0x1a, 0x45, 0xba, 0x40, 0xdd, 0x98, 0xaa, 0x92, 0x8f, 0xa7, 0xe4, 0x33, 0x58, 0x9f, 0xe4, - 0xa6, 0xc6, 0x30, 0xf3, 0x57, 0x55, 0x79, 0xdb, 0xae, 0xbc, 0xc2, 0x54, 0xc3, 0x79, 0xf3, 0x60, - 0x08, 0xb5, 0xcf, 0x79, 0x1f, 0x33, 0x91, 0x2e, 0x1d, 0x39, 0xf2, 0x01, 0xe3, 0xd8, 0xb3, 0x45, - 0xc8, 0x22, 0x2b, 0x61, 0x4b, 0x6b, 0x4f, 0x4d, 0x29, 0xf7, 0x60, 0x35, 0xa6, 0x53, 0x4c, 0x33, - 0xbf, 0xac, 0xf2, 0xaf, 0xb9, 0xfc, 0x5f, 0x4a, 0x75, 0x68, 0xd0, 0xe0, 0x47, 0x0f, 0xf6, 0x95, - 0x26, 0x7b, 0xce, 0x45, 0x9a, 0x44, 0xe3, 0x3e, 0xe3, 0x83, 0x19, 0x45, 0x21, 0x67, 0xd6, 0xca, - 0x73, 0x9c, 0x2a, 0x36, 0xcb, 0x0b, 0x2a, 0x1a, 0x93, 0x87, 0x8e, 0x47, 0x49, 0xf1, 0xd8, 0x71, - 0x6e, 0x5f, 0xa7, 0x11, 0xa6, 0x18, 0xa9, 0xe4, 0x72, 0x2e, 0x8e, 0xd2, 0x09, 0x6c, 0xcc, 0x63, - 0x64, 0x0b, 0x2a, 0x8c, 0x47, 0x78, 0xad, 0x92, 0x57, 0x42, 0x2d, 0x90, 0x7d, 0x00, 0xe5, 0x93, - 0x1f, 0x76, 0x3d, 0xb6, 0x4e, 0xc1, 0x13, 0xa8, 0xa8, 0x08, 0x0b, 0xfb, 0xf8, 0x3e, 0xac, 0x17, - 0x97, 0x45, 0x33, 0xac, 0x87, 0x6b, 0x85, 0x6d, 0xc9, 0x82, 0x9f, 0x3d, 0x68, 0xbe, 0x48, 0x04, - 0x3b, 0x67, 0x7d, 0x6a, 0xef, 0xca, 0x8d, 0x68, 0x3e, 0x54, 0xfb, 0x29, 0x52, 0x81, 0x91, 0xa1, - 0x61, 0x45, 0x79, 0x53, 0xb8, 0xf2, 0xc6, 0xc8, 0xac, 0x9b, 0x93, 0xa5, 0x57, 0x84, 0x31, 0x4a, - 0x2f, 0xbd, 0x68, 0x56, 0x94, 0xf5, 0xc6, 0x6c, 0xc8, 0x84, 0xda, 0xaf, 0x4a, 0xa8, 0x05, 0x72, - 0x17, 0x56, 0x46, 0x74, 0x80, 0xea, 0xc6, 0x35, 0x0e, 0x5b, 0xae, 0x95, 0x2f, 0xe9, 0x00, 0x43, - 0x05, 0x05, 0xbf, 0x78, 0xb0, 0x22, 0x45, 0xd9, 0x1b, 0x71, 0xc1, 0xb2, 0x9e, 0x48, 0x2e, 0x91, - 0x1b, 0xae, 0x75, 0xa9, 0x79, 0x25, 0x15, 0x12, 0xe6, 0x78, 0x2d, 0x0c, 0x6c, 0x5a, 0x27, 0x35, - 0x1a, 0x7e, 0x0c, 0xe5, 0x24, 0xd6, 0x84, 0x1b, 0x87, 0xf7, 0x8a, 0xbb, 0xb3, 0x6c, 0x53, 0x42, - 0xe9, 0x22, 0x3d, 0x39, 0x5e, 0xa9, 0x7a, 0xfe, 0x85, 0x27, 0xc7, 0xab, 0xe0, 0x6d, 0x09, 0x36, - 0x5f, 0x26, 0x99, 0xb0, 0xeb, 0x1f, 0xe2, 0x77, 0x63, 0xcc, 0x04, 0x79, 0x17, 0x5a, 0xd4, 0xa8, - 0x7a, 0xb9, 0xc6, 0x37, 0xad, 0x52, 0x2d, 0xc8, 0x36, 0xac, 0x9a, 0x2b, 0xab, 0x6b, 0x31, 0x12, - 0xf9, 0x74, 0xee, 0x1e, 0xcc, 0x18, 0x2d, 0x48, 0xa5, 0x74, 0x85, 0xfb, 0xd1, 0xfe, 0xd5, 0x83, - 0xba, 0xd3, 0x2e, 0x1c, 0x3d, 0x91, 0x43, 0x11, 0x17, 0x26, 0xaf, 0x3a, 0x93, 0xaf, 0xa0, 0x7a, - 0x81, 0x34, 0x9a, 0xa5, 0x7d, 0xf4, 0xcf, 0xd2, 0x76, 0xbe, 0xd0, 0x5e, 0x4f, 0xb9, 0x44, 0x6d, - 0x8c, 0xf6, 0x11, 0x34, 0xf3, 0x00, 0xd9, 0x80, 0xf2, 0x25, 0x4e, 0x0d, 0x0b, 0x79, 0x94, 0xfb, - 0x32, 0xa1, 0xf1, 0xd8, 0x5e, 0x02, 0x2d, 0x1c, 0x95, 0x1e, 0x7b, 0xc1, 0x27, 0xb0, 0x55, 0x4c, - 0x99, 0x8d, 0x12, 0x9e, 0x2d, 0x7a, 0x47, 0xbc, 0x05, 0xef, 0x48, 0xf0, 0xc6, 0x03, 0x72, 0x82, - 0xff, 0x6d, 0x26, 0x0f, 0x61, 0xeb, 0x8a, 0x89, 0x8b, 0xde, 0xfc, 0x8b, 0x28, 0x39, 0xd6, 0xc2, - 0x4d, 0x89, 0x9d, 0x16, 0x21, 0x19, 0x57, 0xb9, 0xb8, 0x3f, 0x87, 0xb2, 0xb2, 0x6d, 0x4a, 0xa5, - 0xfb, 0x1f, 0x48, 0x61, 0xb3, 0x40, 0xc9, 0x54, 0xf4, 0x21, 0xd4, 0x6c, 0x7a, 0xf3, 0x46, 0xdd, - 0x72, 0x5d, 0x77, 0xc6, 0xce, 0x84, 0x7c, 0x00, 0x35, 0x97, 0xa5, 0xb4, 0xe4, 0x2f, 0xc8, 0x59, - 0x04, 0xa7, 0xb0, 0x7d, 0x82, 0x22, 0xff, 0x0e, 0xd8, 0x56, 0x10, 0x73, 0x29, 0x3d, 0x3b, 0xff, - 0x01, 0xce, 0xae, 0x6f, 0x29, 0x7f, 0x7d, 0xed, 0xf6, 0x94, 0x67, 0xdb, 0x13, 0xbc, 0x82, 0x3b, - 0x37, 0xe2, 0x9a, 0x7a, 0x3e, 0x82, 0x26, 0xcf, 0xe9, 0x4d, 0x4d, 0xb7, 0x1d, 0xc9, 0x82, 0x53, - 0xc1, 0x34, 0xe8, 0xc2, 0xce, 0x13, 0xf5, 0x92, 0x2c, 0x21, 0x3c, 0xbf, 0xc4, 0x87, 0xbf, 0x79, - 0xb0, 0x6e, 0x7b, 0xf4, 0x0d, 0xa6, 0x13, 0xd6, 0x47, 0x42, 0xa1, 0x99, 0xdf, 0x1c, 0xb2, 0xf7, - 0x77, 0x3b, 0xdc, 0xde, 0x5f, 0x82, 0xea, 0x62, 0x82, 0xad, 0x1f, 0xde, 0xfe, 0xf1, 0x53, 0x69, - 0x2d, 0xa8, 0x77, 0xed, 0x00, 0x8e, 0xbc, 0x07, 0xe4, 0x12, 0x1a, 0xb9, 0x49, 0x92, 0x5d, 0x17, - 0xe3, 0xe6, 0xca, 0xb5, 0xf7, 0x16, 0x83, 0x26, 0xfe, 0x5d, 0x15, 0x7f, 0x97, 0xec, 0xb8, 0xf8, - 0xdd, 0xd7, 0x85, 0x0d, 0xfd, 0xfe, 0xf0, 0x4f, 0x0f, 0x36, 0xf3, 0xfd, 0xb0, 0x75, 0x66, 0xb0, - 0x3e, 0x37, 0x02, 0xf2, 0x4e, 0x3e, 0xd7, 0x82, 0x1e, 0xb6, 0x0f, 0x96, 0x1b, 0x18, 0x42, 0xfb, - 0x8a, 0xd0, 0x1d, 0x72, 0xbb, 0x9b, 0x9f, 0x4c, 0xd6, 0x7d, 0xad, 0xc8, 0x90, 0x04, 0xc8, 0xcd, - 0x09, 0x91, 0xc0, 0x85, 0x5d, 0x3a, 0xbe, 0xf6, 0x76, 0x47, 0x7f, 0x37, 0x76, 0xec, 0x77, 0x63, - 0xe7, 0xa9, 0xfc, 0x6e, 0xb4, 0x09, 0x1f, 0x2c, 0x4e, 0x78, 0x5c, 0xff, 0xd6, 0x7e, 0x96, 0x9e, - 0xad, 0x2a, 0xcf, 0x47, 0x7f, 0x05, 0x00, 0x00, 0xff, 0xff, 0x5b, 0x9c, 0x1d, 0xc4, 0xb5, 0x0a, - 0x00, 0x00, + // 1156 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0x4d, 0x6f, 0xdb, 0x46, + 0x13, 0x06, 0x25, 0xcb, 0x92, 0x46, 0xf2, 0xd7, 0x5a, 0x51, 0x68, 0xd9, 0x46, 0x1c, 0xbe, 0x78, + 0xd3, 0x20, 0x6d, 0x25, 0x54, 0xf6, 0xa1, 0x35, 0xd2, 0x8f, 0xa4, 0x4e, 0xd2, 0x02, 0x49, 0x10, + 0x30, 0xa9, 0x0f, 0xbd, 0x08, 0x6b, 0x72, 0x64, 0x13, 0xa6, 0x48, 0x96, 0xbb, 0xb2, 0x2c, 0x04, + 0xbd, 0xb4, 0xc7, 0x9e, 0xda, 0xfe, 0x8f, 0xfe, 0x84, 0x5e, 0x0b, 0xf4, 0x9a, 0x7b, 0x81, 0x02, + 0xbd, 0xf6, 0x3f, 0x14, 0xbb, 0xdc, 0xa5, 0x48, 0x89, 0x0e, 0x8c, 0xf6, 0x24, 0xce, 0xcc, 0x33, + 0xbb, 0x33, 0xcf, 0x33, 0x3b, 0x10, 0x34, 0x1c, 0x9f, 0x7a, 0x71, 0x37, 0x8a, 0x43, 0x1e, 0x92, + 0xaa, 0x34, 0xa2, 0x93, 0xce, 0xce, 0x69, 0x18, 0x9e, 0xfa, 0xd8, 0xa3, 0x91, 0xd7, 0xa3, 0x41, + 0x10, 0x72, 0xca, 0xbd, 0x30, 0x60, 0x09, 0xac, 0xb3, 0xad, 0xa2, 0xd2, 0x3a, 0x19, 0x0f, 0x7b, + 0x38, 0x8a, 0xf8, 0x54, 0x05, 0x6f, 0xcd, 0x07, 0xb9, 0x37, 0x42, 0xc6, 0xe9, 0x28, 0x4a, 0x00, + 0xd6, 0x4f, 0x25, 0x58, 0x39, 0x1e, 0xfb, 0x01, 0xc6, 0xf4, 0xc4, 0xf3, 0x3d, 0x3e, 0x25, 0x04, + 0x96, 0x02, 0x3a, 0x42, 0xd3, 0xd8, 0x33, 0xee, 0xd6, 0x6d, 0xf9, 0x4d, 0xfe, 0x0f, 0xab, 0xe2, + 0x97, 0x45, 0xd4, 0xc1, 0x81, 0x8c, 0x96, 0x64, 0x74, 0x25, 0xf5, 0x3e, 0x17, 0xb0, 0x3d, 0x68, + 0xb8, 0xc8, 0x9c, 0xd8, 0x8b, 0x44, 0x81, 0x66, 0x59, 0x62, 0xb2, 0x2e, 0x71, 0xb8, 0xef, 0x05, + 0xe7, 0xe6, 0x52, 0x72, 0xb8, 0xf8, 0x26, 0x1d, 0xa8, 0x31, 0xbc, 0xc0, 0xd8, 0xe3, 0x53, 0xb3, + 0x22, 0xfd, 0xa9, 0x2d, 0x62, 0x23, 0xe4, 0xd4, 0xa5, 0x9c, 0x9a, 0xcb, 0x49, 0x4c, 0xdb, 0x64, + 0x0b, 0x6a, 0x43, 0xef, 0x12, 0xdd, 0xc1, 0xc9, 0xd4, 0xac, 0xca, 0x58, 0x55, 0xda, 0x0f, 0xa7, + 0xe4, 0x63, 0xd8, 0xa0, 0xc3, 0x21, 0x3a, 0x1c, 0xdd, 0xc1, 0x05, 0xc6, 0x4c, 0xd0, 0x65, 0xd6, + 0xf6, 0xca, 0x77, 0x1b, 0xfd, 0xf5, 0xae, 0xa2, 0xb5, 0xfb, 0x18, 0x29, 0x1f, 0xc7, 0x68, 0xaf, + 0x6b, 0xe8, 0xb1, 0x42, 0x5a, 0x3f, 0x18, 0xd0, 0xf8, 0x5c, 0xa0, 0x5e, 0x72, 0xca, 0xc7, 0x8c, + 0x98, 0x50, 0xf5, 0x3d, 0xc6, 0x31, 0x66, 0xa6, 0xb1, 0x57, 0x16, 0x17, 0x29, 0x93, 0xec, 0x40, + 0xdd, 0x45, 0x8e, 0x0e, 0x0f, 0x63, 0x66, 0x96, 0x64, 0x6c, 0xe6, 0x20, 0x47, 0xb0, 0xee, 0x53, + 0xc6, 0x07, 0xe3, 0xc8, 0xa5, 0x1c, 0x07, 0x82, 0x7b, 0x49, 0x4a, 0xa3, 0xdf, 0xe9, 0x26, 0xc2, + 0x74, 0xb5, 0x30, 0xdd, 0x57, 0x5a, 0x18, 0x7b, 0x55, 0xe4, 0x7c, 0x25, 0x53, 0x84, 0xd3, 0xfa, + 0xcd, 0x80, 0xaa, 0xaa, 0xf5, 0xbf, 0x88, 0x63, 0x42, 0x55, 0x51, 0xa1, 0x84, 0xd1, 0xa6, 0x38, + 0x40, 0x7d, 0x0e, 0x86, 0x61, 0x3c, 0xa2, 0x5c, 0xc9, 0xb3, 0xa2, 0xbc, 0x8f, 0xa5, 0x93, 0x7c, + 0x06, 0x6b, 0x17, 0x99, 0x49, 0xf1, 0x90, 0x99, 0x15, 0x49, 0x69, 0x3b, 0xa5, 0x34, 0x37, 0x49, + 0xf6, 0x3c, 0xdc, 0xfa, 0xdd, 0x80, 0xda, 0x83, 0xc0, 0x41, 0xc6, 0xe3, 0xe2, 0x39, 0x7b, 0x0f, + 0x6a, 0xc3, 0xa4, 0xd3, 0x84, 0xcd, 0x22, 0xb9, 0x52, 0x04, 0xb9, 0x03, 0xcb, 0x3e, 0x9d, 0x0a, + 0x55, 0xca, 0x12, 0xbb, 0x9a, 0x62, 0x9f, 0x0a, 0xb7, 0xad, 0xa2, 0xe4, 0x1d, 0x58, 0x63, 0x0e, + 0x0d, 0x02, 0x74, 0x07, 0x5a, 0xc6, 0x25, 0x29, 0xd5, 0xaa, 0x72, 0x3f, 0x55, 0x6a, 0xbe, 0x0b, + 0x1b, 0x1a, 0x38, 0x53, 0xb5, 0x22, 0xa1, 0xeb, 0x2a, 0x70, 0xa4, 0xfd, 0xd6, 0x36, 0x54, 0xe4, + 0x35, 0xa2, 0x91, 0x33, 0xca, 0xce, 0x74, 0x23, 0xe2, 0xdb, 0xfa, 0xc3, 0x80, 0xe6, 0xf3, 0x90, + 0x7b, 0x43, 0xcf, 0xa1, 0x7a, 0xf0, 0x17, 0xba, 0x35, 0xa1, 0xea, 0xc4, 0x48, 0x39, 0xba, 0x4a, + 0x31, 0x6d, 0x8a, 0xb1, 0x0f, 0x64, 0x36, 0xba, 0x4a, 0xac, 0xd4, 0x16, 0x59, 0x2e, 0xfa, 0x28, + 0xb2, 0x12, 0x99, 0xb4, 0x49, 0x0e, 0xa0, 0x1c, 0xfa, 0xae, 0x7c, 0x43, 0x8d, 0xbe, 0x95, 0x92, + 0xf1, 0x82, 0x9e, 0xa2, 0xab, 0x95, 0xf1, 0x51, 0x09, 0xe0, 0x21, 0xb3, 0x05, 0x5c, 0x64, 0x05, + 0x38, 0x91, 0xaf, 0xeb, 0x9a, 0x59, 0x01, 0x4e, 0xac, 0x4f, 0x61, 0xf3, 0xcb, 0xc0, 0xc5, 0x4b, + 0x74, 0xb5, 0xa0, 0x72, 0xc8, 0x5a, 0x50, 0xf1, 0x84, 0x5b, 0xf6, 0x59, 0xb1, 0x13, 0x23, 0x6d, + 0xbe, 0x34, 0x6b, 0xde, 0xfa, 0xdb, 0x80, 0xad, 0x2b, 0xef, 0x20, 0xb7, 0xa1, 0xe9, 0x8c, 0xe3, + 0x18, 0x03, 0x3e, 0x88, 0xe8, 0xa9, 0xa6, 0xad, 0xa1, 0x7c, 0x22, 0x8f, 0x6c, 0x43, 0x3d, 0xc0, + 0x4b, 0x15, 0x2f, 0x29, 0x92, 0xf0, 0x32, 0x09, 0xb6, 0xa0, 0xe2, 0x7b, 0x23, 0x8f, 0x4b, 0xf6, + 0x2a, 0x76, 0x62, 0x90, 0xfb, 0xb0, 0x92, 0x1d, 0xc9, 0xa9, 0x24, 0xf0, 0xea, 0xf9, 0xcd, 0x83, + 0xc9, 0x7d, 0x00, 0x9a, 0x56, 0xa8, 0x46, 0x7f, 0x27, 0x4d, 0x2d, 0x60, 0xc3, 0xce, 0xe0, 0xad, + 0x37, 0x25, 0xd8, 0x7c, 0x11, 0x32, 0xae, 0x01, 0x36, 0x7e, 0x33, 0x46, 0xc6, 0xc9, 0xff, 0x60, + 0x45, 0xa1, 0xa6, 0x83, 0xcc, 0x84, 0x34, 0x69, 0x96, 0xd6, 0x36, 0x2c, 0xab, 0x97, 0x99, 0x34, + 0xaa, 0x2c, 0xf2, 0xc9, 0xdc, 0x0b, 0xb8, 0x33, 0x93, 0x6f, 0xf1, 0x2a, 0xe9, 0xcb, 0xbd, 0x8c, + 0xce, 0xaf, 0x06, 0xd4, 0x53, 0x6f, 0xd1, 0x20, 0x0b, 0x5f, 0x44, 0xf9, 0x99, 0x96, 0x4e, 0x7c, + 0x93, 0x67, 0x50, 0x3d, 0x43, 0xea, 0xce, 0xae, 0xdd, 0xbf, 0xde, 0xb5, 0xdd, 0x2f, 0x92, 0xac, + 0x47, 0x81, 0x88, 0xea, 0x33, 0x3a, 0x87, 0xd0, 0xcc, 0x06, 0xc8, 0x3a, 0x94, 0xcf, 0x71, 0xaa, + 0xaa, 0x10, 0x9f, 0x42, 0xcd, 0x0b, 0xea, 0x8f, 0xb5, 0xcc, 0x89, 0x71, 0x58, 0xfa, 0xd0, 0xb0, + 0x8e, 0xa0, 0x95, 0xbf, 0x92, 0x45, 0x61, 0xc0, 0xc4, 0x22, 0x59, 0x66, 0x72, 0x77, 0xcb, 0x63, + 0x1a, 0xfd, 0x56, 0x5a, 0x61, 0x66, 0xaf, 0xdb, 0x0a, 0x63, 0xfd, 0x68, 0x00, 0x79, 0x82, 0xff, + 0x4e, 0x9a, 0x0f, 0xa0, 0x35, 0xf1, 0xf8, 0xd9, 0x60, 0x7e, 0x35, 0x8a, 0x52, 0x6b, 0xf6, 0xa6, + 0x88, 0x1d, 0xe7, 0x43, 0xe2, 0x5c, 0x99, 0x92, 0xae, 0xba, 0xb2, 0xc4, 0x36, 0x85, 0x53, 0x6d, + 0x39, 0x66, 0xc5, 0xb0, 0x99, 0x2b, 0x49, 0x35, 0xf6, 0x3e, 0xd4, 0xf4, 0xf5, 0xaa, 0xb5, 0x8d, + 0xb4, 0xb5, 0x14, 0x9c, 0x42, 0x32, 0x3c, 0x94, 0xae, 0xc1, 0xc3, 0x2f, 0x06, 0xb4, 0x9f, 0x20, + 0xcf, 0x2e, 0x2e, 0xcd, 0xc5, 0x01, 0xb4, 0x43, 0xdf, 0xcd, 0x75, 0x39, 0xcd, 0x3e, 0xcd, 0x56, + 0xe8, 0xbb, 0xb9, 0xd7, 0x23, 0x9f, 0xe1, 0x01, 0xb4, 0x03, 0x9c, 0x14, 0x65, 0x25, 0x4a, 0xb6, + 0x02, 0x9c, 0x2c, 0x66, 0x15, 0x3f, 0x5e, 0xbd, 0x44, 0x96, 0x32, 0x4b, 0xe4, 0x15, 0xdc, 0x5c, + 0xa8, 0x57, 0x11, 0xf5, 0x11, 0x34, 0x83, 0x8c, 0x5f, 0x91, 0x75, 0x23, 0xed, 0x3f, 0x97, 0x94, + 0x83, 0x5a, 0xfb, 0xb0, 0xfb, 0x8c, 0xc6, 0xe7, 0x59, 0xc4, 0x03, 0x66, 0x23, 0x75, 0x35, 0x19, + 0x05, 0xcb, 0xbc, 0xff, 0xa7, 0x01, 0x6b, 0x5a, 0x80, 0x97, 0x18, 0x5f, 0x78, 0x0e, 0x12, 0x0a, + 0xcd, 0xec, 0x74, 0x92, 0x9d, 0xb7, 0xbd, 0x93, 0xce, 0xee, 0x15, 0xd1, 0xa4, 0x21, 0xab, 0xf5, + 0xdd, 0x9b, 0xbf, 0x7e, 0x2e, 0xad, 0x5a, 0xf5, 0x9e, 0x56, 0xf7, 0xd0, 0xb8, 0x47, 0xce, 0xa1, + 0x91, 0x19, 0x13, 0xb2, 0x9d, 0x9e, 0xb1, 0x38, 0xcf, 0x9d, 0x9d, 0xe2, 0xa0, 0x3a, 0xff, 0xb6, + 0x3c, 0x7f, 0x9b, 0x6c, 0xa5, 0xe7, 0xf7, 0x5e, 0xe7, 0xc6, 0xff, 0xdb, 0xfe, 0xf7, 0x25, 0xd8, + 0xcc, 0xb2, 0xa2, 0xfb, 0x64, 0xb0, 0x36, 0x27, 0x03, 0xb9, 0x95, 0xbd, 0xab, 0x60, 0xa0, 0x3a, + 0x7b, 0x57, 0x03, 0x54, 0x41, 0xbb, 0xb2, 0xa0, 0x9b, 0xe4, 0x46, 0x2f, 0xab, 0x0e, 0xeb, 0xbd, + 0x96, 0xc5, 0x90, 0x09, 0xb4, 0x8b, 0x55, 0x22, 0xb3, 0x2d, 0xf8, 0x56, 0x19, 0x3b, 0xed, 0x85, + 0x3f, 0x61, 0x8f, 0xc4, 0x5f, 0x67, 0x7d, 0xf1, 0xbd, 0xe2, 0x8b, 0x1f, 0xd6, 0xbf, 0xd6, 0xff, + 0xcc, 0x4f, 0x96, 0x65, 0xe6, 0xfe, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6e, 0xc1, 0x1d, 0xff, + 0xb8, 0x0b, 0x00, 0x00, } diff --git a/api/v2/clairpb/clair.pb.gw.go b/api/v2/clairpb/clair.pb.gw.go index f45c4f86..21bfdf12 100644 --- a/api/v2/clairpb/clair.pb.gw.go +++ b/api/v2/clairpb/clair.pb.gw.go @@ -112,8 +112,8 @@ func request_NotificationService_GetNotification_0(ctx context.Context, marshale } -func request_NotificationService_DeleteNotification_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DeleteNotificationRequest +func request_NotificationService_MarkNotificationAsRead_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq MarkNotificationAsReadRequest var metadata runtime.ServerMetadata var ( @@ -134,7 +134,7 @@ func request_NotificationService_DeleteNotification_0(ctx context.Context, marsh return nil, metadata, err } - msg, err := client.DeleteNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.MarkNotificationAsRead(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } @@ -301,7 +301,7 @@ func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeM }) - mux.Handle("DELETE", pattern_NotificationService_DeleteNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("DELETE", pattern_NotificationService_MarkNotificationAsRead_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(ctx) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { @@ -319,14 +319,14 @@ func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeM runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_NotificationService_DeleteNotification_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_NotificationService_MarkNotificationAsRead_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_NotificationService_DeleteNotification_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_NotificationService_MarkNotificationAsRead_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -336,11 +336,11 @@ func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeM var ( pattern_NotificationService_GetNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, "")) - pattern_NotificationService_DeleteNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, "")) + pattern_NotificationService_MarkNotificationAsRead_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"notifications", "name"}, "")) ) var ( forward_NotificationService_GetNotification_0 = runtime.ForwardResponseMessage - forward_NotificationService_DeleteNotification_0 = runtime.ForwardResponseMessage + forward_NotificationService_MarkNotificationAsRead_0 = runtime.ForwardResponseMessage ) diff --git a/api/v2/clairpb/clair.proto b/api/v2/clairpb/clair.proto index c2e8fb06..8d704230 100644 --- a/api/v2/clairpb/clair.proto +++ b/api/v2/clairpb/clair.proto @@ -18,6 +18,7 @@ option go_package = "clairpb"; package clairpb; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; message Vulnerability { string name = 1; @@ -26,60 +27,72 @@ message Vulnerability { string link = 4; string severity = 5; string metadata = 6; + // fixed_by exists when vulnerability is under feature. string fixed_by = 7; - repeated Feature fixed_in_features = 8; + // affected_versions exists when vulnerability is under notification. + repeated Feature affected_versions = 8; } -message Feature { +message ClairStatus { + // listers and detectors are processors implemented in this Clair and used to + // scan ancestries + repeated string listers = 1; + repeated string detectors = 2; + google.protobuf.Timestamp last_update_time = 3; +} + +message Feature{ string name = 1; string namespace_name = 2; string version = 3; string version_format = 4; - string added_by = 5; - repeated Vulnerability vulnerabilities = 6; + repeated Vulnerability vulnerabilities = 5; } message Ancestry { string name = 1; - int32 engine_version = 2; + repeated Feature features = 2; repeated Layer layers = 3; -} -message LayersIntroducingVulnerabilty { - Vulnerability vulnerability = 1; - repeated OrderedLayerName layers = 2; -} - -message OrderedLayerName { - int32 index = 1; - string layer_name = 2; + // scanned_listers and scanned_detectors are used to scan this ancestry, it + // may be different from listers and detectors in ClairStatus since the + // ancestry could be scanned by previous version of Clair. + repeated string scanned_listers = 4; + repeated string scanned_detectors = 5; } message Layer { - string name = 1; - repeated string namespace_names = 2; + string hash = 1; } message Notification { string name = 1; string created = 2; string notified = 3; - string deleted = 4; - int32 limit = 5; - Page page = 6; + string deleted = 4; + PagedVulnerableAncestries old = 5; + PagedVulnerableAncestries new = 6; } -message Page { - string this_token = 1; - string next_token = 2; - LayersIntroducingVulnerabilty old = 3; - LayersIntroducingVulnerabilty new = 4; +message IndexedAncestryName { + // index is unique to name in all streams simultaneously streamed, increasing + // and larger than all indexes in previous page in same stream. + int32 index = 1; + string name = 2; } +message PagedVulnerableAncestries { + string current_page = 1; + // if next_page is empty, it signals the end of all pages. + string next_page = 2; + int32 limit = 3; + Vulnerability vulnerability = 4; + repeated IndexedAncestryName ancestries = 5; +} message PostAncestryRequest { message PostLayer { - string name = 1; + string hash = 1; string path = 2; map headers = 3; } @@ -89,7 +102,7 @@ message PostAncestryRequest { } message PostAncestryResponse { - int32 engine_version = 1; + ClairStatus status = 1; } message GetAncestryRequest { @@ -100,25 +113,25 @@ message GetAncestryRequest { message GetAncestryResponse { Ancestry ancestry = 1; - repeated Feature features = 2; + ClairStatus status = 2; } message GetNotificationRequest { - string page = 1; - int32 limit = 2; - string name = 3; + // if the vulnerability_page is empty, it implies the first page. + string old_vulnerability_page = 1; + string new_vulnerability_page = 2; + int32 limit = 3; + string name = 4; } message GetNotificationResponse { Notification notification = 1; } -message DeleteNotificationRequest { +message MarkNotificationAsReadRequest { string name = 1; } - - service AncestryService{ rpc PostAncestry(PostAncestryRequest) returns (PostAncestryResponse) { option (google.api.http) = { @@ -141,7 +154,7 @@ service NotificationService{ }; } - rpc DeleteNotification(DeleteNotificationRequest) returns (google.protobuf.Empty) { + rpc MarkNotificationAsRead(MarkNotificationAsReadRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/notifications/{name}" }; diff --git a/api/v2/clairpb/clair.swagger.json b/api/v2/clairpb/clair.swagger.json index 99623387..3e54a8a2 100644 --- a/api/v2/clairpb/clair.swagger.json +++ b/api/v2/clairpb/clair.swagger.json @@ -98,7 +98,14 @@ "type": "string" }, { - "name": "page", + "name": "old_vulnerability_page", + "description": "if the vulnerability_page is empty, it implies the first page.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "new_vulnerability_page", "in": "query", "required": false, "type": "string" @@ -116,7 +123,7 @@ ] }, "delete": { - "operationId": "DeleteNotification", + "operationId": "MarkNotificationAsRead", "responses": { "200": { "description": "", @@ -143,7 +150,7 @@ "PostAncestryRequestPostLayer": { "type": "object", "properties": { - "name": { + "hash": { "type": "string" }, "path": { @@ -163,15 +170,52 @@ "name": { "type": "string" }, - "engine_version": { - "type": "integer", - "format": "int32" + "features": { + "type": "array", + "items": { + "$ref": "#/definitions/clairpbFeature" + } }, "layers": { "type": "array", "items": { "$ref": "#/definitions/clairpbLayer" } + }, + "scanned_listers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "scanned_listers and scanned_detectors are used to scan this ancestry, it\nmay be different from listers and detectors in ClairStatus since the\nancestry could be scanned by previous version of Clair." + }, + "scanned_detectors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "clairpbClairStatus": { + "type": "object", + "properties": { + "listers": { + "type": "array", + "items": { + "type": "string" + }, + "title": "listers and detectors are processors implemented in this Clair and used to\nscan ancestries" + }, + "detectors": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_update_time": { + "type": "string", + "format": "date-time" } } }, @@ -190,9 +234,6 @@ "version_format": { "type": "string" }, - "added_by": { - "type": "string" - }, "vulnerabilities": { "type": "array", "items": { @@ -207,11 +248,8 @@ "ancestry": { "$ref": "#/definitions/clairpbAncestry" }, - "features": { - "type": "array", - "items": { - "$ref": "#/definitions/clairpbFeature" - } + "status": { + "$ref": "#/definitions/clairpbClairStatus" } } }, @@ -223,31 +261,24 @@ } } }, - "clairpbLayer": { + "clairpbIndexedAncestryName": { "type": "object", "properties": { + "index": { + "type": "integer", + "format": "int32", + "description": "index is unique to name in all streams simultaneously streamed, increasing\nand larger than all indexes in previous page in same stream." + }, "name": { "type": "string" - }, - "namespace_names": { - "type": "array", - "items": { - "type": "string" - } } } }, - "clairpbLayersIntroducingVulnerabilty": { + "clairpbLayer": { "type": "object", "properties": { - "vulnerability": { - "$ref": "#/definitions/clairpbVulnerability" - }, - "layers": { - "type": "array", - "items": { - "$ref": "#/definitions/clairpbOrderedLayerName" - } + "hash": { + "type": "string" } } }, @@ -266,41 +297,36 @@ "deleted": { "type": "string" }, - "limit": { - "type": "integer", - "format": "int32" - }, - "page": { - "$ref": "#/definitions/clairpbPage" - } - } - }, - "clairpbOrderedLayerName": { - "type": "object", - "properties": { - "index": { - "type": "integer", - "format": "int32" + "old": { + "$ref": "#/definitions/clairpbPagedVulnerableAncestries" }, - "layer_name": { - "type": "string" + "new": { + "$ref": "#/definitions/clairpbPagedVulnerableAncestries" } } }, - "clairpbPage": { + "clairpbPagedVulnerableAncestries": { "type": "object", "properties": { - "this_token": { + "current_page": { "type": "string" }, - "next_token": { - "type": "string" + "next_page": { + "type": "string", + "description": "if next_page is empty, it signals the end of all pages." }, - "old": { - "$ref": "#/definitions/clairpbLayersIntroducingVulnerabilty" + "limit": { + "type": "integer", + "format": "int32" }, - "new": { - "$ref": "#/definitions/clairpbLayersIntroducingVulnerabilty" + "vulnerability": { + "$ref": "#/definitions/clairpbVulnerability" + }, + "ancestries": { + "type": "array", + "items": { + "$ref": "#/definitions/clairpbIndexedAncestryName" + } } } }, @@ -324,9 +350,8 @@ "clairpbPostAncestryResponse": { "type": "object", "properties": { - "engine_version": { - "type": "integer", - "format": "int32" + "status": { + "$ref": "#/definitions/clairpbClairStatus" } } }, @@ -352,13 +377,15 @@ "type": "string" }, "fixed_by": { - "type": "string" + "type": "string", + "description": "fixed_by exists when vulnerability is under feature." }, - "fixed_in_features": { + "affected_versions": { "type": "array", "items": { "$ref": "#/definitions/clairpbFeature" - } + }, + "description": "affected_versions exists when vulnerability is under notification." } } }, diff --git a/api/v2/clairpb/convert.go b/api/v2/clairpb/convert.go index 34e7eb6e..a3584587 100644 --- a/api/v2/clairpb/convert.go +++ b/api/v2/clairpb/convert.go @@ -18,76 +18,81 @@ import ( "encoding/json" "fmt" - "github.com/coreos/clair/api/token" "github.com/coreos/clair/database" "github.com/coreos/clair/ext/versionfmt" ) -func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, pageToken string, nextPage database.VulnerabilityNotificationPageNumber, key string) (*Notification, error) { - var oldVuln *LayersIntroducingVulnerabilty - if dbNotification.OldVulnerability != nil { - v, err := LayersIntroducingVulnerabiltyFromDatabaseModel(*dbNotification.OldVulnerability) - if err != nil { - return nil, err - } - oldVuln = v +// PagedVulnerableAncestriesFromDatabaseModel converts database +// PagedVulnerableAncestries to api PagedVulnerableAncestries and assigns +// indexes to ancestries. +func PagedVulnerableAncestriesFromDatabaseModel(dbVuln *database.PagedVulnerableAncestries) (*PagedVulnerableAncestries, error) { + if dbVuln == nil { + return nil, nil } - var newVuln *LayersIntroducingVulnerabilty - if dbNotification.NewVulnerability != nil { - v, err := LayersIntroducingVulnerabiltyFromDatabaseModel(*dbNotification.NewVulnerability) - if err != nil { - return nil, err - } - newVuln = v + vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability) + if err != nil { + return nil, err + } + + next := "" + if !dbVuln.End { + next = string(dbVuln.Next) } - var nextPageStr string - if nextPage != database.NoVulnerabilityNotificationPage { - nextPageBytes, _ := token.Marshal(nextPage, key) - nextPageStr = string(nextPageBytes) + vulnAncestry := PagedVulnerableAncestries{ + Vulnerability: vuln, + CurrentPage: string(dbVuln.Current), + NextPage: next, + Limit: int32(dbVuln.Limit), } - var created, notified, deleted string + for index, ancestryName := range dbVuln.Affected { + indexedAncestry := IndexedAncestryName{ + Name: ancestryName, + Index: int32(index), + } + vulnAncestry.Ancestries = append(vulnAncestry.Ancestries, &indexedAncestry) + } + + return &vulnAncestry, nil +} + +// NotificationFromDatabaseModel converts database notification, old and new +// vulnerabilities' paged vulnerable ancestries to be api notification. +func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotificationWithVulnerable) (*Notification, error) { + var ( + noti Notification + err error + ) + + noti.Name = dbNotification.Name if !dbNotification.Created.IsZero() { - created = fmt.Sprintf("%d", dbNotification.Created.Unix()) + noti.Created = fmt.Sprintf("%d", dbNotification.Created.Unix()) } + if !dbNotification.Notified.IsZero() { - notified = fmt.Sprintf("%d", dbNotification.Notified.Unix()) + noti.Notified = fmt.Sprintf("%d", dbNotification.Notified.Unix()) } + if !dbNotification.Deleted.IsZero() { - deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix()) - } - - return &Notification{ - Name: dbNotification.Name, - Created: created, - Notified: notified, - Deleted: deleted, - Limit: int32(limit), - Page: &Page{ - ThisToken: pageToken, - NextToken: nextPageStr, - Old: oldVuln, - New: newVuln, - }, - }, nil -} + noti.Deleted = fmt.Sprintf("%d", dbNotification.Deleted.Unix()) + } -func LayersIntroducingVulnerabiltyFromDatabaseModel(dbVuln database.Vulnerability) (*LayersIntroducingVulnerabilty, error) { - vuln, err := VulnerabilityFromDatabaseModel(dbVuln, true) + noti.Old, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.Old) if err != nil { return nil, err } - var orderedLayers []*OrderedLayerName - return &LayersIntroducingVulnerabilty{ - Vulnerability: vuln, - Layers: orderedLayers, - }, nil + noti.New, err = PagedVulnerableAncestriesFromDatabaseModel(dbNotification.New) + if err != nil { + return nil, err + } + + return ¬i, nil } -func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn bool) (*Vulnerability, error) { +func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability) (*Vulnerability, error) { metaString := "" if dbVuln.Metadata != nil { metadataByte, err := json.Marshal(dbVuln.Metadata) @@ -97,69 +102,54 @@ func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn b metaString = string(metadataByte) } - vuln := Vulnerability{ + return &Vulnerability{ Name: dbVuln.Name, NamespaceName: dbVuln.Namespace.Name, Description: dbVuln.Description, Link: dbVuln.Link, Severity: string(dbVuln.Severity), Metadata: metaString, - } - - if dbVuln.FixedBy != versionfmt.MaxVersion { - vuln.FixedBy = dbVuln.FixedBy - } - - if withFixedIn { - for _, dbFeatureVersion := range dbVuln.FixedIn { - f, err := FeatureFromDatabaseModel(dbFeatureVersion, false) - if err != nil { - return nil, err - } + }, nil +} - vuln.FixedInFeatures = append(vuln.FixedInFeatures, f) - } +func VulnerabilityWithFixedInFromDatabaseModel(dbVuln database.VulnerabilityWithFixedIn) (*Vulnerability, error) { + vuln, err := VulnerabilityFromDatabaseModel(dbVuln.Vulnerability) + if err != nil { + return nil, err } - return &vuln, nil + vuln.FixedBy = dbVuln.FixedInVersion + return vuln, nil } -func LayerFromDatabaseModel(dbLayer database.Layer) *Layer { - layer := Layer{ - Name: dbLayer.Name, +// AncestryFromDatabaseModel converts database ancestry to api ancestry. +func AncestryFromDatabaseModel(dbAncestry database.Ancestry) *Ancestry { + ancestry := &Ancestry{ + Name: dbAncestry.Name, } - for _, ns := range dbLayer.Namespaces { - layer.NamespaceNames = append(layer.NamespaceNames, ns.Name) + for _, layer := range dbAncestry.Layers { + ancestry.Layers = append(ancestry.Layers, LayerFromDatabaseModel(layer)) } + return ancestry +} +// LayerFromDatabaseModel converts database layer to api layer. +func LayerFromDatabaseModel(dbLayer database.Layer) *Layer { + layer := Layer{Hash: dbLayer.Hash} return &layer } -func FeatureFromDatabaseModel(fv database.FeatureVersion, withVulnerabilities bool) (*Feature, error) { - version := fv.Version +// NamespacedFeatureFromDatabaseModel converts database namespacedFeature to api Feature. +func NamespacedFeatureFromDatabaseModel(feature database.NamespacedFeature) *Feature { + version := feature.Feature.Version if version == versionfmt.MaxVersion { version = "None" } - f := &Feature{ - Name: fv.Feature.Name, - NamespaceName: fv.Feature.Namespace.Name, - VersionFormat: fv.Feature.Namespace.VersionFormat, - Version: version, - AddedBy: fv.AddedBy.Name, - } - - if withVulnerabilities { - for _, dbVuln := range fv.AffectedBy { - // VulnerabilityFromDatabaseModel should be called without FixedIn, - // Otherwise it might cause infinite loop - vul, err := VulnerabilityFromDatabaseModel(dbVuln, false) - if err != nil { - return nil, err - } - f.Vulnerabilities = append(f.Vulnerabilities, vul) - } + return &Feature{ + Name: feature.Feature.Name, + NamespaceName: feature.Namespace.Name, + VersionFormat: feature.Namespace.VersionFormat, + Version: version, } - - return f, nil } diff --git a/api/v2/rpc.go b/api/v2/rpc.go index 9ff840cb..9225a8fe 100644 --- a/api/v2/rpc.go +++ b/api/v2/rpc.go @@ -17,24 +17,21 @@ package v2 import ( "fmt" + "github.com/golang/protobuf/ptypes" google_protobuf1 "github.com/golang/protobuf/ptypes/empty" - log "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/coreos/clair" - "github.com/coreos/clair/api/token" pb "github.com/coreos/clair/api/v2/clairpb" "github.com/coreos/clair/database" "github.com/coreos/clair/pkg/commonerr" - "github.com/coreos/clair/pkg/tarutil" ) // NotificationServer implements NotificationService interface for serving RPC. type NotificationServer struct { - Store database.Datastore - PaginationKey string + Store database.Datastore } // AncestryServer implements AncestryService interface for serving RPC. @@ -46,211 +43,211 @@ type AncestryServer struct { func (s *AncestryServer) PostAncestry(ctx context.Context, req *pb.PostAncestryRequest) (*pb.PostAncestryResponse, error) { ancestryName := req.GetAncestryName() if ancestryName == "" { - return nil, status.Error(codes.InvalidArgument, "Failed to provide proper ancestry name") + return nil, status.Error(codes.InvalidArgument, "ancestry name should not be empty") } layers := req.GetLayers() if len(layers) == 0 { - return nil, status.Error(codes.InvalidArgument, "At least one layer should be provided for an ancestry") + return nil, status.Error(codes.InvalidArgument, "ancestry should have at least one layer") + } + + ancestryFormat := req.GetFormat() + if ancestryFormat == "" { + return nil, status.Error(codes.InvalidArgument, "ancestry format should not be empty") } - var currentName, parentName, rootName string - for i, layer := range layers { + ancestryLayers := []clair.LayerRequest{} + for _, layer := range layers { if layer == nil { - err := status.Error(codes.InvalidArgument, "Failed to provide layer") - return nil, s.rollBackOnError(err, currentName, rootName) + err := status.Error(codes.InvalidArgument, "ancestry layer is invalid") + return nil, err } - // TODO(keyboardnerd): after altering the database to support ancestry, - // we should use the ancestry name and index as key instead of - // the amalgamation of ancestry name of index - // Hack: layer name is [ancestryName]-[index] except the tail layer, - // tail layer name is [ancestryName] - if i == len(layers)-1 { - currentName = ancestryName - } else { - currentName = fmt.Sprintf("%s-%d", ancestryName, i) + if layer.GetHash() == "" { + return nil, status.Error(codes.InvalidArgument, "ancestry layer hash should not be empty") } - // if rootName is unset, this is the first iteration over the layers and - // the current layer is the root of the ancestry - if rootName == "" { - rootName = currentName + if layer.GetPath() == "" { + return nil, status.Error(codes.InvalidArgument, "ancestry layer path should not be empty") } - err := clair.ProcessLayer(s.Store, req.GetFormat(), currentName, parentName, layer.GetPath(), layer.GetHeaders()) - if err != nil { - return nil, s.rollBackOnError(err, currentName, rootName) - } + ancestryLayers = append(ancestryLayers, clair.LayerRequest{ + Hash: layer.Hash, + Headers: layer.Headers, + Path: layer.Path, + }) + } - // Now that the current layer is processed, set the parentName for the - // next iteration. - parentName = currentName + err := clair.ProcessAncestry(s.Store, ancestryFormat, ancestryName, ancestryLayers) + if err != nil { + return nil, status.Error(codes.Internal, "ancestry is failed to be processed: "+err.Error()) } - return &pb.PostAncestryResponse{ - EngineVersion: clair.Version, - }, nil + clairStatus, err := s.getClairStatus() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &pb.PostAncestryResponse{Status: clairStatus}, nil } -// GetAncestry implements retrieving an ancestry via the Clair gRPC service. -func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) { - if req.GetAncestryName() == "" { - return nil, status.Errorf(codes.InvalidArgument, "invalid get ancestry request") +func (s *AncestryServer) getClairStatus() (*pb.ClairStatus, error) { + status := &pb.ClairStatus{ + Listers: clair.Processors.Listers, + Detectors: clair.Processors.Detectors, } - // TODO(keyboardnerd): after altering the database to support ancestry, this - // function is iteratively querying for for r.GetIndex() th parent of the - // requested layer until the indexed layer is found or index is out of bound - // this is a hack and will be replaced with one query - ancestry, features, err := s.getAncestry(req.GetAncestryName(), req.GetWithFeatures(), req.GetWithVulnerabilities()) - if err == commonerr.ErrNotFound { - return nil, status.Error(codes.NotFound, err.Error()) - } else if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + t, firstUpdate, err := clair.GetLastUpdateTime(s.Store) + if err != nil { + return nil, err + } + if firstUpdate { + return status, nil } - return &pb.GetAncestryResponse{ - Ancestry: ancestry, - Features: features, - }, nil + status.LastUpdateTime, err = ptypes.TimestampProto(t) + if err != nil { + return nil, err + } + return status, nil } -// GetNotification implements retrieving a notification via the Clair gRPC -// service. -func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) { - if req.GetName() == "" { - return nil, status.Error(codes.InvalidArgument, "Failed to provide notification name") +// GetAncestry implements retrieving an ancestry via the Clair gRPC service. +func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) { + if req.GetAncestryName() == "" { + return nil, status.Errorf(codes.InvalidArgument, "ancestry name should not be empty") } - if req.GetLimit() <= 0 { - return nil, status.Error(codes.InvalidArgument, "Failed to provide page limit") + tx, err := s.Store.Begin() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) } + defer tx.Rollback() - page := database.VulnerabilityNotificationFirstPage - pageToken := req.GetPage() - if pageToken != "" { - err := token.Unmarshal(pageToken, s.PaginationKey, &page) + ancestry, _, ok, err := tx.FindAncestry(req.GetAncestryName()) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } else if !ok { + return nil, status.Error(codes.NotFound, fmt.Sprintf("requested ancestry '%s' is not found", req.GetAncestryName())) + } + + pbAncestry := pb.AncestryFromDatabaseModel(ancestry) + if req.GetWithFeatures() || req.GetWithVulnerabilities() { + ancestryWFeature, ok, err := tx.FindAncestryFeatures(ancestry.Name) if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "Invalid page format %s", err.Error()) + return nil, status.Error(codes.Internal, err.Error()) } - } else { - pageTokenBytes, err := token.Marshal(page, s.PaginationKey) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "Failed to marshal token: %s", err.Error()) + + if !ok { + return nil, status.Error(codes.NotFound, fmt.Sprintf("requested ancestry '%s' is not found", req.GetAncestryName())) } - pageToken = string(pageTokenBytes) - } + pbAncestry.ScannedDetectors = ancestryWFeature.ProcessedBy.Detectors + pbAncestry.ScannedListers = ancestryWFeature.ProcessedBy.Listers - dbNotification, nextPage, err := s.Store.GetNotification(req.GetName(), int(req.GetLimit()), page) - if err == commonerr.ErrNotFound { - return nil, status.Error(codes.NotFound, err.Error()) - } else if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + if req.GetWithVulnerabilities() { + featureVulnerabilities, err := tx.FindAffectedNamespacedFeatures(ancestryWFeature.Features) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + for _, fv := range featureVulnerabilities { + // Ensure that every feature can be found. + if !fv.Valid { + return nil, status.Error(codes.Internal, "ancestry feature is not found") + } + + pbFeature := pb.NamespacedFeatureFromDatabaseModel(fv.NamespacedFeature) + for _, v := range fv.AffectedBy { + pbVuln, err := pb.VulnerabilityWithFixedInFromDatabaseModel(v) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + pbFeature.Vulnerabilities = append(pbFeature.Vulnerabilities, pbVuln) + } + + pbAncestry.Features = append(pbAncestry.Features, pbFeature) + } + } else { + for _, f := range ancestryWFeature.Features { + pbAncestry.Features = append(pbAncestry.Features, pb.NamespacedFeatureFromDatabaseModel(f)) + } + } } - notification, err := pb.NotificationFromDatabaseModel(dbNotification, int(req.GetLimit()), pageToken, nextPage, s.PaginationKey) + clairStatus, err := s.getClairStatus() if err != nil { return nil, status.Error(codes.Internal, err.Error()) } - return &pb.GetNotificationResponse{Notification: notification}, nil + return &pb.GetAncestryResponse{ + Status: clairStatus, + Ancestry: pbAncestry, + }, nil } -// DeleteNotification implements deleting a notification via the Clair gRPC +// GetNotification implements retrieving a notification via the Clair gRPC // service. -func (s *NotificationServer) DeleteNotification(ctx context.Context, req *pb.DeleteNotificationRequest) (*google_protobuf1.Empty, error) { +func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) { if req.GetName() == "" { - return nil, status.Error(codes.InvalidArgument, "Failed to provide notification name") + return nil, status.Error(codes.InvalidArgument, "notification name should not be empty") } - err := s.Store.DeleteNotification(req.GetName()) - if err == commonerr.ErrNotFound { - return nil, status.Error(codes.NotFound, err.Error()) - } else if err != nil { + if req.GetLimit() <= 0 { + return nil, status.Error(codes.InvalidArgument, "notification page limit should not be empty or less than 1") + } + + tx, err := s.Store.Begin() + if err != nil { return nil, status.Error(codes.Internal, err.Error()) } + defer tx.Rollback() - return &google_protobuf1.Empty{}, nil -} + dbNotification, ok, err := tx.FindVulnerabilityNotification( + req.GetName(), + int(req.GetLimit()), + database.PageNumber(req.GetOldVulnerabilityPage()), + database.PageNumber(req.GetNewVulnerabilityPage()), + ) -// rollBackOnError handles server error and rollback whole ancestry insertion if -// any layer failed to be inserted. -func (s *AncestryServer) rollBackOnError(err error, currentLayerName, rootLayerName string) error { - // if the current layer failed to be inserted and it's the root layer, - // then the ancestry is not yet in the database. - if currentLayerName != rootLayerName { - errrb := s.Store.DeleteLayer(rootLayerName) - if errrb != nil { - return status.Errorf(codes.Internal, errrb.Error()) - } - log.WithField("layer name", currentLayerName).Warnf("Can't process %s: roll back the ancestry", currentLayerName) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) } - if err == tarutil.ErrCouldNotExtract || - err == tarutil.ErrExtractedFileTooBig || - err == clair.ErrUnsupported { - return status.Errorf(codes.InvalidArgument, "unprocessable entity %s", err.Error()) + if !ok { + return nil, status.Error(codes.NotFound, fmt.Sprintf("requested notification '%s' is not found", req.GetName())) } - if _, badreq := err.(*commonerr.ErrBadRequest); badreq { - return status.Error(codes.InvalidArgument, err.Error()) + notification, err := pb.NotificationFromDatabaseModel(dbNotification) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) } - return status.Error(codes.Internal, err.Error()) + return &pb.GetNotificationResponse{Notification: notification}, nil } -// TODO(keyboardnerd): Remove this Legacy compability code once the database is -// revised. -// getAncestry returns an ancestry from database by getting all parents of a -// layer given the layer name, and the layer's feature list if -// withFeature/withVulnerability is turned on. -func (s *AncestryServer) getAncestry(name string, withFeature bool, withVulnerability bool) (ancestry *pb.Ancestry, features []*pb.Feature, err error) { - var ( - layers = []*pb.Layer{} - layer database.Layer - ) - ancestry = &pb.Ancestry{} - - layer, err = s.Store.FindLayer(name, withFeature, withVulnerability) - if err != nil { - return +// MarkNotificationAsRead implements deleting a notification via the Clair gRPC +// service. +func (s *NotificationServer) MarkNotificationAsRead(ctx context.Context, req *pb.MarkNotificationAsReadRequest) (*google_protobuf1.Empty, error) { + if req.GetName() == "" { + return nil, status.Error(codes.InvalidArgument, "notification name should not be empty") } - if withFeature { - for _, fv := range layer.Features { - f, e := pb.FeatureFromDatabaseModel(fv, withVulnerability) - if e != nil { - err = e - return - } - - features = append(features, f) - } + tx, err := s.Store.Begin() + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) } - ancestry.Name = name - ancestry.EngineVersion = int32(layer.EngineVersion) - for name != "" { - layer, err = s.Store.FindLayer(name, false, false) - if err != nil { - return - } - - if layer.Parent != nil { - name = layer.Parent.Name - } else { - name = "" - } - - layers = append(layers, pb.LayerFromDatabaseModel(layer)) + defer tx.Rollback() + err = tx.DeleteNotification(req.GetName()) + if err == commonerr.ErrNotFound { + return nil, status.Error(codes.NotFound, "requested notification \""+req.GetName()+"\" is not found") + } else if err != nil { + return nil, status.Error(codes.Internal, err.Error()) } - // reverse layers to make the root layer at the top - for i, j := 0, len(layers)-1; i < j; i, j = i+1, j-1 { - layers[i], layers[j] = layers[j], layers[i] + if err := tx.Commit(); err != nil { + return nil, status.Error(codes.Internal, err.Error()) } - ancestry.Layers = layers - return + return &google_protobuf1.Empty{}, nil } diff --git a/api/v2/server.go b/api/v2/server.go index 8b153680..95b74330 100644 --- a/api/v2/server.go +++ b/api/v2/server.go @@ -48,7 +48,7 @@ func handleShutdown(err error) { var ( promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "clair_v2_api_response_duration_milliseconds", - Help: "The duration of time it takes to receieve and write a response to an V2 API request", + Help: "The duration of time it takes to receive and write a response to an V2 API request", Buckets: prometheus.ExponentialBuckets(9.375, 2, 10), }, []string{"route", "code"}) ) @@ -57,7 +57,7 @@ func init() { prometheus.MustRegister(promResponseDurationMilliseconds) } -func newGrpcServer(paginationKey string, store database.Datastore, tlsConfig *tls.Config) *grpc.Server { +func newGrpcServer(store database.Datastore, tlsConfig *tls.Config) *grpc.Server { grpcOpts := []grpc.ServerOption{ grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), @@ -69,7 +69,7 @@ func newGrpcServer(paginationKey string, store database.Datastore, tlsConfig *tl grpcServer := grpc.NewServer(grpcOpts...) pb.RegisterAncestryServiceServer(grpcServer, &AncestryServer{Store: store}) - pb.RegisterNotificationServiceServer(grpcServer, &NotificationServer{PaginationKey: paginationKey, Store: store}) + pb.RegisterNotificationServiceServer(grpcServer, &NotificationServer{Store: store}) return grpcServer } @@ -98,11 +98,11 @@ func logHandler(handler http.Handler) http.Handler { } log.WithFields(log.Fields{ - "remote addr": r.RemoteAddr, - "method": r.Method, - "request uri": r.RequestURI, - "status": statusStr, - "elapsed time": time.Since(start), + "remote addr": r.RemoteAddr, + "method": r.Method, + "request uri": r.RequestURI, + "status": statusStr, + "elapsed time (ms)": float64(time.Since(start).Nanoseconds()) * 1e-6, }).Info("Handled HTTP request") }) } @@ -148,7 +148,7 @@ func servePrometheus(mux *http.ServeMux) { } // Run initializes grpc and grpc gateway api services on the same port -func Run(GrpcPort int, tlsConfig *tls.Config, PaginationKey, CertFile, KeyFile string, store database.Datastore) { +func Run(GrpcPort int, tlsConfig *tls.Config, CertFile, KeyFile string, store database.Datastore) { l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", GrpcPort)) if err != nil { log.WithError(err).Fatalf("could not bind to port %d", GrpcPort) @@ -175,7 +175,7 @@ func Run(GrpcPort int, tlsConfig *tls.Config, PaginationKey, CertFile, KeyFile s apiListener = tls.NewListener(tcpMux.Match(cmux.Any()), tlsConfig) go func() { handleShutdown(tcpMux.Serve()) }() - grpcServer := newGrpcServer(PaginationKey, store, tlsConfig) + grpcServer := newGrpcServer(store, tlsConfig) gwmux := newGrpcGatewayServer(ctx, apiListener.Addr().String(), tlsConfig) httpMux.Handle("/", gwmux) @@ -188,7 +188,7 @@ func Run(GrpcPort int, tlsConfig *tls.Config, PaginationKey, CertFile, KeyFile s apiListener = tcpMux.Match(cmux.Any()) go func() { handleShutdown(tcpMux.Serve()) }() - grpcServer := newGrpcServer(PaginationKey, store, nil) + grpcServer := newGrpcServer(store, nil) go func() { handleShutdown(grpcServer.Serve(grpcL)) }() gwmux := newGrpcGatewayServer(ctx, apiListener.Addr().String(), nil)