Skip to content

Add tool list_unstructured_data_pipelines_for_user#254

Open
gshikhar2021 wants to merge 2 commits into
redhat-data-and-ai:mainfrom
gshikhar2021:list-pipelines
Open

Add tool list_unstructured_data_pipelines_for_user#254
gshikhar2021 wants to merge 2 commits into
redhat-data-and-ai:mainfrom
gshikhar2021:list-pipelines

Conversation

@gshikhar2021

Copy link
Copy Markdown
Contributor
  • this tool will list all the CR from kubernetes cluster using service account role and access using incluster config
  • list all the databases from snowflake using user's OAuth token

Shikhar Gupta added 2 commits June 26, 2026 17:02
- this tool will list all the CR from kubernetes cluster using service account role and access
- list all the databases from snowflake
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new Model Context Protocol (MCP) server component to the controller. The primary goal is to provide AI assistants with the ability to query and list both Kubernetes-based data pipelines and Snowflake databases. The changes include the necessary client-side logic for both platforms, secure token handling within the context, and the full suite of Kubernetes manifests required to deploy the server within the cluster.

Highlights

  • New MCP Tool Implementation: Added the 'list_unstructured_data_pipelines_for_user' tool, which aggregates and lists Kubernetes UnstructuredDataPipeline custom resources and Snowflake databases.
  • Infrastructure and Deployment: Added comprehensive Kubernetes manifests, including RBAC, ConfigMaps, and Deployments, along with Makefile targets to build and deploy the MCP server.
  • Client Integration: Implemented a Kubernetes in-cluster client for resource discovery and a Snowflake client that utilizes OAuth tokens for secure database querying.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/test-e2e.yml
  • Ignored by pattern: vendor/** (281)
    • vendor/github.com/99designs/go-keychain/.gitignore
    • vendor/github.com/99designs/go-keychain/.golangci.yml
    • vendor/github.com/99designs/go-keychain/.travis.yml
    • vendor/github.com/99designs/go-keychain/LICENSE
    • vendor/github.com/99designs/go-keychain/README.md
    • vendor/github.com/99designs/go-keychain/corefoundation.go
    • vendor/github.com/99designs/go-keychain/datetime.go
    • vendor/github.com/99designs/go-keychain/ios.go
    • vendor/github.com/99designs/go-keychain/keychain.go
    • vendor/github.com/99designs/go-keychain/macos.go
    • vendor/github.com/99designs/go-keychain/util.go
    • vendor/github.com/99designs/keyring/.gitattributes
    • vendor/github.com/99designs/keyring/.gitignore
    • vendor/github.com/99designs/keyring/.golangci.yml
    • vendor/github.com/99designs/keyring/LICENSE
    • vendor/github.com/99designs/keyring/README.md
    • vendor/github.com/99designs/keyring/Vagrantfile
    • vendor/github.com/99designs/keyring/array.go
    • vendor/github.com/99designs/keyring/config.go
    • vendor/github.com/99designs/keyring/docker-compose.yml
    • vendor/github.com/99designs/keyring/file.go
    • vendor/github.com/99designs/keyring/keychain.go
    • vendor/github.com/99designs/keyring/keyctl.go
    • vendor/github.com/99designs/keyring/keyring.go
    • vendor/github.com/99designs/keyring/kwallet.go
    • vendor/github.com/99designs/keyring/pass.go
    • vendor/github.com/99designs/keyring/prompt.go
    • vendor/github.com/99designs/keyring/secretservice.go
    • vendor/github.com/99designs/keyring/tilde.go
    • vendor/github.com/99designs/keyring/wincred.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/CHANGELOG.md
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/LICENSE.txt
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/README.md
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/ci.yml
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/cloud.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/doc.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/core.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/doc.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/errors.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/etag.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/exported.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/pipeline.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/request.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/response_error.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log/log.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async/async.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body/body.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/loc/loc.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/op/op.go
    • vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/poller.go
    • ... and -231 more files (display capped at 50)
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an MCP server for the unstructured data controller, adding Dockerfile configurations, deployment manifests, and tools to list pipelines and Snowflake databases. Key feedback includes removing a hardcoded 'SHIKHAR%' pattern from the Snowflake database query, simplifying redundant connection pool configurations on short-lived database connections, allowing the pipeline listing tool to return partial results upon partial failures, and adding a local kubeconfig fallback to the Kubernetes client to improve the local development experience.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/snowflake/client.go

// Run SHOW DATABASES query
// This will return only databases the authenticated user has access to (RBAC enforced by Snowflake)
rows, err := db.QueryContext(ctx, "SHOW DATABASES LIKE 'SHIKHAR%';")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The query SHOW DATABASES LIKE 'SHIKHAR%'; contains a hardcoded pattern 'SHIKHAR%' which appears to be a leftover from development/testing. This will prevent users from seeing any databases that do not match this prefix. It should be removed to list all databases the user has access to.

Suggested change
rows, err := db.QueryContext(ctx, "SHOW DATABASES LIKE 'SHIKHAR%';")
rows, err := db.QueryContext(ctx, "SHOW DATABASES;")

Comment thread pkg/snowflake/client.go
Comment on lines +64 to +70
// Configure connection pool for security
// MaxOpenConns=1: Single connection per request (we open/close immediately)
// MaxIdleConns=0: Don't keep connections idle (reduces token exposure time)
// ConnMaxLifetime=5min: Close connections after 5 minutes (limits token lifetime)
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
db.SetConnMaxLifetime(5 * time.Minute)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the db connection pool is opened and immediately closed (via defer db.Close()) within the lifecycle of a single ShowDatabases call, configuring connection pool limits like SetMaxIdleConns(0) and SetConnMaxLifetime(5 * time.Minute) is redundant and has no effect. If you intend to keep the connection pool open across multiple requests, you should manage the sql.DB lifecycle outside of this function. Otherwise, these configuration lines can be simplified or removed.

Comment on lines +53 to +77
// List Kubernetes UnstructuredDataPipeline CRs
var pipelines []k8sclient.PipelineInfo
if k8sClient != nil {
var err error
pipelines, err = k8sClient.ListPipelines(ctx)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{
Text: fmt.Sprintf("Error listing pipelines: %v", err),
}},
IsError: true,
}, nil, nil
}
}

// Query Snowflake databases using the user's OAuth token
databases, err := snowflake.ShowDatabases(ctx, oauthToken)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{
Text: fmt.Sprintf("Error querying Snowflake: %v", err),
}},
IsError: true,
}, nil, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tool currently fails completely if either the Kubernetes client or the Snowflake client encounters an error. Consider making the tool more resilient by returning partial results (e.g., returning the successfully fetched pipelines even if Snowflake queries fail, or vice versa) along with an error/warning message in the response content, rather than failing the entire request.

Comment thread pkg/k8sclient/client.go
Comment on lines +50 to +58
func NewInClusterClient() (*Client, error) {
// rest.InClusterConfig() reads from:
// 1. /var/run/secrets/kubernetes.io/serviceaccount/token (for auth)
// 2. /var/run/secrets/kubernetes.io/serviceaccount/ca.crt (for TLS)
// 3. KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT env vars
config, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to get in-cluster config: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The NewInClusterClient function currently only supports in-cluster configuration. If the server is run locally for development or debugging, it will fail to start and exit. Consider falling back to loading the local kubeconfig (using clientcmd.NewNonInteractiveDeferredLoadingClientConfig) when rest.InClusterConfig() returns rest.ErrNotInCluster. This significantly improves the local development experience.

@vinamra28

Copy link
Copy Markdown
Member

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Model Context Protocol (MCP) server deployment, including a Dockerfile, Makefile targets, Kubernetes manifests, and tools for health checks and listing pipelines. Key feedback focuses on removing a hardcoded testing query in the Snowflake client, dynamically detecting the Kubernetes namespace instead of hardcoding it, utilizing config.GetConfig() to support seamless local development, and replacing a hardcoded condition string with an existing API constant.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/snowflake/client.go

// Run SHOW DATABASES query
// This will return only databases the authenticated user has access to (RBAC enforced by Snowflake)
rows, err := db.QueryContext(ctx, "SHOW DATABASES LIKE 'SHIKHAR%';")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The query SHOW DATABASES LIKE 'SHIKHAR%'; is hardcoded to only list databases starting with 'SHIKHAR%'. This appears to be a leftover from local testing and will prevent other users from listing their databases. It should be changed to a generic SHOW DATABASES; query to list all databases the authenticated user has access to.

Suggested change
rows, err := db.QueryContext(ctx, "SHOW DATABASES LIKE 'SHIKHAR%';")
rows, err := db.QueryContext(ctx, "SHOW DATABASES;")

Comment thread pkg/k8sclient/client.go
Comment on lines +42 to +45
// Client wraps a Kubernetes client for accessing cluster resources
type Client struct {
client client.Client
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Add a namespace field to the Client struct so that we can dynamically list pipelines in the namespace where the MCP server is deployed, rather than relying on a hardcoded namespace.

Suggested change
// Client wraps a Kubernetes client for accessing cluster resources
type Client struct {
client client.Client
}
// Client wraps a Kubernetes client for accessing cluster resources
type Client struct {
client client.Client
namespace string
}

Comment thread pkg/k8sclient/client.go
Comment on lines +50 to +69
func NewInClusterClient() (*Client, error) {
// rest.InClusterConfig() reads from:
// 1. /var/run/secrets/kubernetes.io/serviceaccount/token (for auth)
// 2. /var/run/secrets/kubernetes.io/serviceaccount/ca.crt (for TLS)
// 3. KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT env vars
config, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to get in-cluster config: %w", err)
}

// Create a controller-runtime client with our scheme (includes CRDs)
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
}

return &Client{
client: k8sClient,
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update the client constructor to use config.GetConfig() for automatic local/in-cluster configuration loading, and dynamically read the current namespace from the service account mount path /var/run/secrets/kubernetes.io/serviceaccount/namespace with a fallback to the default namespace.

func NewInClusterClient() (*Client, error) {
	// config.GetConfig() automatically handles both in-cluster config
	// and local kubeconfig fallback, making local development seamless.
	cfg, err := config.GetConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to get kubernetes config: %w", err)
	}

	// Create a controller-runtime client with our scheme (includes CRDs)
	k8sClient, err := client.New(cfg, client.Options{Scheme: scheme})
	if err != nil {
		return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
	}

	// Read the namespace from the service account directory, falling back to default
	ns := "unstructured-controller-namespace"
	if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
		ns = strings.TrimSpace(string(data))
	}

	return &Client{
		client:    k8sClient,
		namespace: ns,
	}, nil
}

Comment thread pkg/k8sclient/client.go
Comment on lines +80 to +90
func (c *Client) ListPipelines(ctx context.Context) ([]PipelineInfo, error) {
pipelineList := &operatorv1alpha1.UnstructuredDataPipelineList{}

// List all pipelines in the unstructured-controller-namespace
// This will use the ServiceAccount's RBAC permissions
err := c.client.List(ctx, pipelineList, &client.ListOptions{
Namespace: "unstructured-controller-namespace",
})
if err != nil {
return nil, fmt.Errorf("failed to list pipelines: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the dynamically detected namespace instead of the hardcoded namespace.

Suggested change
func (c *Client) ListPipelines(ctx context.Context) ([]PipelineInfo, error) {
pipelineList := &operatorv1alpha1.UnstructuredDataPipelineList{}
// List all pipelines in the unstructured-controller-namespace
// This will use the ServiceAccount's RBAC permissions
err := c.client.List(ctx, pipelineList, &client.ListOptions{
Namespace: "unstructured-controller-namespace",
})
if err != nil {
return nil, fmt.Errorf("failed to list pipelines: %w", err)
}
func (c *Client) ListPipelines(ctx context.Context) ([]PipelineInfo, error) {
pipelineList := &operatorv1alpha1.UnstructuredDataPipelineList{}
// List all pipelines in the detected namespace
// This will use the ServiceAccount's RBAC permissions
err := c.client.List(ctx, pipelineList, &client.ListOptions{
Namespace: c.namespace,
})
if err != nil {
return nil, fmt.Errorf("failed to list pipelines: %w", err)
}

Comment thread pkg/k8sclient/client.go
Comment on lines +19 to +29
import (
"context"
"fmt"

operatorv1alpha1 "github.com/redhat-data-and-ai/unstructured-data-controller/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support seamless local development and testing, we can use sigs.k8s.io/controller-runtime/pkg/client/config's GetConfig() which automatically falls back to the local kubeconfig when run outside of a Kubernetes cluster. Let's update the imports to include the config package and other standard packages needed for dynamic namespace detection.

import (
	"context"
	"fmt"
	"os"
	"strings"

	operatorv1alpha1 "github.com/redhat-data-and-ai/unstructured-data-controller/api/v1alpha1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/config"
)

Comment thread pkg/k8sclient/client.go
Comment on lines +101 to +103
for _, condition := range pipeline.Status.Conditions {
if condition.Type == "UnstructuredDataPipelineReady" {
info.Status = string(condition.Status)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The condition type "UnstructuredDataPipelineReady" is hardcoded here, but there is already a constant UnstructuredDataPipelineCondition defined in api/v1alpha1/unstructureddatapipeline_types.go for this exact purpose. Using the defined constant improves maintainability and prevents potential typos.

Suggested change
for _, condition := range pipeline.Status.Conditions {
if condition.Type == "UnstructuredDataPipelineReady" {
info.Status = string(condition.Status)
for _, condition := range pipeline.Status.Conditions {
if condition.Type == operatorv1alpha1.UnstructuredDataPipelineCondition {
info.Status = string(condition.Status)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants