Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions devops-mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The Google Cloud DevOps MCP Server provides a suite of tools for:

* **Artifact Registry:** Setting up and managing repositories.
* **Cloud Build:** Managing and running build triggers.
* **Cloud Deploy:** Managing delivery pipelines, targets, releases, and rollouts.
* **Cloud Run:** Deploying and managing services from images or source.
* **Cloud Storage:** Managing buckets and uploading source code.
* **Developer Connect:** Establishing connections to git repositories.
Expand Down
42 changes: 42 additions & 0 deletions devops-mcp-server/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,48 @@ Runs a Cloud Build trigger.
- `tag` (string, optional): The tag to run the trigger at (regex).
- `commit_sha` (string, optional): The exact commit SHA to run the trigger at.

## Cloud Deploy

### `clouddeploy.list_delivery_pipelines`
Lists the Cloud Deploy delivery pipelines in a specified GCP project and location.

**Arguments:**
- `project_id` (string, required): The Google Cloud project ID.
- `location` (string, required): The Google Cloud location.

### `clouddeploy.list_targets`
Lists the Cloud Deploy targets in a specified GCP project and location.

**Arguments:**
- `project_id` (string, required): The Google Cloud project ID.
- `location` (string, required): The Google Cloud location.

### `clouddeploy.list_releases`
Lists the Cloud Deploy releases for a specified pipeline.

**Arguments:**
- `project_id` (string, required): The Google Cloud project ID.
- `location` (string, required): The Google Cloud location.
- `pipeline_id` (string, required): The Delivery Pipeline ID.

### `clouddeploy.list_rollouts`
Lists the Cloud Deploy rollouts for a specified release.

**Arguments:**
- `project_id` (string, required): The Google Cloud project ID.
- `location` (string, required): The Google Cloud location.
- `pipeline_id` (string, required): The Delivery Pipeline ID.
- `release_id` (string, required): The Release ID.

### `clouddeploy.create_release`
Creates a new Cloud Deploy release for a specified delivery pipeline.

**Arguments:**
- `project_id` (string, required): The Google Cloud project ID.
- `location` (string, required): The Google Cloud location.
- `pipeline_id` (string, required): The Delivery Pipeline ID.
- `release_id` (string, required): The ID of the release to create.

## Cloud Run

### `cloudrun.list_services`
Expand Down
162 changes: 162 additions & 0 deletions devops-mcp-server/clouddeploy/client/clouddeployclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clouddeployclient

import (
"context"
"fmt"

deploy "cloud.google.com/go/deploy/apiv1"
deploypb "cloud.google.com/go/deploy/apiv1/deploypb"
"google.golang.org/api/iterator"
)

// contextKey is a private type to use as a key for context values.
type contextKey string

const (
cloudDeployClientContextKey contextKey = "cloudDeployClient"
)

// ClientFrom returns the CloudDeployClient stored in the context, if any.
func ClientFrom(ctx context.Context) (CloudDeployClient, bool) {
client, ok := ctx.Value(cloudDeployClientContextKey).(CloudDeployClient)
return client, ok
}

// ContextWithClient returns a new context with the provided CloudDeployClient.
func ContextWithClient(ctx context.Context, client CloudDeployClient) context.Context {
return context.WithValue(ctx, cloudDeployClientContextKey, client)
}

// CloudDeployClient is an interface for interacting with the Cloud Deploy API.
type CloudDeployClient interface {
ListDeliveryPipelines(ctx context.Context, projectID, location string) ([]*deploypb.DeliveryPipeline, error)
ListTargets(ctx context.Context, projectID, location string) ([]*deploypb.Target, error)
ListReleases(ctx context.Context, projectID, location, pipelineID string) ([]*deploypb.Release, error)
ListRollouts(ctx context.Context, projectID, location, pipelineID, releaseID string) ([]*deploypb.Rollout, error)
CreateRelease(ctx context.Context, projectID, location, pipelineID, releaseID string) (*deploy.CreateReleaseOperation, error)
}

// NewCloudDeployClient creates a new Cloud Deploy client.
func NewCloudDeployClient(ctx context.Context) (CloudDeployClient, error) {
c, err := deploy.NewCloudDeployClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create Cloud Deploy client: %v", err)
}

return &CloudDeployClientImpl{
client: c,
}, nil
}

// CloudDeployClientImpl is an implementation of the CloudDeployClient interface.
type CloudDeployClientImpl struct {
client *deploy.CloudDeployClient
}

// ListDeliveryPipelines lists Delivery Pipelines in a given project and location
func (c *CloudDeployClientImpl) ListDeliveryPipelines(ctx context.Context, projectID, location string) ([]*deploypb.DeliveryPipeline, error) {
req := &deploypb.ListDeliveryPipelinesRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
}
it := c.client.ListDeliveryPipelines(ctx, req)
var pipelines []*deploypb.DeliveryPipeline
for {
pipeline, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list delivery pipelines: %w", err)
}
pipelines = append(pipelines, pipeline)
}
return pipelines, nil
}

// ListTargets lists Targets in a given project and location
func (c *CloudDeployClientImpl) ListTargets(ctx context.Context, projectID, location string) ([]*deploypb.Target, error) {
req := &deploypb.ListTargetsRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
}
it := c.client.ListTargets(ctx, req)
var targets []*deploypb.Target
for {
target, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list targets: %w", err)
}
targets = append(targets, target)
}
return targets, nil
}

