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
104 changes: 101 additions & 3 deletions devops-mcp-server/cloudbuild/client/cloudbuildclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import (
"strings"

cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2"
cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"

logging "cloud.google.com/go/logging/apiv2"
build "google.golang.org/api/cloudbuild/v1"
"google.golang.org/api/iterator"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"

cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
loggingpb "cloud.google.com/go/logging/apiv2/loggingpb"
)

// contextKey is a private type to use as a key for context values.
Expand All @@ -51,6 +54,14 @@ type CloudBuildClient interface {
GetLatestBuildForTrigger(ctx context.Context, projectID, location, triggerID string) (*cloudbuildpb.Build, error)
ListBuildTriggers(ctx context.Context, projectID, location string) ([]*cloudbuildpb.BuildTrigger, error)
RunBuildTrigger(ctx context.Context, projectID, location, triggerID, branch, tag, commitSha string) (*cloudbuild.RunBuildTriggerOperation, error)
ListBuilds(ctx context.Context, projectID, location string) ([]*cloudbuildpb.Build, error)
GetBuildInfo(ctx context.Context, projectID, location, buildID string) (BuildInfo, error)
StartBuild(ctx context.Context, projectID, location string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error)
}

type BuildInfo struct {
BuildDetails *cloudbuildpb.Build
Logs string
}

// NewCloudBuildClient creates a new Cloud Build client.
Expand All @@ -65,13 +76,23 @@ func NewCloudBuildClient(ctx context.Context) (CloudBuildClient, error) {
return nil, fmt.Errorf("failed to create Cloud Build service: %v", err)
}

return &CloudBuildClientImpl{v1client: c, legacyClient: c2}, nil
loggingClient, err := logging.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create Logging client: %v", err)
}

return &CloudBuildClientImpl{
v1client: c,
legacyClient: c2,
loggingClient: loggingClient,
}, nil
}

// CloudBuildClientImpl is an implementation of the CloudBuildClient interface.
type CloudBuildClientImpl struct {
v1client *cloudbuild.Client
legacyClient *build.Service
loggingClient *logging.Client
}

// CreateCloudBuildTrigger creates a new build trigger.
Expand Down Expand Up @@ -181,3 +202,80 @@ func (c *CloudBuildClientImpl) RunBuildTrigger(ctx context.Context, projectID, l
}
return op, nil
}


func (c *CloudBuildClientImpl) ListBuilds(ctx context.Context, projectID, location string) ([]*cloudbuildpb.Build, error) {
req := &cloudbuildpb.ListBuildsRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
}
it := c.v1client.ListBuilds(ctx, req)
var builds []*cloudbuildpb.Build
for {
build, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list builds: %v", err)
}
builds = append(builds, build)
}
return builds, nil
}

func (c *CloudBuildClientImpl) GetBuildInfo(ctx context.Context, projectID, location, buildID string) (BuildInfo, error) {
req := &cloudbuildpb.GetBuildRequest{
Name: fmt.Sprintf("projects/%s/locations/%s/builds/%s", projectID, location, buildID),
}
build, err := c.v1client.GetBuild(ctx, req)
if err != nil {
return BuildInfo{}, fmt.Errorf("failed to get build info: %w", err)
}
info := BuildInfo{BuildDetails: build}
logReq := &loggingpb.ListLogEntriesRequest{
ResourceNames: []string{fmt.Sprintf("projects/%s", projectID)},
Filter: fmt.Sprintf(`resource.type="build" AND resource.labels.build_id="%s" AND logName="projects/%s/logs/cloudbuild"`, buildID, projectID),
}
it := c.loggingClient.ListLogEntries(ctx, logReq)
var logs []string
for {
entry, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return BuildInfo{}, fmt.Errorf("failed to list log entries: %w", err)
}
var logMessage string
switch payload := entry.Payload.(type) {
case *loggingpb.LogEntry_TextPayload:
logMessage = payload.TextPayload
case *loggingpb.LogEntry_JsonPayload:
jsonBytes, err := protojson.Marshal(payload.JsonPayload)
if err != nil {
logMessage = fmt.Sprintf("failed to marshal json payload to string: %v", err)
} else {
logMessage = string(jsonBytes)
}
case *loggingpb.LogEntry_ProtoPayload:
logMessage = fmt.Sprintf("%v", payload.ProtoPayload)
default:
return BuildInfo{}, fmt.Errorf("unknown log entry payload type")
}
logs = append(logs, logMessage)
}
info.Logs = strings.Join(logs, "\n")
return info, nil
}

