Skip to content

Search Query API

Joydeep Banerjee edited this page Aug 10, 2022 · 17 revisions

In this topic you will learn how to expose the search API with a route and how to use the API to resolve search queries.

The API is a GraphQL endpoint. You can use any client like curl or Postman. In this topic we'll use GraphQL Playground which is included with the search API deployment for development purposes.

Configure API access

At this point you should have installed the Search operator on your OpenShift cluster.

Use the following commands to enable the Playground client and create a route to access the API from outside of your cluster.

# 1. Pause the operator.
oc annotate search search-v2-operator -n open-cluster-management search-pause=true
    
# 2. Enable Playground on the API deployment.
oc set env deployment/search-api -n open-cluster-management PLAYGROUND_MODE=true   

# 3. Create a route to access the API from outside the cluster.
oc create route passthrough search-api --service=search-search-api -n open-cluster-management

# 4. Open Playground from your browser.
PLAYGROUND_URL=https://$(oc get route search-api -o custom-columns=host:.spec.host --no-headers -n open-cluster-management)/playground
echo "Open Playground from your browser: $PLAYGROUND_URL"

Authentication

The Search API is protected using Kubernetes authentication. You must obtain your OCP API token and add it to the request as the Authorization header.

  • Get your API token from the OCP CLI:
    oc whoami -t
    
  • Get your API token from the OCP web console:
    Go to https://oauth-openshift.apps.{{your-ocp-cluster-url}}/oauth/token/request
    

Add the header Authorization: "Bearer {{your-api-token}}". image

Sample Queries

  1. Search for deployments:
    Query:
    query mySearch($input: [SearchInput]) {
        searchResult: search(input: $input) {
        		items
            }
    }
    
    Variables:
    {"input":[
        {
            "keywords":[],
            "filters":[
                {"property":"kind","values":["Deployment"]}],
            "limit":10
        }
    ]}
    
    Result: image

Generic Schema

The schema is one of the most important concepts when working with a GraphQL API. It specifies the capabilities of the API and defines how clients can request the data. It is often seen as a contract between the server and client. A schema is simply a collection of GraphQL types. There are 3 special root types that are entry points for all requests sent by the client:

type Query { ... }
type Mutation { ... }
type Subscription { ... }

We do not use subscription root type today.

Type Query

# Queries supported by the Search Query API.
type Query {
  # Search for resources and their relationships.
  # *[PLACEHOLDER] Results only include kubernetes resources for which the authenticated user has list permission.*
  #
  # For more information see the feature spec.
  search(input: [SearchInput]): [SearchResult]

  # Query all values for the given property.
  # Optionally, a query can be included to filter the results.
  # For example, if we want to get the names of all resources in the namespace foo, we can pass a query with the filter `{property: namespace, values:['foo']}`
  #
  # **Default limit is** 10,000
  # A value of -1 will remove the limit. Use carefully because it may impact the service.
  searchComplete(property: String!, query: SearchInput, limit: Int): [String]

  # Returns all properties from resources currently in the index.
  searchSchema: Map

  # Saved search queries for the authenticated user.
  # **[PLACEHOLDER] This query is not yet implemented in V2.**
  savedSearches: [userSearch]

  # Additional information about the service status or conditions found while processing the query.
  # This is similar to the errors query, but without implying that there was a problem processing the query.
  messages: [Message]
}

Type Mutation

These are mainly for internal uses - and used by ACM as a product.

# Supported mutations
type Mutation {
  # Delete search query for the authenticated user.
  # **[PLACEHOLDER] This query is not yet implemented in V2.**
  deleteSearch(resource: String): String

  # Save a search query for the authenticated user.
  # **[PLACEHOLDER] This query is not yet implemented in V2.**
  saveSearch(resource: String): String
}

Other Types

The types below are used above as input or output of the Queries under root Query type and Mutation type.

# Map of strings. Used to hold data for a result item.
scalar Map

# A message is used to communicate conditions detected while executing a query on the server.
type Message {
  # Unique identifier to be used by clients to process the message independently of locale or grammatical changes.
  id: String!

  # Message type.
  # **Values:** information, warning, error.
  kind: String

  # Message text.
  description: String
}


