Skip to content
Draft
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
79 changes: 70 additions & 9 deletions cmd/connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,41 +461,87 @@ func PingConnection() *cli.Command {
printErrorForOutput(output, errors2.Wrap(err, "failed to select the environment"))
return cli.Exit("", 1)
}

if output != "json" {
infoPrinter.Printf("Testing connection '%s' in environment '%s'...\n", name, environment)
}

manager, errs := connection.NewManagerFromConfigWithContext(ctx, cm)
if len(errs) > 0 {
// Handle each error in the errs slice
// Check if the error is for the specific connection we're testing
for _, err := range errs {
printErrorForOutput(output, errors2.Wrap(err, "failed to create connection manager"))
errStr := err.Error()
if output == "json" {
printConnectionTestErrorJSON(name, errStr)
} else {
errorPrinter.Printf("\nConnection setup failed:\n%s\n", errStr)
}
}
return cli.Exit("", 1)
}

conn := manager.GetConnection(name)
if conn == nil {
printErrorForOutput(output, &config.MissingConnectionError{
missingErr := &config.MissingConnectionError{
Name: name,
ConfigFilePath: configFilePath,
EnvironmentName: cm.SelectedEnvironmentName,
})
}
if output == "json" {
printConnectionTestErrorJSON(name, missingErr.Error())
} else {
errorPrinter.Printf("\n%s\n", missingErr.Error())
fmt.Println()
infoPrinter.Println("Available connections in this environment:")
connTypes := cm.SelectedEnvironment.Connections.ConnectionsSummaryList()
if len(connTypes) == 0 {
fmt.Println(" (none)")
} else {
for connName, connType := range connTypes {
fmt.Printf(" - %s (%s)\n", connName, connType)
}
}
fmt.Println()
infoPrinter.Println("To add a new connection, run: bruin connections add")
}
return cli.Exit("", 1)
}

// Get connection type for better error messages
connType := manager.GetConnectionType(name)

if tester, ok := conn.(interface {
Ping(ctx context.Context) error
}); ok {
testErr := tester.Ping(ctx)
if testErr != nil {
printErrorForOutput(output, errors2.Wrap(testErr, fmt.Sprintf("failed to test connection '%s'", name)))
if output == "json" {
printConnectionTestErrorJSON(name, testErr.Error())
} else {
errorPrinter.Printf("\nConnection test failed for '%s' (%s):\n", name, connType)
fmt.Printf("\n%s\n", testErr.Error())
}
return cli.Exit("", 1)
}
} else {
infoPrinter.Printf("Connection '%s' does not support testing yet.\n", name)
if output == "json" {
jsonOutput := map[string]interface{}{
"status": "skipped",
"message": fmt.Sprintf("Connection '%s' does not support testing yet", name),
}
jsonBytes, _ := json.Marshal(jsonOutput)
fmt.Println(string(jsonBytes))
} else {
warningPrinter.Printf("Connection '%s' (%s) does not support testing yet.\n", name, connType)
}
return nil
}

if output == "json" {
jsonOutput := map[string]string{
"status": "success",
jsonOutput := map[string]interface{}{
"status": "success",
"connection": name,
"type": connType,
}
jsonBytes, err := json.Marshal(jsonOutput)
if err != nil {
Expand All @@ -504,10 +550,25 @@ func PingConnection() *cli.Command {
}
fmt.Println(string(jsonBytes))
} else {
infoPrinter.Printf("Successfully tested connection '%s' in environment: %s\n", name, environment)
successPrinter.Printf("\nConnection '%s' (%s) is working correctly!\n", name, connType)
}

return nil
},
}
}

// printConnectionTestErrorJSON outputs a connection test error in JSON format.
func printConnectionTestErrorJSON(name string, errMsg string) {
jsonOutput := map[string]string{
"status": "error",
"connection": name,
"error": errMsg,
}
jsonBytes, err := json.Marshal(jsonOutput)
if err != nil {
errorPrinter.Printf("failed to marshal error JSON: %v\n", err)
return
}
fmt.Println(string(jsonBytes))
}
1 change: 0 additions & 1 deletion internal/data/embed_darwin_amd64.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package data

