Skip to content

Commit

Permalink
ACM:8774: Implement federated global search (#189)
Browse files Browse the repository at this point in the history
* Initial federated search implementation

Signed-off-by: Jorge Padilla <[email protected]>

* Save progress

Signed-off-by: Jorge Padilla <[email protected]>

* Save progress

Signed-off-by: Jorge Padilla <[email protected]>

* Update make

Signed-off-by: Jorge Padilla <[email protected]>

* Cleanup implementation

Signed-off-by: Jorge Padilla <[email protected]>

* Update make

Signed-off-by: Jorge Padilla <[email protected]>

* Support GraphQL __schema

Signed-off-by: Jorge Padilla <[email protected]>

* Fix searchSchema

Signed-off-by: Jorge Padilla <[email protected]>

* Fix searchSchema empty state

Signed-off-by: Jorge Padilla <[email protected]>

* Simplify Make

Signed-off-by: Jorge Padilla <[email protected]>

* Lint and fedConfig updates

Signed-off-by: Jorge Padilla <[email protected]>

* Hack for alias issue

Signed-off-by: Jorge Padilla <[email protected]>

* Update managedhub prop to match console

Signed-off-by: Jorge Padilla <[email protected]>

* Remove duplicates; initial TLS validation

Signed-off-by: Jorge Padilla <[email protected]>

* Add getFederatedResponse function (#193)

* getFederatedResponse function

Signed-off-by: Sherin Varughese <[email protected]>

* add comments

Signed-off-by: Sherin Varughese <[email protected]>

---------

Signed-off-by: Sherin Varughese <[email protected]>

* add mockClient (#194)

Signed-off-by: Sherin Varughese <[email protected]>

* Read federation configuration from kubeapi

Signed-off-by: Jorge Padilla <[email protected]>

* update Tests (#195)

Signed-off-by: Sherin Varughese <[email protected]>

* Configure local secret

Signed-off-by: Jorge Padilla <[email protected]>

* fix lint (#196)

Signed-off-by: Sherin Varughese <[email protected]>

* Simplify setup and update http client pool (#198)

* Simplify setup; disable client pool

Signed-off-by: Jorge Padilla <[email protected]>

* Simplify setup; disable client pool

Signed-off-by: Jorge Padilla <[email protected]>

* Refactor Http client code

Signed-off-by: Jorge Padilla <[email protected]>

* Use HTTPClient interface

Signed-off-by: Jorge Padilla <[email protected]>

* Update mock client pool

Signed-off-by: Jorge Padilla <[email protected]>

* cleanup

Signed-off-by: Jorge Padilla <[email protected]>

* Refactor

Signed-off-by: Jorge Padilla <[email protected]>

* Simplify ManagedServiceAccount enablement

Signed-off-by: Jorge Padilla <[email protected]>

* Remove old setup script

Signed-off-by: Jorge Padilla <[email protected]>

---------

Signed-off-by: Jorge Padilla <[email protected]>

* Add debug

Signed-off-by: Jorge Padilla <[email protected]>

* change GraphQLPayload errors struct (#197)

* change GraphQLPayload errors struct

Signed-off-by: Sherin Varughese <[email protected]>

* remove client

Signed-off-by: Sherin Varughese <[email protected]>

---------

Signed-off-by: Sherin Varughese <[email protected]>

* Fix client pool (#201)

Signed-off-by: Jorge Padilla <[email protected]>

* mock GetHttpClient fn (#200)

Signed-off-by: Sherin Varughese <[email protected]>

* Change managedHub property to camelCase

Signed-off-by: Jorge Padilla <[email protected]>

* [Federation side branch] Use a feature flag to enable federated search (#202)

* Use a feature flag to enable federated search

Signed-off-by: Jorge Padilla <[email protected]>

* Update fedConfig (cache and async); remove global sa

Signed-off-by: Jorge Padilla <[email protected]>

* Improve setup.sh

Signed-off-by: Jorge Padilla <[email protected]>

* Cleanup

Signed-off-by: Jorge Padilla <[email protected]>

* Update setup secript; add related WIP

Signed-off-by: Jorge Padilla <[email protected]>

---------

Signed-off-by: Jorge Padilla <[email protected]>

* Review comments; disable related

Signed-off-by: Jorge Padilla <[email protected]>

* Review comments

Signed-off-by: Jorge Padilla <[email protected]>

---------

Signed-off-by: Jorge Padilla <[email protected]>
Signed-off-by: Sherin Varughese <[email protected]>
Co-authored-by: Sherin Varughese <[email protected]>
  • Loading branch information
jlpadilla and SherinV authored Jan 31, 2024
1 parent c7e67e6 commit a4f2595
Show file tree
Hide file tree
Showing 16 changed files with 1,261 additions and 29 deletions.
63 changes: 48 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@ default::
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'

setup: ## Generate ssl certificate for development.
cd sslcert; openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -config req.conf -extensions 'v3_req'

setup-dev: ## Configure local environment to use the postgres instance on the dev cluster.
@echo "Using current target cluster.\\n"
@echo "$(shell oc cluster-info)"
@echo "\\n1. [MANUAL STEP] Set these environment variables.\\n"
setup: ## Configure local development environment.
@echo "Using current OCP target cluster.\\n"
@echo "$(shell oc cluster-info|grep 'Kubernetes')"
@echo "\\n1. Generating local self-signed certificate.\\n"
cd sslcert; openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -config req.conf -extensions 'v3_req' &> /dev/null
@echo "\\n2. [MANUAL STEP] Set these environment variables on your terminal.\\n"
export DB_NAME=$(shell oc get secret search-postgres -n open-cluster-management -o jsonpath='{.data.database-name}'|base64 -D)
export DB_USER=$(shell oc get secret search-postgres -n open-cluster-management -o jsonpath='{.data.database-user}'|base64 -D)
export DB_PASS=$(shell oc get secret search-postgres -n open-cluster-management -o jsonpath='{.data.database-password}'|base64 -D)
@echo "\\n2. [MANUAL STEP] Start port forwarding.\\n"
@echo "\\n3. [MANUAL STEP] Start port forwarding.\\n"
@echo "oc port-forward service/search-postgres -n open-cluster-management 5432:5432 \\n"

gqlgen: ## Generate graphql model. See: https://gqlgen.com/
go run github.com/99designs/gqlgen generate

.PHONY: run
run: ## Run the service locally.
PLAYGROUND_MODE=true go run main.go --v=4
PLAYGROUND_MODE=true go run -tags development main.go --v=4

.PHONY: lint
lint: ## Run lint and gosec tools.
Expand Down Expand Up @@ -71,13 +70,47 @@ test-scale-ui: check-locust ## Start Locust and open the web browser to drive sc
test-scale-setup: ## Creates the search-api route in the current target cluster.
oc create route passthrough search-api --service=search-search-api -n open-cluster-management

test-send: ## Sends a graphQL query using cURL for development testing.
URL=https://localhost:4010/searchapi/graphql \
TOKEN=$(shell oc whoami -t) \
curl --insecure --location --request POST ${URL} \
--header "Authorization: Bearer ${TOKEN}" --header 'Content-Type: application/json' \
--data-raw '{"query":"query q($$input: [SearchInput]) { search(input: $$input) { count items } }","variables":{"input":[{"keywords":[],"filters":[{"property":"kind","values":["ConfigMap"]}],"limit": 3}]}}' | jq

# Target API URL for the test queries.
SEARCH_API_URL=https://localhost:4010/searchapi/graphql
FEDERATED ?= ${F}
ifeq (${FEDERATED}, true)
SEARCH_API_URL=https://localhost:4010/federated
endif

# Specifies the query string to send with the test requests.
# Q is an alias for QUERY.
QUERY ?= ${Q}
QUERY_STR = "{"query":"query Search() { }","variables":{} }"
ifeq (${QUERY}, schema)
QUERY_STR='{"query":"query Schema() { searchSchema() }","variables":{} }'
else ifeq (${QUERY}, searchComplete)
QUERY_STR='{"query":"query SearchComplete { searchComplete(property: \"kind\") }","variables":{} }'
else ifeq (${QUERY}, search)
QUERY_STR='{"query":"query Search($$input: [SearchInput]) { search(input: $$input) { count items } }","variables":{"input":[{"keywords":[],"filters":[{"property":"kind","values":["ConfigMap"]}],"limit": 3}]}}'
else ifeq (${QUERY}, searchAlias)
QUERY_STR='{"query":"query Search($$input: [SearchInput]) { searchResult: search(input: $$input) { count items __typename } }","variables":{"input":[{"keywords":[],"filters":[{"property":"kind","values":["ConfigMap"]}],"limit": 3}]}}'
else ifeq (${QUERY}, searchCount)
QUERY_STR='{"query":"query Search($$input: [SearchInput]) { search(input: $$input) { count } }","variables":{"input":[{"keywords":[],"filters":[{"property":"kind","values":["ConfigMap"]}],"limit": 3}]}}'
else ifeq (${QUERY}, searchCompleteAlias)
QUERY_STR='{"query":"query SearchComplete { aliasedResult: searchComplete(property: \"kind\") }","variables":{} }'
else ifeq (${QUERY}, searchRelated)
QUERY_STR='{"query":"query Search($$input: [SearchInput]) { search(input: $$input) { count related { kind count } } }","variables":{"input":[{"keywords":[],"filters":[{"property":"kind","values":["Deployment"]}],"limit": 3}]}}'
endif

send: ## Sends a graphQL request using cURL for development and testing. QUERY (alias Q) is a required parameter, values are: [schema|search|searchComplete|searchCount|messages].
ifeq (${QUERY},)
@echo "QUERY (or Q) is required. Example: make send QUERY=searchComplete"
@echo "Valid QUERY values: schema, search, searchComplete, searchCount, messages"
exit 1
endif
# Sending query with the following parameters:
# - URL ${URL}
# - GRAPHQL QUERY ${QUERY_STR}
#
curl --insecure --location --request POST ${SEARCH_API_URL} \
--header "Authorization: Bearer ${API_TOKEN}" --header 'Content-Type: application/json' \
--data-raw ${QUERY_STR} | jq

check-locust: ## Checks if Locust is installed in the system.
ifeq (,$(shell which locust))
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
Expand Down Expand Up @@ -82,7 +83,7 @@ require (
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.1 // indirect
golang.org/x/tools v0.9.3 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.27.2
k8s.io/apimachinery v0.27.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
55 changes: 44 additions & 11 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

var Cfg = new()

// Define a Config type to hold our config properties.
// Defines the configurable options for this microservice.
type Config struct {
API_SERVER_URL string // address for Kubernetes API Server
AuthCacheTTL int // Time-to-live (milliseconds) of Authentication (TokenReview) cache.
Expand All @@ -31,14 +31,35 @@ type Config struct {
DBPass string
DBPort int
DBUser string
Features featureFlags // Enable or disable features.
Federation federationConfig // Federated search configuration.
HttpPort int
PlaygroundMode bool // Enable the GraphQL Playground client.
QueryLimit int // The default LIMIT to use on queries. Client can override.
RelationLevel int // The number of levels/hops for finding relationships for a particular resource
SlowLog int // Logs when queries are slower than the specified time duration in ms. Default 300ms
// Placeholder for future use.
// QueryLoopLimit int // number of queries handled at a time
// RBAC_INACTIVITY_TIMEOUT int
}

// Define feature flags.
type featureFlags struct {
FederatedSearch bool // Enable federated search.
}

// Http Client Pool Transport settings for federated client pool.
type httpClientPool struct {
MaxConnsPerHost int
MaxIdleConns int
MaxIdleConnPerHost int
MaxIdleConnTimeout int
ResponseHeaderTimeout int
RequestTimeout int // Timeout for outbound federated requests.
}

// Federated search configuration options.
type federationConfig struct {
GlobalHubName string // Identifies the global hub cluster, similar to local-cluster
ConfigCacheTTL int // Time-to-live (milliseconds) of federation config cache.
HttpPool httpClientPool // Transport settings for federated client pool.
}

func new() *Config {
Expand All @@ -61,16 +82,28 @@ func new() *Config {
DBPass: getEnv("DB_PASS", ""),
DBPort: getEnvAsInt("DB_PORT", 5432),
DBUser: getEnv("DB_USER", ""),
HttpPort: getEnvAsInt("HTTP_PORT", 4010),
PlaygroundMode: getEnvAsBool("PLAYGROUND_MODE", false),
QueryLimit: getEnvAsInt("QUERY_LIMIT", 1000),
SlowLog: getEnvAsInt("SLOW_LOG", 300),
Features: featureFlags{
FederatedSearch: getEnvAsBool("FEATURE_FEDERATED_SEARCH", false), // In Dev mode default to true.
},
Federation: federationConfig{
GlobalHubName: getEnv("GLOBAL_HUB_NAME", "global-hub"),
ConfigCacheTTL: getEnvAsInt("FEDERATION_CONFIG_CACHE_TTL", 2*60*1000), // 2 mins
HttpPool: httpClientPool{ // Default values for federated client pool.
MaxConnsPerHost: getEnvAsInt("MAX_CONNS_PER_HOST", 2),
MaxIdleConns: getEnvAsInt("MAX_IDLE_CONNS", 10),
MaxIdleConnPerHost: getEnvAsInt("MAX_IDLE_CONN_PER_HOST", 2),
MaxIdleConnTimeout: getEnvAsInt("MAX_IDLE_CONN_TIMEOUT", 15*1000), // 15 seconds.
ResponseHeaderTimeout: getEnvAsInt("RESPONSE_HEADER_TIMEOUT", 15*1000), // 15 seconds.
RequestTimeout: getEnvAsInt("FEDERATED_REQUEST_TIMEOUT", 60*1000), // 60 seconds.
},
},
HttpPort: getEnvAsInt("HTTP_PORT", 4010),
PlaygroundMode: getEnvAsBool("PLAYGROUND_MODE", false),
QueryLimit: getEnvAsInt("QUERY_LIMIT", 1000),
SlowLog: getEnvAsInt("SLOW_LOG", 300),
// Setting default level to 0 to check if user has explicitly set this variable
// This will be updated to 1 for default searches and 3 for applications - unless set by the user
RelationLevel: getEnvAsInt("RELATION_LEVEL", 0),
// Placeholder for future use.
// QueryLoopLimit: getEnvAsInt("QUERY_LOOP_LIMIT", 5000),
// RBAC_INACTIVITY_TIMEOUT: getEnvAsInt("RBAC_INACTIVITY_TIMEOUT", 600000), // 10 minutes
}
conf.DBPass = url.QueryEscape(conf.DBPass)
return conf
Expand Down
20 changes: 20 additions & 0 deletions pkg/config/config_development.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Contributors to the Open Cluster Management project

//go:build development
// +build development

// This file is excluded from compilation unless the build flag -tags development is used.
// Use `make run` to run with the development flag.
package config

import (
"os"

"k8s.io/klog/v2"
)

func init() {
klog.Warning("!!! Running in development mode. !!!")
os.Setenv("FEATURE_FEDERATED_SEARCH", "true")
Cfg = new()
}
22 changes: 22 additions & 0 deletions pkg/federated/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Federated Search

Use federated search to query and combine results from multiple Red Hat Advanced Cluster Management.

## Pre-requisites
1. Multicluster Global Hub Operator 1.0.0 or later.
2. Red Hat Advanced Cluster Management 2.10.0 or later.
- Managed Hub clusters mu have RHACM 2.9.0 or later

## Setup
Execute the script at `./setup.sh` to configure Global Search on the Global Hub cluster.

The script automates the following steps:
1. Enable the Managed Service Account add-on in the MulticlusterEngine CR.
2. Create a service account and secret to access resources managed from the Global Hub cluster.
3. Create a route and managed service acount on each managed hub to access resources managed by each managed hub.
4. Configure the Console to use the Global Search API.

> NOTES:
> Must run using an account with role `open-cluster-management:admin-aggregate` or higher.
> You must re-run this script when a Managed Hub is added.
> This setup is required for Development Preview, it will be fully automated for GA.
16 changes: 16 additions & 0 deletions pkg/federated/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# !/bin/bash
# Copyright Contributors to the Open Cluster Management project

# Delete resources created by setup.sh
MANAGED_HUBS=($(oc get managedcluster -o json | jq -r '.items[] | select(.status.clusterClaims[] | .name == "hub.open-cluster-management.io" and .value != "NotInstalled") | .metadata.name'))

for MANAGED_HUB in "${MANAGED_HUBS[@]}"; do
oc delete -n ${MANAGED_HUB} -f ./federation-managed-hub-config.yaml
done

# Disable global search feature in the console.
oc patch configmap console-mce-config -n multicluster-engine -p '{"data": {"globalSearchFeatureFlag": "disabled"}}'
oc patch configmap console-config -n open-cluster-management -p '{"data": {"globalSearchFeatureFlag": "disabled"}}'

# Disable federated search feature in the search-api.
oc patch search search-v2-operator -n open-cluster-management --type='merge' -p '{"spec":{"deployments":{"queryapi":{"envVar":[{"name":"FEATURE_FEDERATED_SEARCH", "value":"false"}]}}}}'
86 changes: 86 additions & 0 deletions pkg/federated/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright Contributors to the Open Cluster Management project
package federated

import (
"crypto/tls"
"net/http"
"sync"
"time"

config "github.com/stolostron/search-v2-api/pkg/config"
"k8s.io/klog/v2"
)

// Returns a client to process the federated request.
func GetHttpClient(remoteService RemoteSearchService) HTTPClient {
// Get http client from pool.
client := httpClientPool.Get().(*RealHTTPClient)

tlsConfig := tls.Config{
MinVersion: tls.VersionTLS13, // TODO: Verify if 1.3 is ok now. It caused issues in the past.
}
if remoteService.TLSCert != "" && remoteService.TLSKey != "" {
tlsConfig.Certificates = []tls.Certificate{
{
// RootCAs: nil,
Certificate: [][]byte{[]byte(remoteService.TLSCert)},
PrivateKey: []byte(remoteService.TLSKey),
},
}
} else {
klog.Warningf("TLS cert and key not provided for %s. Skipping TLS verification.", remoteService.Name)
tlsConfig.InsecureSkipVerify = true // #nosec G402 - FIXME: Add TLS verification.
}

client.SetTLSClientConfig(&tlsConfig)

return client
}

// shared HTTP transport and client for efficient connection reuse as per
// godoc: https://cs.opensource.google/go/go/+/go1.21.5:src/net/http/transport.go;l=95 and
// https://stuartleeks.com/posts/connection-re-use-in-golang-with-http-client/
var tr = &http.Transport{
MaxIdleConns: config.Cfg.Federation.HttpPool.MaxIdleConns,
IdleConnTimeout: time.Duration(config.Cfg.Federation.HttpPool.MaxIdleConnTimeout) * time.Millisecond,
ResponseHeaderTimeout: time.Duration(config.Cfg.Federation.HttpPool.ResponseHeaderTimeout) * time.Millisecond,
DisableKeepAlives: false,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS13, // TODO: Verify if 1.3 is ok now. It caused issues in the past.
},
MaxConnsPerHost: config.Cfg.Federation.HttpPool.MaxConnsPerHost,
MaxIdleConnsPerHost: config.Cfg.Federation.HttpPool.MaxIdleConnPerHost,
}

var httpClientPool = sync.Pool{
New: func() interface{} {
klog.V(6).Infof("Creating new RealHTTPClient from pool.")
return &RealHTTPClient{
&http.Client{
Transport: tr,
Timeout: time.Duration(config.Cfg.Federation.HttpPool.RequestTimeout) * time.Millisecond,
},
}
},
}

// HTTPClient is an interface for an HTTP client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
SetTLSClientConfig(*tls.Config)
}

// RealHTTPClient is a real implementation of the HTTPClient interface.
type RealHTTPClient struct {
*http.Client
}

// Do implements the HTTPClient interface for RealHTTPClient.
func (c RealHTTPClient) Do(req *http.Request) (*http.Response, error) {
return c.Client.Do(req)
}

// SetTLSClientConfig sets the TLS client configuration for the HTTP client.
func (c RealHTTPClient) SetTLSClientConfig(config *tls.Config) {
c.Transport.(*http.Transport).TLSClientConfig = config
}
Loading

0 comments on commit a4f2595

Please sign in to comment.