# Defines a key/value to filter results.
# When multiple values are provided for a property, it is interpreted as an OR operation.
input SearchFilter {
  # Name of the property (key).
  property: String!

  # Values for the property. Multiple values per property are interpreted as an OR operation.
  # Optionally one of these operations `=,!,!=,>,>=,<,<=` can be included at the begining of the value.
  # By default the equality operation is used.
  values: [String]!
}

# Input options to the search query.
input SearchInput {
  # List of strings to match resources.
  # Will match resources containing any of the keywords in any text field.
  # When multiple keywords are provided, it is interpreted as an AND operation.
  # Matches are case insensitive.
  keywords: [String]

  # List of SearchFilter, which is a key(property) and values.
  # When multiple filters are provided, results will match all fiters (AND operation).
  filters: [SearchFilter]

  # Max number of results returned by the query.
  # **Default is** 10,000
  # A value of -1 will remove the limit. Use carefully because it may impact the service.
  limit: Int

  # Filter relationships to the specified kinds.
  # If empty, all relationships will be included.
  # This filter is used with the 'related' field on SearchResult.
  relatedKinds: [String]
}

# Resources related to the items resolved from the search query.
type SearchRelatedResult {
  #
  kind: String!

  # Total number of related resources.
  # **NOTE:** Should not use count in combination with items. If items is requested, the count is simply the size of items.
  count: Int

  # Resources matched by the query.
  items: [Map]
}

# Data returned by the search query.
type SearchResult {
  # Total number of resources matching the query.
  # **NOTE:** Should not use count in combination with items. If items is requested, the count is simply the size of items.
  count: Int

  # Resources matching the search query.
  items: [Map]

  # Resources related to the query results (items).
  # For example, if searching for deployments, this will return the related pod resources.
  related: [SearchRelatedResult]
}

# Data required to save a user search query.
type userSearch {
  # Unique identifier of the saved search query.
  id: String

  # Name of the saved search query.
  name: String

  # Description of the saved search query.
  description: String

  # The search query in text format.
  # Example:
  # - `kind:pod,deployment namespace:default bar foo`
  searchText: String
}

Schema Design

A few words about some specific design choices.

type SearchResult {
count: Int
items: [Map]
related: [SearchRelatedResult]
}
 
type SearchRelatedResult {
kind: String!
count: Int
items: [Map]
}

SearchResult is one of the key types that are used to model resources. Count returns all the count of items in the set. Item is the array of real kubernetes resource yaml snippets that search captures. Search does NOT capture the entire resource. Related resources hold the details of the type of resources we seek to find out relation. The above values are set taking into account the filters set using SearchInput.

This generic model fits ALL types of Kubernetes resources.

Examples

Let us see below, how easily and powerful we could exploit our Query - search(input: [SearchInput]): [SearchResult] to get data for deployments, pods or for that matter of fact any other resources:

Deployment
Query:
query mySearch($input: [SearchInput]) {
    searchResult: search(input: $input) {
    		items
        }
}
Variables:
{"input":[
    {
        "keywords":[],
        "filters":[
            {"property":"kind","values":["Deployment"]}],
        "limit":10
    }
]}

In this case, SearchResult holds data about deployments. By adjusting the filters, this could be across the fleet, tied down to a namespace across the fleet or pegged down to a namespace and a cluster. Additionally we could filter by other properties of the resource.

Pod
Query:
query mySearch($input: [SearchInput]) {
    searchResult: search(input: $input) {
    		items
        }
}
Variables:
{"input":[
    {
        "keywords":[],
        "filters":[
            {"property":"kind","values":["Pod"]}],
        "limit":10
    }
]}

In this case, SearchResult holds data about pods. By adjusting the filters, this could be across the fleet, tied down to a namespace across the fleet or pegged down to a namespace and a cluster. Additionally we could filter by other properties of the resource.

Any Other resource
Query:
query mySearch($input: [SearchInput]) {
    searchResult: search(input: $input) {
    		items
        }
}
Variables:
{"input":[
    {
        "keywords":[],
        "filters":[
            {"property":"kind","values":["AnyOtherResource"]}],
        "limit":10
    }
]}

