Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental] Add Subscription Support for the Tanzu Hub Client #195

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 80 additions & 20 deletions client/hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,93 @@ equivalent functionality can be introduced.

## Creating a Tanzu Hub Client

To create a Tanzu Hub client, use the `CreateHubClient(contextName string)` API
To create a Tanzu Hub client, use the `NewClient(contextName string)` API
by providing the `tanzu` context name. An authenticated Tanzu Hub client for the specified tanzu context will be returned.
This client includes an authenticated GraphQLClient from the `github.com/Khan/genqlient` package
that can be used to do GraphQL queries. Internally it configures the client with an access token for each request.
Internally it configures the client with an access token for each request.
By default, it will get the Tanzu Hub endpoint from the specified context metadata. To specify any custom Tanzu Hub
endpoint for testing please configure the `TANZU_HUB_GRAPHQL_ENDPOINT` environment variable.
endpoint for testing please configure the `TANZU_HUB_ENDPOINT` environment variable.

Note that the authenticated client is assured to have at least 30 min access to the GraphQL endpoint.
If you want a long running client beyond this period, recommendation is to reinitialize your client.

## Generating the golang stub to invoke graphQL queries
## Examples
anujc25 marked this conversation as resolved.
Show resolved Hide resolved

There are many golang client libraries for the graphQL, however, Tanzu Plugin Runtime uses `github.com/Khan/genqlient` and
also returns the corresponding graphQL client as part of the Tanzu Hub client creation.
### Query/Mutation

github.com/Khan/genqlient is a Go library to easily generate type-safe code to query a GraphQL API.
```golang
const QueryAllProjects_Operation = `
query QueryAllProjects {
applicationEngineQuery {
queryProjects(first: 1000) {
projects {
name
}
}
}
}`

To help plugin authors generate the stub for the Tanzu Hub endpoint, a [tanzuhub.mk](../../hack/hub/tanzuhub.mk) has been provided.
This makefile provides an easy means for the plugin authors to initialize a `hub` package and also generate the stub from the graphQL queries.
To use this library plugin authors can follow the below steps:
// getProjects is a wrapper of an `QueryAllProjects“ API call to fetch project names
func getProjects(contextName string) ([]string, error) {
hc, err := NewClient(contextName )

1. Copy the [tanzuhub.mk](../../hack/hub/tanzuhub.mk) to your project and import it to your `Makefile` with `include ./tanzuhub.mk`
2. Configure the `TANZU_HUB_SCHEMA_FILE_URL` environment variable to the `schema.graphql` of the Tanzu Hub
3. Run `make tanzu-hub-stub-init` to initialize a `hub` package. This will create the following files under the `hub` package:
* `genqlient.yaml`: Configuration file for generating golang code from GraphQL query with `github.com/Khan/genqlient`
* `queries.graphql`: File to write all graphQL queries
* `main.go`: A golang file with necessary imports to easily run `go generate` to generate stub code
4. Once the initialization is done, you can add your GraphQL queries to the `queries.graphql` file
5. After adding new graphQL queries or updating an existing query, run `make tanzu-hub-stub-generate` to generate a golang stub for the GraphQL queries
* This will create a `generate.go` file under the `hub` package with golang APIs that can be consumed directly by other packages by passing the GraphQLClient available with TanzuHub client
req := &hub.Request{
OpName: "QueryAllProjects",
Query: QueryAllProjects_Operation,
}
var responseData QueryAllProjectsResponse // Assuming the response type is already defined
err := hc.Request(context.Background(), req, &responseData)
if err != nil {
return nil, err
}

// Process the response
projects := []string{}
for _, p := range responseData.ApplicationEngineQuery.QueryProjects.Projects {
projects = append(projects, p.Name)
}

return projects, nil
}
```

### Subscriptions

```golang
const SubscribeAppLogs_Operation = `
subscription appLogs($appEntityId: EntityId!) {
kubernetesAppLogs(appEntityId: $appEntityId, logParams: {includeTimestamps: true, tailLines: 50, includePrevious: false}) {
value
timestamp
}
}`

func subscribeAppLogs(contextName, appEntityId string) ([]string, error) {
hc, err := NewClient(contextName )

req := &hub.Request{
OpName: "SubscribeAppLogs",
Query: SubscribeAppLogs_Operation,
Variables: map[string]string{"appEntityId": appEntityId}
}

err := hc.Subscribe(context.Background(), req, logEventHandler)
if err != nil {
return nil, err
}

return nil
}

