diff --git a/cloudbuild/trigger.go b/cloudbuild/trigger.go new file mode 100644 index 0000000000000000000000000000000000000000..8b35e04ad8f8fe594cb7c26b9e6d4212112b6444 --- /dev/null +++ b/cloudbuild/trigger.go @@ -0,0 +1,305 @@ +package cloudbuild + +import ( + "context" + "fmt" + "strings" + + // Using v1 for triggers with special handling for GitLab + cloudbuildv1 "google.golang.org/api/cloudbuild/v1" + cloudbuildv2 "google.golang.org/api/cloudbuild/v2" + "google.golang.org/api/googleapi" +) + +// CreateTrigger creates a new build trigger in Cloud Build +// It creates a build trigger that will run when code is pushed to the specified branch +func (c *Client) CreateTrigger(projectID, location, name, repoName, branchPattern, buildConfigPath string, repoType, repoURI string) (*cloudbuildv1.BuildTrigger, error) { + // We'll set up the basic fields for all trigger types, but the source configuration + // (TriggerTemplate or GitFileSource) will be determined by repository type + + // Common fields for all trigger types + triggerDescription := fmt.Sprintf("Trigger for %s on branch %s", repoName, branchPattern) + + // Use the provided repository URI if available + repositoryURI := repoURI + + // Check if we can list any connections (but we don't filter by type anymore) + _, err := c.ListConnections(projectID, location) + if err != nil { + fmt.Printf("Warning: Unable to list connections: %v\n", err) + } + + // Try to get repository information from Cloud Build API + repos, err := c.ListRepositoriesFromTriggerService(projectID, location) + if err == nil && len(repos) > 0 { + for _, repo := range repos { + var simpleName string + // Extract simple name from repo for comparison + if repo.RemoteUri != "" { + // Save the URI for later use in GitLab config + repositoryURI = repo.RemoteUri + + uriParts := strings.Split(repo.RemoteUri, "/") + if len(uriParts) > 0 { + simpleName = strings.TrimSuffix(uriParts[len(uriParts)-1], ".git") + } + } else if repo.Name != "" { + parts := strings.Split(repo.Name, "/") + simpleName = parts[len(parts)-1] + } + + // Check if this matches our target repository + if simpleName == repoName { + fmt.Printf("Found matching repository with URI: %s\n", repositoryURI) + break + } + } + } + + // Special case: for repository names starting with "github_" or containing an underscore + // we assume they're standard Cloud Source Repositories + if strings.HasPrefix(repoName, "github_") || strings.Contains(repoName, "_") { + fmt.Println("Detected Cloud Source Repository based on name pattern") + } + + // Create a trigger based on repository type + fmt.Printf("Setting up trigger for repository: %s\n", repoName) + + // IMPORTANT: We must create a clean trigger object with ONLY the relevant source configuration + // The CloudBuild API does not allow both TriggerTemplate and GitFileSource to be set + // Configure the trigger differently based on the repository type + var trigger *cloudbuildv1.BuildTrigger + + if strings.ToUpper(repoType) == "GITLAB" { + // For GitLab repositories, we need to use GitFileSource with the correct configuration + fmt.Println("Configuring trigger for GitLab repository with enhanced settings") + + // Make sure we have a valid repository URI + if repositoryURI == "" { + return nil, fmt.Errorf("repository URI is required for GitLab triggers but was empty") + } + + // Extract host URI from full repository URI if needed + uriParts := strings.Split(repositoryURI, "/") + var hostUri string + if len(uriParts) >= 3 { + // Format should be https://gitlab.domain.com + hostUri = uriParts[0] + "//" + uriParts[2] + fmt.Printf("Using GitLab host URI: %s\n", hostUri) + } + + // Create the proper GitLab trigger configuration + trigger = &cloudbuildv1.BuildTrigger{ + Name: name, + Description: triggerDescription, + // Important: For GitLab, DO NOT set Filename here as it conflicts with GitFileSource.Path + Disabled: false, + } + + // Set the GitFileSource configuration for GitLab + trigger.GitFileSource = &cloudbuildv1.GitFileSource{ + Path: buildConfigPath, + Uri: repositoryURI, + Revision: branchPattern, + RepoType: "GITLAB", // Ensure this is uppercase + } + + // Check if we need to add other fields for GitLab configuration + // Add debug information + fmt.Printf("GitLab Trigger Details:\n") + fmt.Printf(" Repository URI: %s\n", repositoryURI) + fmt.Printf(" Branch/Revision: %s\n", branchPattern) + fmt.Printf(" Config Path: %s\n", buildConfigPath) + } else { + // For GitHub and Cloud Source Repositories, use TriggerTemplate + fmt.Println("Configuring trigger for GitHub/CSR repository") + trigger = &cloudbuildv1.BuildTrigger{ + Name: name, + Description: triggerDescription, + Filename: buildConfigPath, + Disabled: false, + TriggerTemplate: &cloudbuildv1.RepoSource{ + ProjectId: projectID, + RepoName: repoName, + BranchName: branchPattern, + }, + } + } + + // Log the configuration + fmt.Printf("Repository: %s, Branch: %s, Type: %s\n", repoName, branchPattern, repoType) + + // Log the final trigger configuration for debugging + fmt.Println("\nFinal trigger configuration:") + if trigger.TriggerTemplate != nil { + fmt.Printf("Using TriggerTemplate with repository: %s, branch: %s\n", + trigger.TriggerTemplate.RepoName, trigger.TriggerTemplate.BranchName) + } else if trigger.GitFileSource != nil { + fmt.Printf("Using GitFileSource with URI: %s, revision: %s, repo type: %s\n", + trigger.GitFileSource.Uri, trigger.GitFileSource.Revision, trigger.GitFileSource.RepoType) + } + + // Create a v1 service for trigger operations + ctx := context.Background() + // Use the same credentials that were used for the v2 service + v1Service, err := cloudbuildv1.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("error creating Cloud Build v1 service: %w", err) + } + + // Add verbose diagnostics about what we're about to send to the API + fmt.Println("\n=== TRIGGER CREATION DIAGNOSTICS ===") + fmt.Printf("ProjectID: %s\n", projectID) + fmt.Printf("Location: %s (note: v1 API ignores this)\n", location) + fmt.Printf("Trigger Name: %s\n", trigger.Name) + fmt.Printf("Description: %s\n", trigger.Description) + fmt.Printf("Build Config Path: %s\n", buildConfigPath) + if trigger.TriggerTemplate != nil { + fmt.Println("Using TriggerTemplate configuration:") + fmt.Printf(" - Project ID: %s\n", trigger.TriggerTemplate.ProjectId) + fmt.Printf(" - Repository: %s\n", trigger.TriggerTemplate.RepoName) + fmt.Printf(" - Branch: %s\n", trigger.TriggerTemplate.BranchName) + if trigger.TriggerTemplate.Dir != "" { + fmt.Printf(" - Directory: %s\n", trigger.TriggerTemplate.Dir) + } + } else if trigger.GitFileSource != nil { + fmt.Println("Using GitFileSource configuration:") + fmt.Printf(" - Repository URI: %s\n", trigger.GitFileSource.Uri) + fmt.Printf(" - Repository Type: %s\n", trigger.GitFileSource.RepoType) + fmt.Printf(" - Revision: %s\n", trigger.GitFileSource.Revision) + fmt.Printf(" - Build Config Path: %s\n", trigger.GitFileSource.Path) + } + + // Make the API call to create the trigger + // Note: In v1 API, triggers are created at the project level, not location + fmt.Println("\nAttempting API call to create trigger...") + resp, err := v1Service.Projects.Triggers.Create(projectID, trigger).Do() + if err != nil { + fmt.Println("\n=== ERROR DETAILS ===") + fmt.Printf("Error type: %T\n", err) + fmt.Printf("Full error: %v\n", err) + + // Check if it's a Google API error, which gives us more details + if gerr, ok := err.(*googleapi.Error); ok { + fmt.Printf("HTTP Status: %d\n", gerr.Code) + fmt.Printf("Error Message: %s\n", gerr.Message) + + if len(gerr.Errors) > 0 { + fmt.Println("Error details:") + for i, e := range gerr.Errors { + fmt.Printf(" %d. Reason: %s, Message: %s\n", i+1, e.Reason, e.Message) + } + } + } + + // Suggest a solution based on the error + fmt.Println("\n=== SUGGESTED RESOLUTION ===") + fmt.Println("1. Verify the repository exists in your Google Cloud project") + fmt.Println("2. Check that the service account has permission to create triggers") + fmt.Println("3. Try creating a trigger manually in the console to verify project configuration") + fmt.Println("4. For GitLab repositories, ensure the Secret Manager permissions are correct") + + return nil, fmt.Errorf("error creating trigger: %w", err) + } + + return resp, nil +} + +// ListTriggers lists all triggers in the specified project +// Note: In v1 API, triggers are at the project level, not location +func (c *Client) ListTriggers(projectID, location string) ([]*cloudbuildv1.BuildTrigger, error) { + // Create a v1 service for trigger operations + ctx := context.Background() + // Use the same credentials that were used for the v2 service + v1Service, err := cloudbuildv1.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("error creating Cloud Build v1 service: %w", err) + } + + // Make the API call to list triggers + // Note: location parameter is ignored since v1 API doesn't support it + resp, err := v1Service.Projects.Triggers.List(projectID).Do() + if err != nil { + return nil, fmt.Errorf("error listing triggers: %w", err) + } + + return resp.Triggers, nil +} + +// ListRepositoriesFromTriggerService lists repositories that can be used for triggers +func (c *Client) ListRepositoriesFromTriggerService(projectID, location string) ([]*cloudbuildv2.Repository, error) { + // Format is just for log messages as we'll fetch repositories through connections + _ = fmt.Sprintf("projects/%s/locations/%s", projectID, location) + + // First, get all connections to list repositories from each + connections, err := c.ListConnections(projectID, location) + if err != nil { + return nil, fmt.Errorf("error listing connections: %w", err) + } + + // Collect repositories from all connections + var allRepositories []*cloudbuildv2.Repository + + // For each connection, list its repositories + for _, conn := range connections { + // Extract the connection name from the full resource name + parts := strings.Split(conn.Name, "/") + if len(parts) < 6 { + continue // Invalid format, skip + } + connName := parts[len(parts)-1] + + // Get repositories for this connection + repos, err := c.ListLinkableRepositories(projectID, location, connName) + if err != nil { + // Log the error but continue with other connections + fmt.Printf("Warning: Error listing repositories for connection %s: %v\n", connName, err) + continue + } + + // Add to our collection + allRepositories = append(allRepositories, repos...) + } + + return allRepositories, nil +} + +// GetTrigger retrieves an existing trigger from Cloud Build +func (c *Client) GetTrigger(projectID, location, name string) (*cloudbuildv1.BuildTrigger, error) { + // Create a v1 service for trigger operations + ctx := context.Background() + // Use the same credentials that were used for the v2 service + v1Service, err := cloudbuildv1.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("error creating Cloud Build v1 service: %w", err) + } + + // Make the API call to get the trigger + // Note: location parameter is ignored since v1 API doesn't support it + resp, err := v1Service.Projects.Triggers.Get(projectID, name).Do() + if err != nil { + return nil, fmt.Errorf("error getting trigger: %w", err) + } + + return resp, nil +} + +// DeleteTrigger deletes an existing trigger from Cloud Build +func (c *Client) DeleteTrigger(projectID, location, name string) error { + // Create a v1 service for trigger operations + ctx := context.Background() + // Use the same credentials that were used for the v2 service + v1Service, err := cloudbuildv1.NewService(ctx) + if err != nil { + return fmt.Errorf("error creating Cloud Build v1 service: %w", err) + } + + // Make the API call to delete the trigger + // Note: location parameter is ignored since v1 API doesn't support it + _, err = v1Service.Projects.Triggers.Delete(projectID, name).Do() + if err != nil { + return fmt.Errorf("error deleting trigger: %w", err) + } + + return nil +} diff --git a/config/config.go b/config/config.go index 725e5a126359ffc1cc4cae6f29df2b8304649938..7f6e1a38a08b50046d5be934fffbf62646e13058 100644 --- a/config/config.go +++ b/config/config.go @@ -30,6 +30,19 @@ type GitlabConnectionConfig struct { AuthTokenVersion string `json:"auth_token_version"` } +// TriggerConfig holds settings for creating a new build trigger +type TriggerConfig struct { + Name string `json:"name"` // Name of the trigger + ProjectID string `json:"project_id"` // Google Cloud project ID + Location string `json:"location"` // Region where the trigger will be created + RepositoryName string `json:"repository_name"` // Repository name to use for the trigger + BranchPattern string `json:"branch_pattern"` // Branch pattern that will trigger the build + ConfigPath string `json:"config_path"` // Path to the Cloud Build config file (YAML or JSON) + Description string `json:"description,omitempty"` // Optional description for the trigger + RepositoryType string `json:"repository_type,omitempty"` + RepositoryURI string `json:"repository_uri,omitempty"` +} + // LoadConfig loads the configuration from a JSON file func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) @@ -67,3 +80,49 @@ func SaveConfig(config *Config, path string) error { return nil } + +// LoadTriggerConfig loads a trigger configuration from a JSON file +func LoadTriggerConfig(path string) (*TriggerConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read trigger config file: %w", err) + } + + var config TriggerConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse trigger config file: %w", err) + } + + // Set defaults if not specified + if config.Location == "" { + config.Location = "asia-east1" + } + + if config.BranchPattern == "" { + config.BranchPattern = "master" + } + + if config.ConfigPath == "" { + config.ConfigPath = "/cloudbuild.yaml" + } + + if config.Name == "" { + config.Name = "test-trigger" + } + + return &config, nil +} + +// SaveTriggerConfig saves a trigger configuration to a JSON file +func SaveTriggerConfig(config *TriggerConfig, path string) error { + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal trigger config: %w", err) + } + + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("failed to write trigger config file: %w", err) + } + + return nil +} diff --git a/config/trigger.json b/config/trigger.json new file mode 100644 index 0000000000000000000000000000000000000000..40185c3eda36a67eb9c42018d28f18a123caabf4 --- /dev/null +++ b/config/trigger.json @@ -0,0 +1,11 @@ +{ + "name": "cloud-run-test-trigger", + "project_id": "vison-code", + "location": "asia-east1", + "repository_name": "cloud-run-test", + "branch_pattern": "master", + "config_path": "/cloudbuild.yaml", + "description": "Trigger for GitLab cloud-run-test repository on master branch", + "repository_type": "GITLAB", + "repository_uri": "https://gitlab.niveussolutions.com/shreyas.prabhu/cloud-run-test.git" +} diff --git a/main b/main index 1e231dbe52fffb9e34df919df0c9fcc664b50ff5..ce9d1ec25e057bc3027e09ad39fba4b1742b587f 100755 Binary files a/main and b/main differ diff --git a/main.go b/main.go index ad48610fd0ccb863cfc773db8499ac025ebab503..22022acea11ef602462aa6d59f0cc98bc127221d 100644 --- a/main.go +++ b/main.go @@ -7,9 +7,11 @@ import ( "log" "os" "strings" + "time" "github.com/niveus/cloud-build-go/cloudbuild" "github.com/niveus/cloud-build-go/config" + cloudbuildv1 "google.golang.org/api/cloudbuild/v1" cloudbuildv2 "google.golang.org/api/cloudbuild/v2" ) @@ -20,37 +22,74 @@ func printRepositoryInfo(repo *cloudbuildv2.Repository) { fmt.Printf(" Remote URI: %s\n", repo.RemoteUri) fmt.Printf(" Create Time: %s\n", repo.CreateTime) fmt.Printf(" Update Time: %s\n", repo.UpdateTime) - + fmt.Println(" Annotations:") for k, v := range repo.Annotations { fmt.Printf(" %s: %s\n", k, v) } } +// formatTimestamp generates a human-readable timestamp from trigger ID or current time if ID is empty +func formatTimestamp(id string) string { + // In Cloud Build v1 API, we don't have direct access to update time + // But we can extract the timestamp from the ID if available or use current time + if id == "" { + return time.Now().Format(time.RFC3339) + } + + // Just return the ID since we can't reliably extract a timestamp + return id +} + func main() { // Define command-line flags configPath := flag.String("config", "config/config.json", "Path to config file") - action := flag.String("action", "list-connections", "Action to perform: list-connections, create-gitlab, delete-connection, list-linkable-repos, inspect") + action := flag.String("action", "list-connections", "Action to perform: list-connections, create-gitlab, delete-connection, list-linkable-repos, inspect, list-repositories, list-repositories-for-triggers, create-trigger, list-triggers") connectionName := flag.String("name", "", "Name for the connection") + triggerName := flag.String("trigger-name", "test-trigger", "Name for the trigger") + repositoryName := flag.String("repo", "", "Repository name to use for the trigger") + branchPattern := flag.String("branch", "master", "Branch pattern for the trigger") + buildConfigPath := flag.String("config-path", "/cloudbuild.yaml", "Path to the Cloud Build configuration file") + triggerConfigPath := flag.String("trigger-config", "", "Path to trigger configuration JSON file") flag.Parse() // Project ID, location, connection type will come from config file var projectIDValue, locationValue, connTypeValue string - + // Load config file cfg, err := config.LoadConfig(*configPath) if err != nil { - log.Fatalf("Failed to load config file: %v", err) + fmt.Printf("Warning: Unable to load config from %s: %v\n", *configPath, err) + // Create a minimal config with just connection type + cfg = &config.Config{ + DefaultConnType: "GITHUB", + } } - // Set values from config file + + // Also try to load trigger config if specified for repository type detection + var triggerCfg *config.TriggerConfig + if *action == "create-trigger" && *triggerConfigPath != "" { + triggerCfg, err = config.LoadTriggerConfig(*triggerConfigPath) + if err == nil && triggerCfg != nil { + fmt.Printf("Loaded trigger config from %s, repository type: %s\n", + *triggerConfigPath, triggerCfg.RepositoryType) + if triggerCfg.RepositoryType != "" { + // Override default connection type with repository type from trigger config + cfg.DefaultConnType = triggerCfg.RepositoryType + fmt.Printf("Using repository type from trigger config: %s\n", triggerCfg.RepositoryType) + } + } + } + + // Set up project ID, location and connection type from config or flags projectIDValue = cfg.ProjectID locationValue = cfg.Location connTypeValue = cfg.DefaultConnType - + // Log the values that will be used - log.Printf("Config values set to: Project=%s, Location=%s, Type=%s", + log.Printf("Config values set to: Project=%s, Location=%s, Type=%s", projectIDValue, locationValue, connTypeValue) - + // Check for required values after loading from config if projectIDValue == "" { log.Fatal("project ID is required in the config file") @@ -64,7 +103,7 @@ func main() { // Create a Cloud Build client using our package var client *cloudbuild.Client - + // Authentication approach Application Default Credentials client, err = cloudbuild.NewClient(ctx) if err != nil { @@ -83,7 +122,7 @@ func main() { break } } - + if connConfig == nil { if *connectionName == "" { log.Fatalf("No connection configuration found in config file. Please add connections to your config file.") @@ -91,42 +130,42 @@ func main() { log.Fatalf("No connection configuration found for name '%s' in config file", *connectionName) } } - + // Check if the connection config has GitLab configuration if connConfig.GitlabConfig == nil { log.Fatalf("Connection '%s' does not have GitLab configuration", connConfig.Name) } - - log.Printf("Creating GitLab connection using config: ProjectID=%s, Location=%s, Name=%s", + + log.Printf("Creating GitLab connection using config: ProjectID=%s, Location=%s, Name=%s", projectIDValue, locationValue, connConfig.Name) - - fmt.Printf("Creating GitLab connection '%s' in project '%s' (location: '%s')\n", + + fmt.Printf("Creating GitLab connection '%s' in project '%s' (location: '%s')\n", connConfig.Name, projectIDValue, locationValue) - + // Create the GitLab connection using the Cloud Build SDK err, resourceName := client.CreateGitLabConnection( - projectIDValue, - locationValue, - connConfig.Name, + projectIDValue, + locationValue, + connConfig.Name, connConfig.GitlabConfig, connConfig.Annotations, ) if err != nil { log.Fatalf("Failed to create GitLab connection: %v", err) } - + fmt.Printf("Connection creation initiated successfully. GitLab connection resource: %s\n", resourceName) fmt.Println("Note: The connection might take a moment to become fully active in the Cloud Build system.") case "list-connections": // Use project ID and location from config log.Printf("Listing connections in project %s (location: %s)", projectIDValue, locationValue) - + connections, err := client.ListConnections(projectIDValue, locationValue) if err != nil { log.Fatalf("Failed to list connections: %v", err) } - fmt.Printf("Found %d connections in project %s (location: %s):\n", + fmt.Printf("Found %d connections in project %s (location: %s):\n", len(connections), projectIDValue, locationValue) for i, conn := range connections { // Determine connection type based on config @@ -139,8 +178,6 @@ func main() { fmt.Printf("%d. %s (Type: %s)\n", i+1, conn.Name, connType) } - - case "list-linkable-repos": // If connection name not provided via command line, get it from config connNameToList := *connectionName @@ -160,15 +197,15 @@ func main() { log.Fatalf("No connections found in config file") } } - - log.Printf("Listing linkable repositories for connection %s in project %s (location: %s)", + + log.Printf("Listing linkable repositories for connection %s in project %s (location: %s)", connNameToList, projectIDValue, locationValue) - + repositories, err := client.ListLinkableRepositories(projectIDValue, locationValue, connNameToList) if err != nil { log.Fatalf("Failed to list linkable repositories: %v", err) } - fmt.Printf("Found %d linkable repositories for connection %s:\n", + fmt.Printf("Found %d linkable repositories for connection %s:\n", len(repositories), connNameToList) for i, repo := range repositories { fmt.Printf("%d. %s\n", i+1, repo.Name) @@ -180,7 +217,7 @@ func main() { fmt.Println() } } - + case "link-repository": // If connection name not provided via command line, get it from config connNameToLink := *connectionName @@ -200,23 +237,23 @@ func main() { log.Fatalf("No connections found in config file") } } - + // Get the linkable repositories for this connection - log.Printf("Finding linkable repositories for connection %s in project %s (location: %s)", + log.Printf("Finding linkable repositories for connection %s in project %s (location: %s)", connNameToLink, projectIDValue, locationValue) - + linkableRepos, err := client.ListLinkableRepositories(projectIDValue, locationValue, connNameToLink) if err != nil { log.Fatalf("Failed to list linkable repositories: %v", err) } - + if len(linkableRepos) == 0 { log.Fatalf("No linkable repositories found for connection %s", connNameToLink) } - + // Use the first repository in the list repoToLink := linkableRepos[0] - + // Extract and use a valid repository name // First check if the repository has a RemoteUri field which usually contains the repo info var repoName string @@ -229,12 +266,12 @@ func main() { repoName = strings.TrimSuffix(repoName, ".git") } } - + // If we still don't have a name, use attributes (which is a more reliable way) if repoName == "" { // For GitLab repositories, the name is in the attributes printRepositoryInfo(repoToLink) // Print debug info - + // Try common attribute keys that might contain the name if repoToLink.Annotations != nil { if name, ok := repoToLink.Annotations["name"]; ok && name != "" { @@ -244,21 +281,21 @@ func main() { } } } - + // If we still don't have a name, ask the user to provide one manually if repoName == "" { fmt.Println("Unable to automatically determine repository name. Please provide one manually:") fmt.Print("Repository name: ") fmt.Scanln(&repoName) } - + // Make sure the repository name is valid if repoName == "" { log.Fatalf("Cannot link repository: No valid repository name provided") } - + fmt.Printf("Linking repository: %s\n", repoName) - + // We need to get the remote URI for the repository var remoteURI string if repoToLink.RemoteUri != "" { @@ -275,13 +312,13 @@ func main() { break } } - + // If we still don't have a remote URI, prompt the user if remoteURI == "" { fmt.Println("Please enter the remote URI for the repository (e.g., https://gitlab.com/namespace/repo.git):") fmt.Print("Remote URI: ") fmt.Scanln(&remoteURI) - + if remoteURI == "" { log.Fatalf("Remote URI is required to link a repository") } @@ -289,13 +326,13 @@ func main() { fmt.Printf("Constructed remote URI: %s\n", remoteURI) } } - + // Link the repository with the remote URI err = client.LinkRepository(projectIDValue, locationValue, connNameToLink, repoName, remoteURI) if err != nil { log.Fatalf("Failed to link repository: %v", err) } - + fmt.Printf("Successfully linked repository %s to connection %s\n", repoName, connNameToLink) case "delete-connection": @@ -317,16 +354,16 @@ func main() { log.Fatalf("No connections found in config file") } } - - log.Printf("Deleting connection %s in project %s (location: %s)", + + log.Printf("Deleting connection %s in project %s (location: %s)", connNameToDelete, projectIDValue, locationValue) - + err := client.DeleteConnection(projectIDValue, locationValue, connNameToDelete) if err != nil { log.Fatalf("Failed to delete connection: %v", err) } fmt.Printf("Successfully deleted connection: %s\n", connNameToDelete) - + case "inspect": // This action provides detailed inspection of a connection's configuration and status // If connection name not provided via command line, get it from config @@ -347,18 +384,308 @@ func main() { log.Fatalf("No connections found in config file") } } - - log.Printf("Inspecting connection %s in project %s (location: %s)", + + log.Printf("Inspecting connection %s in project %s (location: %s)", connNameToInspect, projectIDValue, locationValue) - - fmt.Printf("Inspecting connection %s in project %s (location: %s)...\n\n", + + fmt.Printf("Inspecting connection %s in project %s (location: %s)...\n\n", connNameToInspect, projectIDValue, locationValue) err := client.InspectConnection(projectIDValue, locationValue, connNameToInspect) if err != nil { log.Fatalf("Failed to inspect connection: %v", err) } - + + case "list-repositories": + // List all repositories available from the trigger service + log.Printf("Listing repositories in project %s (location: %s)", projectIDValue, locationValue) + + repositories, err := client.ListRepositoriesFromTriggerService(projectIDValue, locationValue) + if err != nil { + log.Fatalf("Failed to list repositories: %v", err) + } + + fmt.Printf("Found %d repositories:\n", len(repositories)) + for i, repo := range repositories { + fmt.Printf("\n%d. Repository Details:\n", i+1) + printRepositoryInfo(repo) + } + + if len(repositories) == 0 { + fmt.Println("\nNo repositories found.") + fmt.Println("Hint: You may need to link a repository first using the 'list-linkable-repos' action.") + } + + case "create-trigger": + // Create variables for trigger details with default values + triggerNameValue := *triggerName + repoNameForTrigger := *repositoryName + branchPatternValue := *branchPattern + buildConfigPathValue := *buildConfigPath + repoTypeValue := connTypeValue // Default to global connection type + + if *triggerConfigPath != "" { + // Load trigger configuration from JSON file + var err error + if triggerCfg == nil { // Only load if not already loaded during startup + triggerCfg, err = config.LoadTriggerConfig(*triggerConfigPath) + if err != nil { + log.Fatalf("Failed to load trigger configuration: %v", err) + } + fmt.Printf("Loaded trigger config with repository type: %s\n", triggerCfg.RepositoryType) + } + + // Use values from config file + if triggerCfg.ProjectID != "" { + projectIDValue = triggerCfg.ProjectID + } + if triggerCfg.Location != "" { + locationValue = triggerCfg.Location + } + if triggerCfg.RepositoryType != "" { + repoTypeValue = triggerCfg.RepositoryType + fmt.Printf("Using repository type from trigger config: %s\n", repoTypeValue) + } + + // Override command-line arguments with values from config + if triggerCfg.Name != "" { + triggerNameValue = triggerCfg.Name + } + if triggerCfg.RepositoryName != "" { + repoNameForTrigger = triggerCfg.RepositoryName + } + if triggerCfg.BranchPattern != "" { + branchPatternValue = triggerCfg.BranchPattern + } + if triggerCfg.ConfigPath != "" { + buildConfigPathValue = triggerCfg.ConfigPath + } + + fmt.Println("Using trigger configuration from file:", *triggerConfigPath) + } + + // Debug output to help track where the repository name is coming from + fmt.Printf("Repository name from config: %s\n", repoNameForTrigger) + + // If repository name is still missing, prompt for selection + if repoNameForTrigger == "" { + fmt.Println("No repository specified. Listing available repositories:") + repositories, err := client.ListRepositoriesFromTriggerService(projectIDValue, locationValue) + if err != nil { + log.Fatalf("Failed to list repositories: %v", err) + } + + fmt.Printf("Found %d repositories:\n", len(repositories)) + // Create a map to store repository display names to actual repository identifiers + repoDisplayMap := make(map[int]string) + + for i, repo := range repositories { + // Extract proper repository identifier + var repoNameDisplay string + if repo.RemoteUri != "" { + // For repositories with a Remote URI, use that (typically GitLab/GitHub repos) + // Extract the actual repository name from the URI + uriParts := strings.Split(repo.RemoteUri, "/") + if len(uriParts) > 0 { + // Get the last segment, removing .git suffix if present + repoBase := uriParts[len(uriParts)-1] + repoNameDisplay = strings.TrimSuffix(repoBase, ".git") + // Store the actual repository name without .git suffix in our map + repoDisplayMap[i+1] = repoNameDisplay + } else { + // Fallback to full URI if we can't parse it + repoNameDisplay = repo.RemoteUri + repoDisplayMap[i+1] = repo.RemoteUri + } + } else if repo.Name != "" { + // If we have a Name but no RemoteUri, use the last part of the Name + parts := strings.Split(repo.Name, "/") + repoNameDisplay = parts[len(parts)-1] + repoDisplayMap[i+1] = repoNameDisplay + } else { + // If we don't have RemoteUri or a valid Name, use "Unknown Repository" + repoNameDisplay = fmt.Sprintf("Unknown Repository %d", i+1) + repoDisplayMap[i+1] = fmt.Sprintf("repo-%d", i+1) // Fallback name + } + fmt.Printf("%d. %s\n", i+1, repoNameDisplay) + } + + if len(repositories) == 0 { + fmt.Println("No repositories found. Please link a repository first.") + os.Exit(1) + } + + // Prompt for repository selection + var repoIndex int + fmt.Print("Enter the number of the repository to use: ") + _, err = fmt.Scanf("%d", &repoIndex) + if err != nil || repoIndex < 1 || repoIndex > len(repositories) { + log.Fatalf("Invalid repository selection") + } + + // Use the repository name from our map + repoNameForTrigger = repoDisplayMap[repoIndex] + + // If we somehow still have an empty repository name, use a fallback + if repoNameForTrigger == "" { + // As a fallback, extract from the repo name + if repositories[repoIndex-1].RemoteUri != "" { + // Use the remote URI + uriParts := strings.Split(repositories[repoIndex-1].RemoteUri, "/") + if len(uriParts) > 0 { + repoNameForTrigger = strings.TrimSuffix(uriParts[len(uriParts)-1], ".git") + } + } else { + // Last resort: use a generic name with timestamp + repoNameForTrigger = fmt.Sprintf("repository-%d", time.Now().Unix()) + } + } + } + + log.Printf("Creating trigger %s for repository %s in project %s (location: %s)", + triggerNameValue, repoNameForTrigger, projectIDValue, locationValue) + + fmt.Printf("Creating Cloud Build trigger with the following configuration:\n") + fmt.Printf(" Trigger Name: %s\n", triggerNameValue) + fmt.Printf(" Region: %s\n", locationValue) + fmt.Printf(" Repository: %s\n", repoNameForTrigger) + fmt.Printf(" Branch Pattern: %s\n", branchPatternValue) + fmt.Printf(" Build Config File: %s\n\n", buildConfigPathValue) + + // Extract repository URI from config file if available + repoURIValue := "" + + // If we loaded from a config file, use URI from there + if triggerCfg != nil && triggerCfg.RepositoryURI != "" { + repoURIValue = triggerCfg.RepositoryURI + } + + // We're already using the existing client, no need to create a new one + // client was created at the beginning of main() + + // Create the trigger with all information + trigger, err := client.CreateTrigger( + projectIDValue, + locationValue, + triggerNameValue, + repoNameForTrigger, + branchPatternValue, + buildConfigPathValue, + repoTypeValue, + repoURIValue, + ) + + if err != nil { + log.Fatalf("Failed to create trigger: %v", err) + } + + // Print success message + fmt.Printf("Successfully created trigger: %s\n", trigger.Name) + fmt.Printf("Trigger description: %s\n", trigger.Description) + + case "list-triggers": + // List all triggers in the project and location + log.Printf("Listing triggers in project %s (location: %s)", projectIDValue, locationValue) + + triggers, err := client.ListTriggers(projectIDValue, locationValue) + if err != nil { + log.Fatalf("Failed to list triggers: %v", err) + } + + fmt.Printf("Found %d build triggers:\n", len(triggers)) + // Use a nil check to ensure we're correctly using the cloudbuildv1 type + _ = (*cloudbuildv1.BuildTrigger)(nil) + for i, trigger := range triggers { + fmt.Printf("\n%d. Trigger: %s\n", i+1, trigger.Name) + fmt.Printf(" Description: %s\n", trigger.Description) + fmt.Printf(" Created: %s\n", trigger.CreateTime) + fmt.Printf(" Last Modified: %s\n", formatTimestamp(trigger.Id)) + + // Print repository information if available + if trigger.TriggerTemplate != nil { + fmt.Printf(" Repository: %s\n", trigger.TriggerTemplate.RepoName) + + // Print branch pattern if available + if trigger.TriggerTemplate.BranchName != "" { + fmt.Printf(" Branch Pattern: %s\n", trigger.TriggerTemplate.BranchName) + } + } + + // Print build configuration file if available + if trigger.Filename != "" { + fmt.Printf(" Build Config File: %s\n", trigger.Filename) + } + } + + if len(triggers) == 0 { + fmt.Println("\nNo triggers found.") + } + + case "list-repositories-for-triggers": + // This case helps identify what repositories are available for trigger creation + fmt.Println("Listing all repositories available for Cloud Build triggers...") + + // Use the current client for this operation + repos, err := client.ListRepositoriesFromTriggerService(projectIDValue, locationValue) + if err != nil { + fmt.Printf("WARNING: Error listing repositories: %v\n", err) + fmt.Println("This could be due to permission issues with the Cloud Build service account.") + + // Try listing connections as a fallback + conns, connErr := client.ListConnections(projectIDValue, locationValue) + if connErr != nil { + log.Fatalf("Failed to list connections: %v", connErr) + } + + fmt.Printf("Found %d connection(s) in project %s (location: %s)\n", len(conns), projectIDValue, locationValue) + for i, conn := range conns { + fmt.Printf(" %d. Connection: %s\n", i+1, conn.Name) + if conn.GitlabConfig != nil { + fmt.Printf(" Type: GitLab\n") + fmt.Printf(" Host URI: %s\n", conn.GitlabConfig.HostUri) + } + } + + fmt.Println("\nTROUBLESHOOTING TIPS:") + fmt.Println("1. Ensure your Cloud Build service account has the Secret Manager Secret Accessor role") + fmt.Println("2. For 1st-gen repositories, use repository names in the format 'github_org_repo'") + fmt.Println("3. For GitLab repositories, verify that the connection is properly set up") + } else { + fmt.Printf("Found %d repository/repositories available for triggers:\n", len(repos)) + + if len(repos) == 0 { + fmt.Println("No repositories available for triggers.") + fmt.Println("You need to connect repositories to your Cloud Build project first.") + } + + for i, repo := range repos { + fmt.Printf("\nRepository %d:\n", i+1) + fmt.Printf(" Full Name: %s\n", repo.Name) + if repo.RemoteUri != "" { + fmt.Printf(" Remote URI: %s\n", repo.RemoteUri) + } + + // Extract the repo name for reference + var simpleName string + if repo.RemoteUri != "" { + uriParts := strings.Split(repo.RemoteUri, "/") + if len(uriParts) > 0 { + simpleName = strings.TrimSuffix(uriParts[len(uriParts)-1], ".git") + } + } else if repo.Name != "" { + // Try to get name from Name field + parts := strings.Split(repo.Name, "/") + simpleName = parts[len(parts)-1] + } + + if simpleName != "" { + fmt.Printf(" Simple Name: %s (use this in your trigger config)\n", simpleName) + } + } + + fmt.Println("\nTo create a trigger, use one of the repository names listed above in your trigger configuration.") + } + default: - log.Fatalf("Unknown action: %s. Supported actions are: list-connections, create-gitlab, delete-connection, list-linkable-repos, inspect", *action) + log.Fatalf("Unknown action: %s. Supported actions are: list-connections, create-gitlab, delete-connection, list-linkable-repos, inspect, list-repositories, list-repositories-for-triggers, create-trigger, list-triggers", *action) } }