In this case, SearchResult holds data about AnyOtherResource. By adjusting the filters, we could get resources across the fleet, tied down to a namespace across the fleet or pegged down to a namespace and a cluster. Additionally we could filter by other properties of the resource.

Related Resources

If instead of looking at a resource directly, we want to look at a service related to a deployment, it’s a simple tweak - note, in effect it's the consumer who decides what data needs to be returned from the same endpoint by using the query. In a standard REST API paradigm, this would usually involve in calling multiple endpoints.

Query:
query mySearch($input: [SearchInput]) {
    searchResult: search(input: $input) {
    		items,
             related{
             items
             },       
        }
}
Variables:
{"input":[
    {
        "keywords":[],
        "filters":[
            {"property":"kind","values":["Deployment"]}],
            "relatedKinds": ["Service"],
        "limit":10
    }
]}

In this case, SearchResult holds data about deployments and also its related services.

Property field in Filter You may be wondering

"filters":[
            {"property":"???","values":["..."]}],

What values are allowed in the property field. All kubernetes yaml consists of key value pairs. The following query lists out all the keys which are collected from across the fleet. And the keys can be used as a property name in the SearchInput Filter. Values will need to be provided by the user using kubernetes knowledge. For example -

  • If property = kind: value = all kubernetes resource types : Pod, ConfigMap, Deployment, ManagedCluster (custom resource) etc.
  • If property = status: value = Error | Running or any other special status you may have related to the kind selected.
query {
 searchSchema
}

Returns
{
   "data": {
       "searchSchema": {
           "allProperties": [
               "cluster",
               "kind",
               "label",
               "name",
               "namespace",
               "status",
               "kubernetesVersion",
               "created",
               "apigroup",
               "apiversion",
               "current",
               "desired",
               "startedAt",
               "image",
               "container",
               "podIP",
               "restarts",
               "hostIP",
               "clusterIP",
               "port",
               "type",
               "claimRef",
               "accessMode",
               "capacity",
               "reclaimPolicy",
               "ready",
               "available",
               "updated",
               "parallelism",
               "successful",
               "completions",
               "url",
               "sourceType",
               "request",
               "storageClassName",
               "volumeName",
               "targetRevision",
               "destinationServer",
               "destinationNamespace",
               "path",
               "repoURL",
               "package",
               "localPlacement",
               "timeWindow",
               "channel",
               "packageFilterVersion",
               "suspend",
               "lastSchedule",
               "schedule",
               "active",
               "architecture",
               "cpu",
               "osImage",
               "role",
               "pathname",
               "memory",
               "nodes",
               "HubAcceptedManagedCluster",
               "ManagedClusterImportSucceeded",
               "ManagedClusterJoined",
               "ManagedClusterConditionAvailable",
               "addon",
               "consoleURL",
               "low",
               "critical",
               "important",
               "moderate",
               "rules",
               "category",
               "numRuleViolations",
               "scope",
               "remediationAction",
               "numRules",
               "disabled",
               "compliant"
           ]
       }
   }
}

SearchComplete

In all examples above, we have used

searchResult: search(input: $input)

And SearchResults returns:

type SearchResult {
count: Int
items: [Map]
related: [SearchRelatedResult]
}

What if we do not want a count or the items itself or related resources, but some field of the items itself - for example pod names. We need to direct our model to query SearchComplete.

searchComplete(
property: String!
query: SearchInput
limit: Int
): [String]

Let’s look at the above - search just needed an array - [SearchInput]. This does not take an array but just an instance. In addition this needs a property and a limit (is this really needed Jorge Padilla because it’s already in SearchInput). And it will return a String array.

Now, let us look at an example. This returns a property of the Pod which is called “name” - ie Name of pods.

Query:
query myBS($property: String!, $zing: SearchInput, $limit:Int) {
  searchComplete(property:$property, query: $zing, limit:$limit)
}

Variable:
{
  "property": "name",
  "zing":
    {
        "keywords":[],
        "filters":[
            {"property":"kind","values":["Pod"]}],
        "limit":10
    }
  
}