package server import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "strings" "github.com/gorilla/mux" "github.com/rs/cors" "sigs.k8s.io/kustomize/api/internal/crawl/index" ) type kustomizeSearch struct { ctx context.Context // Eventually pIndex *index.PlugginIndex idx *index.KustomizeIndex router *mux.Router log *log.Logger } // New server. Creating a server does not launch it. To launch simply: // srv, _ := NewKustomizeSearch(context.Backgroud()) // err := srv.Serve() // if err != nil { // // Handle server issues. // } // // The server has three enpoints, two of which are functional: // // /search: processes the ?q= parameter for a text query and // returns a list of 10 resutls starting from the ?from= value provided, // with the default being zero. // // /metrics: returns overall metrics about the files indexed. Returns // timeseries data for kustomization files, and returns breakdown of file // counts by their 'kind' fields // // /register: not implemented, but meant as an endpoint for adding new // kustomization files to the corpus. func NewKustomizeSearch(ctx context.Context) (*kustomizeSearch, error) { idx, err := index.NewKustomizeIndex(ctx, "kustomize") if err != nil { return nil, err } ks := &kustomizeSearch{ ctx: ctx, idx: idx, router: mux.NewRouter(), log: log.New(os.Stdout, "Kustomize server: ", log.LstdFlags|log.Llongfile|log.LUTC), } return ks, nil } // Set up common middleware and the routes for the server. func (ks *kustomizeSearch) routes() { // Setup middleware. ks.router.Use(func(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") handler.ServeHTTP(w, r) }) }) ks.router.HandleFunc("/liveness", ks.liveness()).Methods(http.MethodGet) ks.router.HandleFunc("/readiness", ks.readiness()).Methods(http.MethodGet) ks.router.HandleFunc("/search", ks.search()).Methods(http.MethodGet) ks.router.HandleFunc("/metrics", ks.metrics()).Methods(http.MethodGet) ks.router.HandleFunc("/register", ks.register()).Methods(http.MethodPost) } // Start listening and serving on the provided port. func (ks *kustomizeSearch) Serve(port int) error { ks.routes() handler := cors.Default().Handler(ks.router) s := &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: handler, // Timeouts/Limits } return s.ListenAndServe() } // /liveness endpoint func (ks *kustomizeSearch) liveness() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } } // /readyness endpoint func (ks *kustomizeSearch) readiness() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opt := index.KustomizeSearchOptions{} _, err := ks.idx.Search("", opt) if err != nil { http.Error(w, `{ "error": "could not connect to database" }`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } } // /register endpoint. func (ks *kustomizeSearch) register() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not implemented", http.StatusInternalServerError) } } // /search endpoint. func (ks *kustomizeSearch) search() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { values := r.URL.Query() queries := values["q"] ks.log.Println("Query: ", values) var from int fromParam := values["from"] if len(fromParam) > 0 { from, _ = strconv.Atoi(fromParam[0]) if from < 0 { from = 0 } } _, noKinds := values["nokinds"] opt := index.KustomizeSearchOptions{ SearchOptions: index.SearchOptions{ Size: 10, From: from, }, KindAggregation: !noKinds, } results, err := ks.idx.Search(strings.Join(queries, " "), opt) if err != nil { ks.log.Println("Error: ", err) http.Error(w, fmt.Sprintf( `{ "error": "could not complete the query" }`), http.StatusInternalServerError) return } enc := json.NewEncoder(w) setIndent(enc) if err = enc.Encode(results); err != nil { http.Error(w, `{ "error": "failed to send back results" }`, http.StatusInternalServerError) return } return } } // metrics endpoint. func (ks *kustomizeSearch) metrics() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { res, err := ks.idx.Search("", index.KustomizeSearchOptions{ KindAggregation: true, TimeseriesAggregation: true, }) if err != nil { http.Error(w, `{ "error": "could not perform the search."}`, http.StatusInternalServerError) return } enc := json.NewEncoder(w) setIndent(enc) if err := enc.Encode(res); err != nil { http.Error(w, `{ "error": "could not format return value" }`, http.StatusInternalServerError) return } } } // make json response human readable. func setIndent(e *json.Encoder) { e.SetIndent("", " ") }