func (c *CloudBuildClientImpl) StartBuild(ctx context.Context, projectID, location string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error) {
req := &cloudbuildpb.CreateBuildRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
Build: &cloudbuildpb.Build{Source: source},
}
ops, err := c.v1client.CreateBuild(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to start build: %v", err)
}
return ops, nil
}
64 changes: 64 additions & 0 deletions devops-mcp-server/cloudbuild/cloudbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
cloudbuildclient "devops-mcp-server/cloudbuild/client"
iamclient "devops-mcp-server/iam/client"
resourcemanagerclient "devops-mcp-server/resourcemanager/client"

cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
)

// Handler holds the clients for the cloudbuild service.
Expand All @@ -39,6 +41,9 @@ func (h *Handler) Register(server *mcp.Server) {
addCreateTriggerTool(server, h.CbClient, h.IClient, h.RClient)
addRunTriggerTool(server, h.CbClient)
addListTriggersTool(server, h.CbClient)
addListBuildsTool(server, h.CbClient)
addGetBuildInfoTool(server, h.CbClient)
addStartBuildTool(server, h.CbClient)
}

type RunTriggerArgs struct {
Expand Down Expand Up @@ -146,3 +151,62 @@ func IsValidServiceAccount(sa string) bool {
var saRegex = regexp.MustCompile(`^serviceAccount:[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com$`)
return saRegex.MatchString(sa)
}

type ListBuildsArgs struct {
ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."`
Location string `json:"location" jsonschema:"The Google Cloud location for the builds."`
}

type GetBuildInfoArgs struct {
ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."`
Location string `json:"location" jsonschema:"The Google Cloud location for the build."`
BuildID string `json:"build_id" jsonschema:"The ID of the build."`
}

type StartBuildArgs struct {
ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."`
Location string `json:"location" jsonschema:"The Google Cloud location for the build."`
Bucket string `json:"bucket" jsonschema:"The Cloud Storage bucket where the source is located."`
Object string `json:"object" jsonschema:"The Cloud Storage object (file) where the source is located."`
}

func addListBuildsTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) {
listBuildsToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args ListBuildsArgs) (*mcp.CallToolResult, any, error) {
res, err := cbClient.ListBuilds(ctx, args.ProjectID, args.Location)
if err != nil {
return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to list builds: %w", err)
}
return &mcp.CallToolResult{}, map[string]any{"builds": res}, nil
}
mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.list_builds", Description: "Lists all Cloud Builds in a given location and project."}, listBuildsToolFunc)
}

func addGetBuildInfoTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) {
getBuildInfoToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args GetBuildInfoArgs) (*mcp.CallToolResult, any, error) {
res, err := cbClient.GetBuildInfo(ctx, args.ProjectID, args.Location, args.BuildID)
if err != nil {
return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to get build info: %w", err)
}
return &mcp.CallToolResult{}, res, nil
}
mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.get_build_info", Description: "Gets information about a specific Cloud Build."}, getBuildInfoToolFunc)

Choose a reason for hiding this comment

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

medium

The tool name cloudbuild.get_build_info is inconsistent with the PR description, which specifies cloudbuild.build_info. Please align the tool name for consistency.

Suggested change
mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.get_build_info", Description: "Gets information about a specific Cloud Build."}, getBuildInfoToolFunc)
mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.build_info", Description: "Gets information about a specific Cloud Build."}, getBuildInfoToolFunc)

}

func addStartBuildTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) {
startBuildToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args StartBuildArgs) (*mcp.CallToolResult, any, error) {
source:= &cloudbuildpb.Source{
Source: &cloudbuildpb.Source_StorageSource{
StorageSource: &cloudbuildpb.StorageSource{
Bucket: args.Bucket,
Object: args.Object,
},
},
}
res, err := cbClient.StartBuild(ctx, args.ProjectID, args.Location, source)
if err != nil {
return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to start build: %w", err)
}
return &mcp.CallToolResult{}, res, nil
}
mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.start_build", Description: "Starts a new Cloud Build from a source in Google Cloud Storage."}, startBuildToolFunc)
}
1 change: 1 addition & 0 deletions devops-mcp-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
cloud.google.com/go/auth v0.17.0
cloud.google.com/go/cloudbuild v1.23.1
cloud.google.com/go/iam v1.5.3
cloud.google.com/go/logging v1.13.0
cloud.google.com/go/resourcemanager v1.10.7
cloud.google.com/go/run v1.12.1
cloud.google.com/go/storage v1.57.0
Expand Down
Loading