import (
Expand Down
1 change: 0 additions & 1 deletion internal/data/embed_darwin_arm64.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package data

import (
Expand Down
1 change: 0 additions & 1 deletion internal/data/embed_linux_amd64.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package data

import (
Expand Down
1 change: 0 additions & 1 deletion internal/data/embed_linux_arm64.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package data

import (
Expand Down
1 change: 0 additions & 1 deletion internal/data/embed_windows_amd64.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package data

import (
Expand Down
111 changes: 109 additions & 2 deletions pkg/bigquery/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,117 @@ func (d *Client) Ping(ctx context.Context) error {
// Use the existing RunQueryWithoutResult method
err := d.RunQueryWithoutResult(ctx, &q)
if err != nil {
return errors.Wrap(err, "failed to run test query on Bigquery connection")
return wrapPingError(err, d.config.ProjectID)
}

return nil // Return nil if the query runs successfully
return nil
}

// BigQueryConnectionError provides detailed error information for BigQuery connection issues.
type BigQueryConnectionError struct {
ProjectID string
Issue string
Hint string
OriginalErr error
}

func (e *BigQueryConnectionError) Error() string {
var result string
result = e.Issue

if e.Hint != "" {
result += "\n\nHint: " + e.Hint
}

return result
}

func (e *BigQueryConnectionError) Unwrap() error {
return e.OriginalErr
}

// wrapPingError wraps ping errors with helpful hints based on common error patterns.
func wrapPingError(err error, projectID string) error {
errStr := err.Error()

// Check for permission denied errors
if strings.Contains(errStr, "Access Denied") || strings.Contains(errStr, "403") || strings.Contains(errStr, "permission") {
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: "Permission denied when connecting to BigQuery",
Hint: `Your service account may be missing required IAM roles. Required roles:
- BigQuery Data Viewer (roles/bigquery.dataViewer) - to read data
- BigQuery Job User (roles/bigquery.jobUser) - to run queries
- BigQuery Data Editor (roles/bigquery.dataEditor) - to write data (if needed)

To add roles in Google Cloud Console:
1. Go to IAM & Admin > IAM
2. Find your service account email
3. Click Edit (pencil icon) and add the required roles

Alternatively, grant 'BigQuery Admin' (roles/bigquery.admin) for full access.`,
OriginalErr: err,
}
}

// Check for project not found errors
if strings.Contains(errStr, "notFound") || strings.Contains(errStr, "404") || strings.Contains(errStr, "project") && strings.Contains(errStr, "not found") {
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: fmt.Sprintf("Project '%s' not found or not accessible", projectID),
Hint: `Check that:
1. The project_id in .bruin.yml matches your Google Cloud project ID exactly
2. The BigQuery API is enabled in your project (APIs & Services > Enable APIs)
3. Your service account has access to this project`,
OriginalErr: err,
}
}

// Check for invalid credentials
if strings.Contains(errStr, "invalid_grant") || strings.Contains(errStr, "Invalid JWT") || strings.Contains(errStr, "credentials") {
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: "Invalid or expired credentials",
Hint: `Your service account key may be invalid or expired. Try:
1. Generate a new service account key in Google Cloud Console
(IAM & Admin > Service Accounts > Keys > Add Key > Create new key > JSON)
2. Update the service_account_file path in .bruin.yml
3. Re-run 'bruin connections test' to verify`,
OriginalErr: err,
}
}

// Check for ADC not configured
if strings.Contains(errStr, "could not find default credentials") || strings.Contains(errStr, "ADC") {
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: "Application Default Credentials not found",
Hint: `Run the following command to set up Application Default Credentials:
gcloud auth application-default login

Or provide explicit credentials by adding service_account_file to your connection in .bruin.yml`,
OriginalErr: err,
}
}

// Check for billing not enabled
if strings.Contains(errStr, "billing") || strings.Contains(errStr, "Billing") {
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: "Billing may not be enabled for this project",
Hint: `BigQuery requires billing to be enabled. Go to:
Google Cloud Console > Billing > Link a billing account to your project`,
OriginalErr: err,
}
}

// Generic fallback with helpful context
return &BigQueryConnectionError{
ProjectID: projectID,
Issue: "Failed to connect to BigQuery: " + errStr,
Hint: "Run 'bruin connections test --name <connection-name>' to debug connection issues.",
OriginalErr: err,
}
}

func (d *Client) IsPartitioningOrClusteringMismatch(ctx context.Context, meta *bigquery.TableMetadata, asset *pipeline.Asset) bool {
Expand Down
37 changes: 37 additions & 0 deletions pkg/config/connection_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,40 @@ func (e *MissingConnectionError) Error() string {
environmentName,
)
}

// ConnectionSetupError provides detailed error information for connection setup issues.
type ConnectionSetupError struct {
ConnectionName string
ConnectionType string
Issue string
Hint string
OriginalErr error
}

func (e *ConnectionSetupError) Error() string {
var sb strings.Builder

if e.ConnectionType != "" {
sb.WriteString(e.ConnectionType)
sb.WriteString(" ")
}
sb.WriteString("connection")
if e.ConnectionName != "" {
sb.WriteString(" '")
sb.WriteString(e.ConnectionName)
sb.WriteString("'")
}
sb.WriteString(": ")
sb.WriteString(e.Issue)

if e.Hint != "" {
sb.WriteString("\n\nHint: ")
sb.WriteString(e.Hint)
}

return sb.String()
}

func (e *ConnectionSetupError) Unwrap() error {
return e.OriginalErr
}
Loading