search.go (167 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package main import ( "context" "fmt" "net/http" "net/url" "sort" "strconv" "strings" "time" "github.com/Masterminds/semver/v3" "go.elastic.co/apm/module/apmzap/v2" "go.elastic.co/apm/v2" "go.uber.org/zap" "github.com/elastic/package-registry/internal/util" "github.com/elastic/package-registry/packages" "github.com/elastic/package-registry/proxymode" ) func searchHandler(logger *zap.Logger, indexer Indexer, cacheTime time.Duration) func(w http.ResponseWriter, r *http.Request) { return searchHandlerWithProxyMode(logger, indexer, proxymode.NoProxy(logger), cacheTime) } func searchHandlerWithProxyMode(logger *zap.Logger, indexer Indexer, proxyMode *proxymode.ProxyMode, cacheTime time.Duration) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { logger := logger.With(apmzap.TraceContext(r.Context())...) filter, err := newSearchFilterFromQuery(r.URL.Query()) if err != nil { badRequest(w, err.Error()) return } opts := packages.GetOptions{ Filter: filter, } packages, err := indexer.Get(r.Context(), &opts) if err != nil { notFoundError(w, fmt.Errorf("fetching package failed: %w", err)) return } if proxyMode.Enabled() { proxiedPackages, err := proxyMode.Search(r) if err != nil { logger.Error("proxy mode: search failed", zap.Error(err)) http.Error(w, "internal server error", http.StatusInternalServerError) return } packages = packages.Join(proxiedPackages) if !opts.Filter.AllVersions { packages = latestPackagesVersion(packages) } } data, err := getSearchOutput(r.Context(), packages) if err != nil { notFoundError(w, err) return } cacheHeaders(w, cacheTime) jsonHeader(w) fmt.Fprint(w, string(data)) } } func newSearchFilterFromQuery(query url.Values) (*packages.Filter, error) { var filter packages.Filter if len(query) == 0 { return &filter, nil } var err error if v := query.Get("kibana.version"); v != "" { filter.KibanaVersion, err = semver.NewVersion(v) if err != nil { return nil, fmt.Errorf("invalid Kibana version '%s': %w", v, err) } } if v := query.Get("category"); v != "" { filter.Category = v } if v := query.Get("package"); v != "" { filter.PackageName = v } if v := query.Get("type"); v != "" { filter.PackageType = v } if v := query.Get("capabilities"); v != "" { filter.Capabilities = strings.Split(v, ",") } if v := query.Get("spec.min"); v != "" { filter.SpecMin, err = getSpecVersion(v) if err != nil { return nil, fmt.Errorf("invalid 'spec.min' version: %w", err) } } if v := query.Get("spec.max"); v != "" { filter.SpecMax, err = getSpecVersion(v) if err != nil { return nil, fmt.Errorf("invalid 'spec.max' version: %w", err) } } if v := query.Get("all"); v != "" { // Default is false, also on error filter.AllVersions, err = strconv.ParseBool(v) if err != nil { return nil, fmt.Errorf("invalid 'all' query param: '%s'", v) } } // Deprecated: release tags to be removed. if v := query.Get("experimental"); v != "" { // In case of error, keep it false filter.Experimental, err = strconv.ParseBool(v) if err != nil { return nil, fmt.Errorf("invalid 'experimental' query param: '%s'", v) } } if v := query.Get("prerelease"); v != "" { // In case of error, keep it false filter.Prerelease, err = strconv.ParseBool(v) if err != nil { return nil, fmt.Errorf("invalid 'prerelease' query param: '%s'", v) } } if v := query.Get("discovery"); v != "" { discovery, err := packages.NewDiscoveryFilter(v) if err != nil { return nil, fmt.Errorf("invalid 'discovery' query param: '%s': %w", v, err) } filter.Discovery = discovery } return &filter, nil } func getSpecVersion(version string) (*semver.Version, error) { // version must cointain just <major.minor> if len(strings.Split(version, ".")) != 2 { return nil, fmt.Errorf("invalid version '%s': it should be <major.version>", version) } specVersion, err := semver.NewVersion(version) if err != nil { return nil, fmt.Errorf("invalid spec version '%s': %w", version, err) } return specVersion, nil } func getSearchOutput(ctx context.Context, packageList packages.Packages) ([]byte, error) { span, _ := apm.StartSpan(ctx, "GetPackageOutput", "app") defer span.End() // Packages need to be sorted to be always outputted in the same order sort.Sort(packageList) var output []packageSummary for _, p := range packageList { data := getPackageSummaryOutput(p) output = append(output, data) } // Instead of return `null` in case of an empty array, return [] if len(output) == 0 { return []byte("[]"), nil } return util.MarshalJSONPretty(output) } type packageSummary struct { packages.BasePackage `json:",inline"` DataStreams []*packages.DataStream `json:"data_streams,omitempty"` } func getPackageSummaryOutput(index *packages.Package) packageSummary { summary := packageSummary{ BasePackage: index.BasePackage, } if len(index.DataStreams) == 0 { return summary } summary.DataStreams = make([]*packages.DataStream, len(index.DataStreams)) for i, datastream := range index.DataStreams { summary.DataStreams[i] = &packages.DataStream{ Type: datastream.Type, Dataset: datastream.Dataset, Title: datastream.Title, } } return summary }