diff --git a/api/api.go b/api/api.go index efc83a7c..9da5073a 100644 --- a/api/api.go +++ b/api/api.go @@ -23,29 +23,37 @@ import ( "strconv" "time" + "github.com/coreos/pkg/capnslog" "github.com/tylerb/graceful" - "github.com/coreos/clair/api/context" - "github.com/coreos/clair/config" + "github.com/coreos/clair/database" "github.com/coreos/clair/pkg/stopper" - "github.com/coreos/pkg/capnslog" ) const timeoutResponse = `{"Error":{"Message":"Clair failed to respond within the configured timeout window.","Type":"Timeout"}}` var log = capnslog.NewPackageLogger("github.com/coreos/clair", "api") -func Run(config *config.APIConfig, ctx *context.RouteContext, st *stopper.Stopper) { +// Config is the configuration for the API service. +type Config struct { + Port int + HealthPort int + Timeout time.Duration + PaginationKey string + CertFile, KeyFile, CAFile string +} + +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 config == nil { + if cfg == nil { log.Infof("main API service is disabled.") return } - log.Infof("starting main API on port %d.", config.Port) + log.Infof("starting main API on port %d.", cfg.Port) - tlsConfig, err := tlsClientConfig(config.CAFile) + tlsConfig, err := tlsClientConfig(cfg.CAFile) if err != nil { log.Fatalf("could not initialize client cert authentication: %s\n", err) } @@ -57,33 +65,33 @@ func Run(config *config.APIConfig, ctx *context.RouteContext, st *stopper.Stoppe Timeout: 0, // Already handled by our TimeOut middleware NoSignalHandling: true, // We want to use our own Stopper Server: &http.Server{ - Addr: ":" + strconv.Itoa(config.Port), + Addr: ":" + strconv.Itoa(cfg.Port), TLSConfig: tlsConfig, - Handler: http.TimeoutHandler(newAPIHandler(ctx), config.Timeout, timeoutResponse), + Handler: http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse), }, } - listenAndServeWithStopper(srv, st, config.CertFile, config.KeyFile) + listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile) log.Info("main API stopped") } -func RunHealth(config *config.APIConfig, ctx *context.RouteContext, st *stopper.Stopper) { +func RunHealth(cfg *Config, store database.Datastore, st *stopper.Stopper) { defer st.End() // Do not run the API service if there is no config. - if config == nil { + if cfg == nil { log.Infof("health API service is disabled.") return } - log.Infof("starting health API on port %d.", config.HealthPort) + log.Infof("starting health API on port %d.", cfg.HealthPort) srv := &graceful.Server{ Timeout: 10 * time.Second, // Interrupt health checks when stopping NoSignalHandling: true, // We want to use our own Stopper Server: &http.Server{ - Addr: ":" + strconv.Itoa(config.HealthPort), - Handler: http.TimeoutHandler(newHealthHandler(ctx), config.Timeout, timeoutResponse), + Addr: ":" + strconv.Itoa(cfg.HealthPort), + Handler: http.TimeoutHandler(newHealthHandler(store), cfg.Timeout, timeoutResponse), }, } diff --git a/api/context/context.go b/api/context/context.go deleted file mode 100644 index d7df44f2..00000000 --- a/api/context/context.go +++ /dev/null @@ -1,66 +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 context - -import ( - "net/http" - "strconv" - "time" - - "github.com/coreos/pkg/capnslog" - "github.com/julienschmidt/httprouter" - "github.com/prometheus/client_golang/prometheus" - - "github.com/coreos/clair/config" - "github.com/coreos/clair/database" -) - -var ( - log = capnslog.NewPackageLogger("github.com/coreos/clair", "api") - - 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, *RouteContext) (route string, status int) - -func HTTPHandler(handler Handler, ctx *RouteContext) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - start := time.Now() - route, status := handler(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.Infof("%s \"%s %s\" %s (%s)", r.RemoteAddr, r.Method, r.RequestURI, statusStr, time.Since(start)) - } -} - -type RouteContext struct { - Store database.Datastore - Config *config.APIConfig -} diff --git a/api/router.go b/api/router.go index 808ca5f7..76d9b827 100644 --- a/api/router.go +++ b/api/router.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -20,8 +20,8 @@ import ( "github.com/julienschmidt/httprouter" - "github.com/coreos/clair/api/context" "github.com/coreos/clair/api/v1" + "github.com/coreos/clair/database" ) // router is an HTTP router that forwards requests to the appropriate sub-router @@ -31,9 +31,9 @@ type router map[string]*httprouter.Router // Let's hope we never have more than 99 API versions. const apiVersionLength = len("v99") -func newAPIHandler(ctx *context.RouteContext) http.Handler { +func newAPIHandler(cfg *Config, store database.Datastore) http.Handler { router := make(router) - router["/v1"] = v1.NewRouter(ctx) + router["/v1"] = v1.NewRouter(store, cfg.PaginationKey) return router } @@ -56,21 +56,22 @@ func (rtr router) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) } -func newHealthHandler(ctx *context.RouteContext) http.Handler { +func newHealthHandler(store database.Datastore) http.Handler { router := httprouter.New() - router.GET("/health", context.HTTPHandler(getHealth, ctx)) + router.GET("/health", healthHandler(store)) return router } -func getHealth(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { - header := w.Header() - header.Set("Server", "clair") +func healthHandler(store database.Datastore) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + header := w.Header() + header.Set("Server", "clair") - status := http.StatusInternalServerError - if ctx.Store.Ping() { - status = http.StatusOK - } + status := http.StatusInternalServerError + if store.Ping() { + status = http.StatusOK + } - w.WriteHeader(status) - return "health", status + w.WriteHeader(status) + } } diff --git a/api/v1/router.go b/api/v1/router.go index 5a3d640e..44608949 100644 --- a/api/v1/router.go +++ b/api/v1/router.go @@ -16,41 +16,83 @@ package v1 import ( + "net/http" + "strconv" + "time" + "github.com/julienschmidt/httprouter" + "github.com/prometheus/client_golang/prometheus" + + "github.com/coreos/clair/database" +) - "github.com/coreos/clair/api/context" +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.Infof("%s \"%s %s\" %s (%s)", r.RemoteAddr, r.Method, r.RequestURI, statusStr, time.Since(start)) + } +} + +type context struct { + Store database.Datastore + PaginationKey string +} + // NewRouter creates an HTTP router for version 1 of the Clair API. -func NewRouter(ctx *context.RouteContext) *httprouter.Router { +func NewRouter(store database.Datastore, paginationKey string) *httprouter.Router { router := httprouter.New() + ctx := &context{store, paginationKey} // Layers - router.POST("/layers", context.HTTPHandler(postLayer, ctx)) - router.GET("/layers/:layerName", context.HTTPHandler(getLayer, ctx)) - router.DELETE("/layers/:layerName", context.HTTPHandler(deleteLayer, ctx)) + router.POST("/layers", httpHandler(postLayer, ctx)) + router.GET("/layers/:layerName", httpHandler(getLayer, ctx)) + router.DELETE("/layers/:layerName", httpHandler(deleteLayer, ctx)) // Namespaces - router.GET("/namespaces", context.HTTPHandler(getNamespaces, ctx)) + router.GET("/namespaces", httpHandler(getNamespaces, ctx)) // Vulnerabilities - router.GET("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(getVulnerabilities, ctx)) - router.POST("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(postVulnerability, ctx)) - router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(getVulnerability, ctx)) - router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(putVulnerability, ctx)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(deleteVulnerability, ctx)) + 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", context.HTTPHandler(getFixes, ctx)) - router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(putFix, ctx)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(deleteFix, ctx)) + 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", context.HTTPHandler(getNotification, ctx)) - router.DELETE("/notifications/:notificationName", context.HTTPHandler(deleteNotification, ctx)) + router.GET("/notifications/:notificationName", httpHandler(getNotification, ctx)) + router.DELETE("/notifications/:notificationName", httpHandler(deleteNotification, ctx)) // Metrics - router.GET("/metrics", context.HTTPHandler(getMetrics, ctx)) + router.GET("/metrics", httpHandler(getMetrics, ctx)) return router } diff --git a/api/v1/routes.go b/api/v1/routes.go index 97048eec..eab3115c 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -26,7 +26,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/coreos/clair" - "github.com/coreos/clair/api/context" "github.com/coreos/clair/database" "github.com/coreos/clair/pkg/commonerr" "github.com/coreos/clair/pkg/tarutil" @@ -96,7 +95,7 @@ func writeResponse(w http.ResponseWriter, r *http.Request, status int, resp inte } } -func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { request := LayerEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -138,7 +137,7 @@ func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx return postLayerRoute, http.StatusCreated } -func getLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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"] @@ -157,7 +156,7 @@ func getLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx * return getLayerRoute, http.StatusOK } -func deleteLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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()}}) @@ -171,7 +170,7 @@ func deleteLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ct return deleteLayerRoute, http.StatusOK } -func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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()}}) @@ -189,7 +188,7 @@ func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, return getNamespacesRoute, http.StatusOK } -func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { query := r.URL.Query() limitStrs, limitExists := query["limit"] @@ -209,7 +208,7 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par page := 0 pageStrs, pageExists := query["page"] if pageExists { - err = tokenUnmarshal(pageStrs[0], ctx.Config.PaginationKey, &page) + err = tokenUnmarshal(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 @@ -239,7 +238,7 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par var nextPageStr string if nextPage != -1 { - nextPageBytes, err := tokenMarshal(nextPage, ctx.Config.PaginationKey) + nextPageBytes, err := tokenMarshal(nextPage, ctx.PaginationKey) if err != nil { writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) return getNotificationRoute, http.StatusBadRequest @@ -251,7 +250,7 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par return getVulnerabilitiesRoute, http.StatusOK } -func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { request := VulnerabilityEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -286,7 +285,7 @@ func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Para return postVulnerabilityRoute, http.StatusCreated } -func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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")) @@ -304,7 +303,7 @@ func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Param return getVulnerabilityRoute, http.StatusOK } -func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { request := VulnerabilityEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -347,7 +346,7 @@ func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Param return putVulnerabilityRoute, http.StatusOK } -func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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()}}) @@ -361,7 +360,7 @@ func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Pa return deleteVulnerabilityRoute, http.StatusOK } -func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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()}}) @@ -376,7 +375,7 @@ func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx * return getFixesRoute, http.StatusOK } -func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { request := FeatureEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -420,7 +419,7 @@ func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *co return putFixRoute, http.StatusOK } -func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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()}}) @@ -434,7 +433,7 @@ func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx return deleteFixRoute, http.StatusOK } -func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context) (string, int) { query := r.URL.Query() limitStrs, limitExists := query["limit"] @@ -452,14 +451,14 @@ func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params page := database.VulnerabilityNotificationFirstPage pageStrs, pageExists := query["page"] if pageExists { - err := tokenUnmarshal(pageStrs[0], ctx.Config.PaginationKey, &page) + err := tokenUnmarshal(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 := tokenMarshal(page, ctx.Config.PaginationKey) + pageTokenBytes, err := tokenMarshal(page, ctx.PaginationKey) if err != nil { writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) return getNotificationRoute, http.StatusBadRequest @@ -476,13 +475,13 @@ func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params return getNotificationRoute, http.StatusInternalServerError } - notification := NotificationFromDatabaseModel(dbNotification, limit, pageToken, nextPage, ctx.Config.PaginationKey) + 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.RouteContext) (string, int) { +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()}}) @@ -496,7 +495,7 @@ func deleteNotification(w http.ResponseWriter, r *http.Request, p httprouter.Par return deleteNotificationRoute, http.StatusOK } -func getMetrics(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { +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/config/config.go b/cmd/clair/config.go similarity index 61% rename from config/config.go rename to cmd/clair/config.go index 12572e6c..c11e971f 100644 --- a/config/config.go +++ b/cmd/clair/config.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package main import ( "errors" @@ -20,21 +20,19 @@ import ( "os" "time" - "github.com/fernet/fernet-go" "gopkg.in/yaml.v2" + + "github.com/coreos/clair" + "github.com/coreos/clair/api" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/notification" + "github.com/fernet/fernet-go" ) -// ErrDatasourceNotLoaded is returned when the datasource variable in the configuration file is not loaded properly +// ErrDatasourceNotLoaded is returned when the datasource variable in the +// configuration file is not loaded properly var ErrDatasourceNotLoaded = errors.New("could not load configuration: no database source specified") -// RegistrableComponentConfig is a configuration block that can be used to -// determine which registrable component should be initialized and pass -// custom configuration to it. -type RegistrableComponentConfig struct { - Type string - Options map[string]interface{} -} - // File represents a YAML configuration file that namespaces all Clair // configuration under the top-level "clair" key. type File struct { @@ -43,57 +41,37 @@ type File struct { // Config is the global configuration for an instance of Clair. type Config struct { - Database RegistrableComponentConfig - Updater *UpdaterConfig - Notifier *NotifierConfig - API *APIConfig -} - -// UpdaterConfig is the configuration for the Updater service. -type UpdaterConfig struct { - Interval time.Duration -} - -// NotifierConfig is the configuration for the Notifier service and its registered notifiers. -type NotifierConfig struct { - Attempts int - RenotifyInterval time.Duration - Params map[string]interface{} `yaml:",inline"` -} - -// APIConfig is the configuration for the API service. -type APIConfig struct { - Port int - HealthPort int - Timeout time.Duration - PaginationKey string - CertFile, KeyFile, CAFile string + Database database.RegistrableComponentConfig + Updater *clair.UpdaterConfig + Notifier *notification.Config + API *api.Config } // DefaultConfig is a configuration that can be used as a fallback value. func DefaultConfig() Config { return Config{ - Database: RegistrableComponentConfig{ + Database: database.RegistrableComponentConfig{ Type: "pgsql", }, - Updater: &UpdaterConfig{ + Updater: &clair.UpdaterConfig{ Interval: 1 * time.Hour, }, - API: &APIConfig{ + API: &api.Config{ Port: 6060, HealthPort: 6061, Timeout: 900 * time.Second, }, - Notifier: &NotifierConfig{ + Notifier: ¬ification.Config{ Attempts: 5, RenotifyInterval: 2 * time.Hour, }, } } -// Load is a shortcut to open a file, read it, and generate a Config. +// LoadConfig is a shortcut to open a file, read it, and generate a Config. +// // It supports relative and absolute paths. Given "", it returns DefaultConfig. -func Load(path string) (config *Config, err error) { +func LoadConfig(path string) (config *Config, err error) { var cfgFile File cfgFile.Clair = DefaultConfig() if path == "" { diff --git a/cmd/clair/main.go b/cmd/clair/main.go index 75a4124e..754e54e5 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -29,8 +29,6 @@ import ( "github.com/coreos/clair" "github.com/coreos/clair/api" - "github.com/coreos/clair/api/context" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" "github.com/coreos/clair/pkg/stopper" @@ -88,7 +86,7 @@ func stopCPUProfiling(f *os.File) { } // Boot starts Clair instance with the provided config. -func Boot(config *config.Config) { +func Boot(config *Config) { rand.Seed(time.Now().UnixNano()) st := stopper.NewStopper() @@ -105,9 +103,9 @@ func Boot(config *config.Config) { // Start API st.Begin() - go api.Run(config.API, &context.RouteContext{db, config.API}, st) + go api.Run(config.API, db, st) st.Begin() - go api.RunHealth(config.API, &context.RouteContext{db, config.API}, st) + go api.RunHealth(config.API, db, st) // Start updater st.Begin() @@ -136,7 +134,7 @@ func main() { } // Load configuration - config, err := config.Load(*flagConfigPath) + config, err := LoadConfig(*flagConfigPath) if err != nil { log.Fatalf("failed to load configuration: %s", err) } diff --git a/database/database.go b/database/database.go index f8ce18dd..d3f7fc0f 100644 --- a/database/database.go +++ b/database/database.go @@ -20,8 +20,6 @@ import ( "errors" "fmt" "time" - - "github.com/coreos/clair/config" ) var ( @@ -35,11 +33,19 @@ var ( ErrInconsistent = errors.New("database: inconsistent database") ) +// RegistrableComponentConfig is a configuration block that can be used to +// determine which registrable component should be initialized and pass custom +// configuration to it. +type RegistrableComponentConfig struct { + Type string + Options map[string]interface{} +} + var drivers = make(map[string]Driver) // Driver is a function that opens a Datastore specified by its database driver type and specific // configuration. -type Driver func(config.RegistrableComponentConfig) (Datastore, error) +type Driver func(RegistrableComponentConfig) (Datastore, error) // Register makes a Constructor available by the provided name. // @@ -56,7 +62,7 @@ func Register(name string, driver Driver) { } // Open opens a Datastore specified by a configuration. -func Open(cfg config.RegistrableComponentConfig) (Datastore, error) { +func Open(cfg RegistrableComponentConfig) (Datastore, error) { driver, ok := drivers[cfg.Type] if !ok { return nil, fmt.Errorf("database: unknown Driver %q (forgotten configuration or import?)", cfg.Type) diff --git a/database/pgsql/pgsql.go b/database/pgsql/pgsql.go index 4abd29ee..3353ddcc 100644 --- a/database/pgsql/pgsql.go +++ b/database/pgsql/pgsql.go @@ -31,7 +31,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/remind101/migrate" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" "github.com/coreos/clair/database/pgsql/migrations" "github.com/coreos/clair/pkg/commonerr" @@ -114,11 +113,13 @@ type Config struct { FixturePath string } -// openDatabase opens a PostgresSQL-backed Datastore using the given configuration. -// It immediately every necessary migrations. If ManageDatabaseLifecycle is specified, -// the database will be created first. If FixturePath is specified, every SQL queries that are -// present insides will be executed. -func openDatabase(registrableComponentConfig config.RegistrableComponentConfig) (database.Datastore, error) { +// openDatabase opens a PostgresSQL-backed Datastore using the given +// configuration. +// +// It immediately runs all necessary migrations. If ManageDatabaseLifecycle is +// specified, the database will be created first. If FixturePath is specified, +// every SQL queries that are present insides will be executed. +func openDatabase(registrableComponentConfig database.RegistrableComponentConfig) (database.Datastore, error) { var pg pgSQL var err error diff --git a/database/pgsql/pgsql_test.go b/database/pgsql/pgsql_test.go index b9ee97af..93f53144 100644 --- a/database/pgsql/pgsql_test.go +++ b/database/pgsql/pgsql_test.go @@ -21,8 +21,9 @@ import ( "runtime" "strings" - "github.com/coreos/clair/config" "github.com/pborman/uuid" + + "github.com/coreos/clair/database" ) func openDatabaseForTest(testName string, loadFixture bool) (*pgSQL, error) { @@ -34,7 +35,7 @@ func openDatabaseForTest(testName string, loadFixture bool) (*pgSQL, error) { return datastore, nil } -func generateTestConfig(testName string, loadFixture bool) config.RegistrableComponentConfig { +func generateTestConfig(testName string, loadFixture bool) database.RegistrableComponentConfig { dbName := "test_" + strings.ToLower(testName) + "_" + strings.Replace(uuid.New(), "-", "_", -1) var fixturePath string @@ -48,7 +49,7 @@ func generateTestConfig(testName string, loadFixture bool) config.RegistrableCom source = fmt.Sprintf(sourceEnv, dbName) } - return config.RegistrableComponentConfig{ + return database.RegistrableComponentConfig{ Options: map[string]interface{}{ "source": source, "cachesize": 0, diff --git a/ext/notification/driver.go b/ext/notification/driver.go index acd4d4f1..ad1dcc1e 100644 --- a/ext/notification/driver.go +++ b/ext/notification/driver.go @@ -22,10 +22,10 @@ package notification import ( "sync" + "time" "github.com/coreos/pkg/capnslog" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" ) @@ -36,11 +36,19 @@ var ( senders = make(map[string]Sender) ) +// Config is the configuration for the Notifier service and its registered +// notifiers. +type Config struct { + Attempts int + RenotifyInterval time.Duration + Params map[string]interface{} `yaml:",inline"` +} + // Sender represents anything that can transmit notifications. type Sender interface { // Configure attempts to initialize the notifier with the provided configuration. // It returns whether the notifier is enabled or not. - Configure(*config.NotifierConfig) (bool, error) + Configure(*Config) (bool, error) // Send informs the existence of the specified notification. Send(notification database.VulnerabilityNotification) error diff --git a/ext/notification/webhook/webhook.go b/ext/notification/webhook/webhook.go index 6a991cf4..d54b588b 100644 --- a/ext/notification/webhook/webhook.go +++ b/ext/notification/webhook/webhook.go @@ -29,7 +29,6 @@ import ( "gopkg.in/yaml.v2" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" "github.com/coreos/clair/ext/notification" ) @@ -55,7 +54,7 @@ func init() { notification.RegisterSender("webhook", &sender{}) } -func (s *sender) Configure(config *config.NotifierConfig) (bool, error) { +func (s *sender) Configure(config *notification.Config) (bool, error) { // Get configuration var httpConfig Config if config == nil { diff --git a/notifier.go b/notifier.go index 50158cdd..0586f019 100644 --- a/notifier.go +++ b/notifier.go @@ -21,7 +21,6 @@ import ( "github.com/pborman/uuid" "github.com/prometheus/client_golang/prometheus" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" "github.com/coreos/clair/ext/notification" "github.com/coreos/clair/pkg/commonerr" @@ -54,7 +53,7 @@ func init() { // RunNotifier begins a process that checks for new notifications that should // be sent out to third parties. -func RunNotifier(config *config.NotifierConfig, datastore database.Datastore, stopper *stopper.Stopper) { +func RunNotifier(config *notification.Config, datastore database.Datastore, stopper *stopper.Stopper) { defer stopper.End() // Configure registered notifiers. diff --git a/updater.go b/updater.go index 41eb8128..d3379cdd 100644 --- a/updater.go +++ b/updater.go @@ -24,7 +24,6 @@ import ( "github.com/pborman/uuid" "github.com/prometheus/client_golang/prometheus" - "github.com/coreos/clair/config" "github.com/coreos/clair/database" "github.com/coreos/clair/ext/vulnmdsrc" "github.com/coreos/clair/ext/vulnsrc" @@ -63,9 +62,14 @@ func init() { prometheus.MustRegister(promUpdaterNotesTotal) } +// UpdaterConfig is the configuration for the Updater service. +type UpdaterConfig struct { + Interval time.Duration +} + // RunUpdater begins a process that updates the vulnerability database at // regular intervals. -func RunUpdater(config *config.UpdaterConfig, datastore database.Datastore, st *stopper.Stopper) { +func RunUpdater(config *UpdaterConfig, datastore database.Datastore, st *stopper.Stopper) { defer st.End() // Do not run the updater if there is no config or if the interval is 0. diff --git a/worker_test.go b/worker_test.go index 34d6f039..b0f14dbc 100644 --- a/worker_test.go +++ b/worker_test.go @@ -78,9 +78,9 @@ func TestProcessWithDistUpgrade(t *testing.T) { // wheezy.tar: FROM debian:wheezy // jessie.tar: RUN sed -i "s/precise/trusty/" /etc/apt/sources.list && apt-get update && // apt-get -y dist-upgrade - assert.Nil(t, Process(datastore, "Docker", "blank", "", testDataPath+"blank.tar.gz", nil)) - assert.Nil(t, Process(datastore, "Docker", "wheezy", "blank", testDataPath+"wheezy.tar.gz", nil)) - assert.Nil(t, Process(datastore, "Docker", "jessie", "wheezy", testDataPath+"jessie.tar.gz", nil)) + assert.Nil(t, ProcessLayer(datastore, "Docker", "blank", "", testDataPath+"blank.tar.gz", nil)) + assert.Nil(t, ProcessLayer(datastore, "Docker", "wheezy", "blank", testDataPath+"wheezy.tar.gz", nil)) + assert.Nil(t, ProcessLayer(datastore, "Docker", "jessie", "wheezy", testDataPath+"jessie.tar.gz", nil)) // Ensure that the 'wheezy' layer has the expected namespace and features. wheezy, ok := datastore.layers["wheezy"]