// ListReleases lists Releases for a specific Delivery Pipeline
func (c *CloudDeployClientImpl) ListReleases(ctx context.Context, projectID, location, pipelineID string) ([]*deploypb.Release, error) {
req := &deploypb.ListReleasesRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s/deliveryPipelines/%s", projectID, location, pipelineID),
}
it := c.client.ListReleases(ctx, req)
var releases []*deploypb.Release
for {
release, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list releases: %w", err)
}
releases = append(releases, release)
}
return releases, nil
}

// ListRollouts lists Rollouts for a specific Release
func (c *CloudDeployClientImpl) ListRollouts(ctx context.Context, projectID, location, pipelineID, releaseID string) ([]*deploypb.Rollout, error) {
req := &deploypb.ListRolloutsRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s/deliveryPipelines/%s/releases/%s", projectID, location, pipelineID, releaseID),
}
it := c.client.ListRollouts(ctx, req)
var rollouts []*deploypb.Rollout
for {
rollout, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list rollouts: %w", err)
}
rollouts = append(rollouts, rollout)
}
return rollouts, nil
}
Comment on lines +71 to +148

Choose a reason for hiding this comment

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

medium

The four List... functions (ListDeliveryPipelines, ListTargets, ListReleases, ListRollouts) contain very similar logic for iterating through pages of results. This duplicated code can be refactored into a single generic helper function to improve maintainability and reduce boilerplate.

Consider creating a generic function to handle the pagination logic, which would make the List... methods much more concise. For example:

type itemIterator[T any] interface {
	Next() (T, error)
}

func listAll[T any](it itemIterator[T], errorFormat string) ([]T, error) {
	var items []T
	for {
		item, err := it.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, fmt.Errorf(errorFormat, err)
		}
		items = append(items, item)
	}
	return items, nil
}

// Then, for example, ListDeliveryPipelines becomes:
func (c *CloudDeployClientImpl) ListDeliveryPipelines(ctx context.Context, projectID, location string) ([]*deploypb.DeliveryPipeline, error) {
	req := &deploypb.ListDeliveryPipelinesRequest{
		Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
	}
	it := c.client.ListDeliveryPipelines(ctx, req)
	return listAll(it, "failed to list delivery pipelines: %w")
}


// CreateRelease creates a new Release to trigger a deployment
func (c *CloudDeployClientImpl) CreateRelease(ctx context.Context, projectID, location, pipelineID, releaseID string) (*deploy.CreateReleaseOperation, error) {
req := &deploypb.CreateReleaseRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s/deliveryPipelines/%s", projectID, location, pipelineID),
ReleaseId: releaseID,
Release: &deploypb.Release{},

Choose a reason for hiding this comment

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

high

The CreateRelease function is creating a release with an empty Release object. While the API requires the release field, sending an empty object means the resulting release will not have any build artifacts to deploy, making it not very useful for most CI/CD scenarios.

The clouddeploy.create_release tool should be extended to accept parameters needed to construct a meaningful Release object, such as build artifacts (e.g., Docker image URIs). This would involve updating the tool's arguments, the client function signature, and this implementation to populate the Release proto.

Suggested change
Release: &deploypb.Release{},
Release: &deploypb.Release{}, // TODO: Populate release details from arguments.

}
op, err := c.client.CreateRelease(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create release: %w", err)
}
return op, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mocks

import (
"context"

deploy "cloud.google.com/go/deploy/apiv1"
deploypb "cloud.google.com/go/deploy/apiv1/deploypb"
)

// MockCloudDeployClient is a mock implementation of the CloudDeployClient interface.
type MockCloudDeployClient struct {
ListDeliveryPipelinesFunc func(ctx context.Context, projectID, location string) ([]*deploypb.DeliveryPipeline, error)
ListTargetsFunc func(ctx context.Context, projectID, location string) ([]*deploypb.Target, error)
ListReleasesFunc func(ctx context.Context, projectID, location, pipelineID string) ([]*deploypb.Release, error)
ListRolloutsFunc func(ctx context.Context, projectID, location, pipelineID, releaseID string) ([]*deploypb.Rollout, error)
CreateReleaseFunc func(ctx context.Context, projectID, location, pipelineID, releaseID string) (*deploy.CreateReleaseOperation, error)
}

func (m *MockCloudDeployClient) ListDeliveryPipelines(ctx context.Context, projectID, location string) ([]*deploypb.DeliveryPipeline, error) {
if m.ListDeliveryPipelinesFunc != nil {
return m.ListDeliveryPipelinesFunc(ctx, projectID, location)
}
return nil, nil
}

func (m *MockCloudDeployClient) ListTargets(ctx context.Context, projectID, location string) ([]*deploypb.Target, error) {
if m.ListTargetsFunc != nil {
return m.ListTargetsFunc(ctx, projectID, location)
}
return nil, nil
}

func (m *MockCloudDeployClient) ListReleases(ctx context.Context, projectID, location, pipelineID string) ([]*deploypb.Release, error) {
if m.ListReleasesFunc != nil {
return m.ListReleasesFunc(ctx, projectID, location, pipelineID)
}
return nil, nil
}

func (m *MockCloudDeployClient) ListRollouts(ctx context.Context, projectID, location, pipelineID, releaseID string) ([]*deploypb.Rollout, error) {
if m.ListRolloutsFunc != nil {
return m.ListRolloutsFunc(ctx, projectID, location, pipelineID, releaseID)
}
return nil, nil
}

func (m *MockCloudDeployClient) CreateRelease(ctx context.Context, projectID, location, pipelineID, releaseID string) (*deploy.CreateReleaseOperation, error) {
if m.CreateReleaseFunc != nil {
return m.CreateReleaseFunc(ctx, projectID, location, pipelineID, releaseID)
}
return nil, nil
}
Loading