func logEventHandler(eventResponse EventResponse) {
respData := eventResponse.Data
fmt.Println(respData)
}
```

## Generating the golang stub for the GraphQL queries

There are many golang client libraries for the GraphQL, however, Tanzu Plugin Runtime recommends
using `github.com/Khan/genqlient` for generating type definitions for your GraphQL queries.

Note: Client does not require users to use this library for generating type definitions and users can always define the Golang type definitions
for the specified query by themselves.
98 changes: 56 additions & 42 deletions client/hub/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,40 @@
package hub

import (
"fmt"
"context"
"net/http"
"os"

"github.com/Khan/genqlient/graphql"
"github.com/pkg/errors"

"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
)

const (
EnvTanzuHubGraphQLEndpoint = "TANZU_HUB_GRAPHQL_ENDPOINT"
EnvTanzuHubEndpoint = "TANZU_HUB_ENDPOINT"
)

// HubClient client to talk to Tanzu Hub through GraphQL apis
// It includes authenticated GraphQL client from github.com/Khan/genqlient
// that can be used to do GraphQL queries
// Client is an interface for the Tanzu Hub Client
type Client interface {
marckhouzam marked this conversation as resolved.
Show resolved Hide resolved
// Request sends a GraphQL request to the Tanzu Hub endpoint
//
// ctx context.Context: The context for the request. If provided, it will be used to cancel the request if the context is canceled.
// req *Request: The GraphQL request to be sent.
// responseData interface{}: The interface to store the response data. The response data will be unmarshaled into this interface.
Request(ctx context.Context, req *Request, responseData interface{}) error

// Subscribe to a GraphQL endpoint and streams events to the provided handler
//
// ctx context.Context: The context for the subscription. If provided, it will be used to cancel the subscription if the context is canceled.
// req *Request: The GraphQL subscription request to be sent.
// handler EventResponseHandler: The handler function to process incoming events.
Subscribe(ctx context.Context, req *Request, handler EventResponseHandler) error
}

// HubClient client to talk to Tanzu Hub through GraphQL APIs
type HubClient struct {
anujc25 marked this conversation as resolved.
Show resolved Hide resolved
// ContextName is Tanzu CLI context name
ContextName string
// GraphQLClient can be used to do graphql queries
GraphQLClient graphql.Client
// contextName is Tanzu CLI context name
contextName string

accessToken string
tanzuHubEndpoint string
Expand All @@ -35,86 +47,88 @@ type HubClient struct {

type ClientOptions func(o *HubClient)

// WithAccessToken creates the HubClient using the specified Access Token
// WithAccessToken creates the Client using the specified Access Token
func WithAccessToken(token string) ClientOptions {
return func(c *HubClient) {
c.accessToken = token
}
}

// WithEndpoint creates the HubClient using the specified Endpoint
// WithEndpoint creates the Client using the specified Endpoint
func WithEndpoint(endpoint string) ClientOptions {
return func(c *HubClient) {
c.tanzuHubEndpoint = endpoint
}
}

// WithHTTPClient creates the HubClient using the specified HttpClient
// WithHTTPClient creates the Client using the specified HttpClient
func WithHTTPClient(httpClient *http.Client) ClientOptions {
return func(c *HubClient) {
c.httpClient = httpClient
}
}

// CreateHubClient returns an authenticated Tanzu Hub client for the specified
// tanzu context. This client includes an authenticated GraphQLClient from github.com/Khan/genqlient
// that can be used to do GraphQL queries.
// Internally it configures the client with CSP access token for each request
// NewClient returns an authenticated Tanzu Hub client for the specified
// tanzu context. Internally it configures the client with CSP access token for each request
//
// Note that the authenticated client is assured to have at least 30 min access to the GraphQL endpoint.
// If you want a long running client beyond this period, recommendation is to reinitialize your client.
//
// EXPERIMENTAL: Both the function's signature and implementation are subjected to change/removal
// if an alternative means to provide equivalent functionality can be introduced.
func CreateHubClient(contextName string, opts ...ClientOptions) (*HubClient, error) {
hc := &HubClient{
ContextName: contextName,
func NewClient(contextName string, opts ...ClientOptions) (Client, error) {
c := &HubClient{
contextName: contextName,
}

// configure all options for the HubClient
for _, o := range opts {
o(hc)
o(c)
}

httpClient, err := hc.getHTTPClient(contextName)
err := c.initializeClient(contextName)
if err != nil {
return nil, err
}
hc.GraphQLClient = graphql.NewClient(fmt.Sprintf("%s/graphql", hc.tanzuHubEndpoint), httpClient)
return hc, nil
return c, nil
}

func (hc *HubClient) getHTTPClient(contextName string) (*http.Client, error) {
func (c *HubClient) initializeClient(contextName string) error {
var err error
if hc.httpClient != nil {
return hc.httpClient, nil
}

if hc.accessToken == "" {
hc.accessToken, err = config.GetTanzuContextAccessToken(contextName)
// Set accessToken if it is not already set
if c.accessToken == "" {
c.accessToken, err = config.GetTanzuContextAccessToken(contextName)
if err != nil {
return nil, err
return err
}
}

if hc.tanzuHubEndpoint == "" {
hc.tanzuHubEndpoint, err = getTanzuHubEndpointFromContext(contextName)
// Set tanzuHubEndpoint if it is not already set
if c.tanzuHubEndpoint == "" {
marckhouzam marked this conversation as resolved.
Show resolved Hide resolved
c.tanzuHubEndpoint, err = getTanzuHubEndpointFromContext(contextName)
if err != nil {
return nil, err
return err
}
}

// Set httpClient if it is not already set
if c.httpClient == nil {
c.httpClient = &http.Client{
Transport: &authTransport{
accessToken: c.accessToken,
wrapped: http.DefaultTransport,
},
Timeout: 0,
}
}

return &http.Client{
Transport: &authTransport{
accessToken: hc.accessToken,
wrapped: http.DefaultTransport,
},
}, nil
return nil
}

func getTanzuHubEndpointFromContext(contextName string) (string, error) {
// If `TANZU_HUB_GRAPHQL_ENDPOINT` environment variable is configured use that
if endpoint := os.Getenv(EnvTanzuHubGraphQLEndpoint); endpoint != "" {
// If `TANZU_HUB_ENDPOINT` environment variable is configured use that
if endpoint := os.Getenv(EnvTanzuHubEndpoint); endpoint != "" {
return endpoint, nil
}

Expand Down
28 changes: 0 additions & 28 deletions client/hub/client_generated_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading