diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.archive-collections.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.archive-collections.go new file mode 100644 index 0000000..88467fb --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.archive-collections.go @@ -0,0 +1,128 @@ +// See entire project at https://github.com/mongodb/atlas-architecture-go-sdk +package main + +import ( + "context" + "fmt" + "log" + "time" + + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + + "github.com/joho/godotenv" +) + +func main() { + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) + } + + secrets, cfg, err := config.LoadAllFromEnv() + if err != nil { + log.Fatalf("Failed to load configuration %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + client, err := auth.NewClient(ctx, cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize authentication client: %v", err) + } + + projectID := cfg.ProjectID + if projectID == "" { + log.Fatal("Failed to find Project ID in configuration") + } + + fmt.Printf("Starting archive analysis for project: %s\n", projectID) + + // Get all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + log.Fatalf("Failed to list clusters: %v", err) + } + + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) + + // Connect to each cluster and analyze collections for archiving + failedArchives := 0 + skippedCandidates := 0 + totalCandidates := 0 + + // Create archive options with custom settings + opts := archive.DefaultOptions() + opts.DefaultRetentionMultiplier = 2 + opts.MinimumRetentionDays = 30 + opts.EnableDataExpiration = true + opts.ArchiveSchedule = "DAILY" + + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) + + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filters them. + // NOTE: In a real implementation, you would analyze collections based on size, age, + // access patterns, and other factors to determine candidates for archiving. + stats := archive.ListCollectionsWithCounts(ctx, client, projectID, clusterName) + candidates := make([]archive.Candidate, 0) + const docThreshold = 100000 + for _, s := range stats { + // Skip internal databases + if s.DatabaseName == "admin" || s.DatabaseName == "local" || s.DatabaseName == "config" { + continue + } + // Demo criterion: collections with >= 100k documents + if s.EstimatedCount >= docThreshold { + candidates = append(candidates, archive.Candidate{ + DatabaseName: s.DatabaseName, + CollectionName: s.CollectionName, + DateField: "createdAt", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + }) + } + } + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + len(candidates), clusterName) + + // Configure online archive for each candidate collection + for _, candidate := range candidates { + // Pre-validate candidate before attempting configuration + if err := archive.ValidateCandidate(candidate, opts); err != nil { + fmt.Printf("- Skipping %s.%s: invalid candidate: %v\n", + candidate.DatabaseName, candidate.CollectionName, err) + skippedCandidates++ + continue + } + + fmt.Printf("- Configuring archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) + if configureErr != nil { + fmt.Printf(" Failed to configure archive: %v\n", configureErr) + failedArchives++ + continue + } + + fmt.Printf(" Successfully configured online archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } + if failedArchives > 0 { + fmt.Printf("WARNING: %d of %d archive configurations failed (excluding skipped)\n", failedArchives, totalCandidates-skippedCandidates) + } + + fmt.Println("Archive analysis and configuration completed.") +} + diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-logs.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-logs.go index 6b1d0fe..175046d 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-logs.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-logs.go @@ -8,7 +8,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" @@ -17,55 +16,58 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch logs with the provided parameters p := &admin.GetHostLogsApiParams{ GroupId: cfg.ProjectID, HostName: cfg.HostName, LogName: "mongodb", } + fmt.Printf("Request parameters: GroupID=%s, HostName=%s, LogName=%s\n", + cfg.ProjectID, cfg.HostName, p.LogName) rc, err := logs.FetchHostLogs(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch logs", err) + log.Fatalf("Failed to fetch logs: %v", err) } defer fileutils.SafeClose(rc) // Prepare output paths + // If the ATLAS_DOWNLOADS_DIR env variable is set, it will be used as the base directory for output files outDir := "logs" prefix := fmt.Sprintf("%s_%s", p.HostName, p.LogName) gzPath, err := fileutils.GenerateOutputPath(outDir, prefix, "gz") if err != nil { - errors.ExitWithError("Failed to generate GZ output path", err) + log.Fatalf("Failed to generate GZ output path: %v", err) } txtPath, err := fileutils.GenerateOutputPath(outDir, prefix, "txt") if err != nil { - errors.ExitWithError("Failed to generate TXT output path", err) + log.Fatalf("Failed to generate TXT output path: %v", err) } // Save compressed logs if err := fileutils.WriteToFile(rc, gzPath); err != nil { - errors.ExitWithError("Failed to save compressed logs", err) + log.Fatalf("Failed to save compressed logs: %v", err) } fmt.Println("Saved compressed log to", gzPath) // Decompress logs if err := fileutils.DecompressGzip(gzPath, txtPath); err != nil { - errors.ExitWithError("Failed to decompress logs", err) + log.Fatalf("Failed to decompress logs: %v", err) } fmt.Println("Uncompressed log to", txtPath) } diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-dev.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-dev.go index b76bf54..9a865b8 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-dev.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-dev.go @@ -9,7 +9,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" "github.com/joho/godotenv" @@ -17,22 +16,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch disk metrics with the provided parameters p := &admin.GetDiskMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -44,13 +43,13 @@ func main() { } view, err := metrics.FetchDiskMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch disk metrics", err) + log.Fatalf("Failed to fetch disk metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-prod.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-prod.go index c2dfbea..47994fb 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-prod.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-prod.go @@ -7,8 +7,6 @@ import ( "fmt" "log" - "atlas-sdk-go/internal/errors" - "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -19,22 +17,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch process metrics with the provided parameters p := &admin.GetHostMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -52,13 +50,13 @@ func main() { view, err := metrics.FetchProcessMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch process metrics", err) + log.Fatalf("Failed to fetch process metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.historical-billing.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.historical-billing.go index 8d43842..a7746c5 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.historical-billing.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.historical-billing.go @@ -11,7 +11,6 @@ import ( "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "github.com/joho/godotenv" @@ -19,25 +18,24 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } - fmt.Printf("Fetching historical invoices for organization: %s\n", p.OrgId) // Fetch invoices from the previous six months with the provided options @@ -46,7 +44,7 @@ func main() { billing.WithIncludeCount(true), billing.WithDateRange(time.Now().AddDate(0, -6, 0), time.Now())) if err != nil { - errors.ExitWithError("Failed to retrieve invoices", err) + log.Fatalf("Failed to retrieve invoices: %v", err) } if invoices.GetTotalCount() > 0 { @@ -60,25 +58,32 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("historical_%s", p.OrgId) - exportInvoicesToJSON(invoices, outDir, prefix) - exportInvoicesToCSV(invoices, outDir, prefix) + err = exportInvoicesToJSON(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } } -func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(invoices.GetResults(), jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported invoice data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -92,9 +97,10 @@ func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, pr } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported invoice data to %s\n", csvPath) + return nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.line-items.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.line-items.go index 3247f42..5ee5c43 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.line-items.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.line-items.go @@ -6,34 +6,33 @@ import ( "fmt" "log" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - - "github.com/joho/godotenv" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -42,7 +41,7 @@ func main() { details, err := billing.CollectLineItemBillingData(ctx, client.InvoicesApi, client.OrganizationsApi, p.OrgId, nil) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve pending invoices for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve pending invoices for %s: %v", p.OrgId, err) } if len(details) == 0 { @@ -55,26 +54,33 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("pending_%s", p.OrgId) - exportInvoicesToJSON(details, outDir, prefix) - exportInvoicesToCSV(details, outDir, prefix) + err = exportInvoicesToJSON(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } } -func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(details, jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported billing data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -96,8 +102,9 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported billing data to %s\n", csvPath) + return nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.linked-billing.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.linked-billing.go index 38253c6..d9292a1 100644 --- a/generated-usage-examples/go/atlas-sdk-go/main.snippet.linked-billing.go +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.linked-billing.go @@ -9,28 +9,28 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -39,7 +39,7 @@ func main() { invoices, err := billing.GetCrossOrgBilling(ctx, client.InvoicesApi, p) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve cross-organization billing data for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve cross-organization billing data for %s: %v", p.OrgId, err) } displayLinkedOrganizations(invoices, p.OrgId) diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example b/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example index a1f8da0..66f7781 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example @@ -1,2 +1,4 @@ -MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id -MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret +MONGODB_ATLAS_SERVICE_ACCOUNT_ID= +MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= +ATLAS_DOWNLOADS_DIR=tmp/atlas_downloads # optional directory for downloads +CONFIG_PATH=./configs/config..json # path to corresponding config file \ No newline at end of file diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore b/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore index 22cadb7..ac48429 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore @@ -1,6 +1,16 @@ -# Secrets +# Secrets (keep example) .env +!.env.example +.env.production -# Logs +# Configs (keep example) +configs/config.json +!configs/config.example.json + +# temporary files +tmp +temp + +# downloaded logs *.log *.gz diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/CHANGELOG.md b/generated-usage-examples/go/atlas-sdk-go/project-copy/CHANGELOG.md index 1675a9b..55ed428 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/CHANGELOG.md +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/CHANGELOG.md @@ -2,10 +2,14 @@ Notable changes to the project. +## v1.2 (2025-08-17) +### Added +- Example scripts for scaling and archiving clusters. + ## v1.1 (2025-06-17) ### Added - Example scripts for fetching cross-organization billing information. - + ## v1.0 (2025-05-29) ### Added - Initial project structure with example scripts for fetching logs and metrics. diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/README.md b/generated-usage-examples/go/atlas-sdk-go/project-copy/README.md index b17f5cb..96b9817 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/README.md +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/README.md @@ -16,8 +16,8 @@ Currently, the repository includes examples that demonstrate the following: - Download logs for a specific host - Pull and parse line-item-level billing data - Return all linked organizations from a specific billing organization -- Get historical invoices for an organization -- Programmatically manage Atlas resources +- Get historical invoices for an organization +- Programmatically archive Atlas cluster data As the Architecture Center documentation evolves, this repository will be updated with new examples and improvements to existing code. @@ -28,12 +28,15 @@ and improvements to existing code. . ├── examples # Runnable examples by category │ ├── billing/ -│ └── monitoring/ +│ ├── monitoring/ +│ └── performance/ ├── configs # Atlas configuration template -│ └── config.json +│ └── config.example.json ├── internal # Shared utilities and helpers +│ ├── archive/ │ ├── auth/ │ ├── billing/ +│ ├── clusters/ │ ├── config/ │ ├── data/ │ ├── errors/ @@ -56,16 +59,18 @@ and improvements to existing code. ## Setting Environment Variables -1. Create a `.env` file in the root directory with your MongoDB Atlas service account credentials: +1. Create a `.env.` file in the root directory with your MongoDB Atlas service account credentials. For example, create a `.env.development` file for your dev environment: ```dotenv - MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_service_account_id - MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_service_account_secret + MONGODB_ATLAS_SERVICE_ACCOUNT_ID= + MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional download directory + CONFIG_PATH="configs/config.development.json" # optional path to Atlas config file ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. > See [Secrets management](https://www.mongodb.com/docs/atlas/architecture/current/auth/#secrets-management). -2. Configure Atlas details in `configs/config.json`: +2. Create a `config..json` file in the `configs/` directory with your Atlas configuration details. For example, create a `configs/config.development.json` for your dev environment: ```json { "MONGODB_ATLAS_BASE_URL": "", @@ -121,6 +126,13 @@ go run examples/monitoring/metrics_disk/main.go go run examples/monitoring/metrics_process/main.go ``` +### Performance + +#### Archive Cluster Data +```bash +go run examples/performance/archiving/main.go +``` + ## Changelog For list of major changes to this project, see [CHANGELOG](CHANGELOG.md). diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json similarity index 100% rename from generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.json rename to generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/historical/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/historical/main.go index 7414bbc..2bc10f8 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/historical/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/historical/main.go @@ -10,7 +10,6 @@ import ( "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "github.com/joho/godotenv" @@ -18,25 +17,24 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } - fmt.Printf("Fetching historical invoices for organization: %s\n", p.OrgId) // Fetch invoices from the previous six months with the provided options @@ -45,7 +43,7 @@ func main() { billing.WithIncludeCount(true), billing.WithDateRange(time.Now().AddDate(0, -6, 0), time.Now())) if err != nil { - errors.ExitWithError("Failed to retrieve invoices", err) + log.Fatalf("Failed to retrieve invoices: %v", err) } if invoices.GetTotalCount() > 0 { @@ -59,25 +57,32 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("historical_%s", p.OrgId) - exportInvoicesToJSON(invoices, outDir, prefix) - exportInvoicesToCSV(invoices, outDir, prefix) + err = exportInvoicesToJSON(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } } -func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(invoices.GetResults(), jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported invoice data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -91,9 +96,10 @@ func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, pr } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported invoice data to %s\n", csvPath) + return nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/line_items/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/line_items/main.go index 634fac2..909108c 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/line_items/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/line_items/main.go @@ -5,34 +5,33 @@ import ( "fmt" "log" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - - "github.com/joho/godotenv" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -41,7 +40,7 @@ func main() { details, err := billing.CollectLineItemBillingData(ctx, client.InvoicesApi, client.OrganizationsApi, p.OrgId, nil) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve pending invoices for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve pending invoices for %s: %v", p.OrgId, err) } if len(details) == 0 { @@ -54,26 +53,33 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("pending_%s", p.OrgId) - exportInvoicesToJSON(details, outDir, prefix) - exportInvoicesToCSV(details, outDir, prefix) + err = exportInvoicesToJSON(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } } -func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(details, jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported billing data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -95,8 +101,9 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported billing data to %s\n", csvPath) + return nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/linked_orgs/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/linked_orgs/main.go index 4fd48c8..a1e851e 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/linked_orgs/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/linked_orgs/main.go @@ -8,28 +8,28 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -38,7 +38,7 @@ func main() { invoices, err := billing.GetCrossOrgBilling(ctx, client.InvoicesApi, p) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve cross-organization billing data for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve cross-organization billing data for %s: %v", p.OrgId, err) } displayLinkedOrganizations(invoices, p.OrgId) diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/logs/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/logs/main.go index fb29556..f22d3da 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/logs/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/logs/main.go @@ -7,7 +7,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" @@ -16,55 +15,58 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch logs with the provided parameters p := &admin.GetHostLogsApiParams{ GroupId: cfg.ProjectID, HostName: cfg.HostName, LogName: "mongodb", } + fmt.Printf("Request parameters: GroupID=%s, HostName=%s, LogName=%s\n", + cfg.ProjectID, cfg.HostName, p.LogName) rc, err := logs.FetchHostLogs(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch logs", err) + log.Fatalf("Failed to fetch logs: %v", err) } defer fileutils.SafeClose(rc) // Prepare output paths + // If the ATLAS_DOWNLOADS_DIR env variable is set, it will be used as the base directory for output files outDir := "logs" prefix := fmt.Sprintf("%s_%s", p.HostName, p.LogName) gzPath, err := fileutils.GenerateOutputPath(outDir, prefix, "gz") if err != nil { - errors.ExitWithError("Failed to generate GZ output path", err) + log.Fatalf("Failed to generate GZ output path: %v", err) } txtPath, err := fileutils.GenerateOutputPath(outDir, prefix, "txt") if err != nil { - errors.ExitWithError("Failed to generate TXT output path", err) + log.Fatalf("Failed to generate TXT output path: %v", err) } // Save compressed logs if err := fileutils.WriteToFile(rc, gzPath); err != nil { - errors.ExitWithError("Failed to save compressed logs", err) + log.Fatalf("Failed to save compressed logs: %v", err) } fmt.Println("Saved compressed log to", gzPath) // Decompress logs if err := fileutils.DecompressGzip(gzPath, txtPath); err != nil { - errors.ExitWithError("Failed to decompress logs", err) + log.Fatalf("Failed to decompress logs: %v", err) } fmt.Println("Uncompressed log to", txtPath) } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_disk/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_disk/main.go index 9ccedb1..71b535d 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_disk/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_disk/main.go @@ -8,7 +8,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" "github.com/joho/godotenv" @@ -16,22 +15,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch disk metrics with the provided parameters p := &admin.GetDiskMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -43,13 +42,13 @@ func main() { } view, err := metrics.FetchDiskMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch disk metrics", err) + log.Fatalf("Failed to fetch disk metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_process/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_process/main.go index fe74e79..6508426 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_process/main.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/monitoring/metrics_process/main.go @@ -6,8 +6,6 @@ import ( "fmt" "log" - "atlas-sdk-go/internal/errors" - "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -18,22 +16,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch process metrics with the provided parameters p := &admin.GetHostMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -51,13 +49,13 @@ func main() { view, err := metrics.FetchProcessMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch process metrics", err) + log.Fatalf("Failed to fetch process metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/performance/archiving/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/performance/archiving/main.go new file mode 100644 index 0000000..dd8e7be --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/performance/archiving/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + + "github.com/joho/godotenv" +) + +func main() { + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) + } + + secrets, cfg, err := config.LoadAllFromEnv() + if err != nil { + log.Fatalf("Failed to load configuration %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + client, err := auth.NewClient(ctx, cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize authentication client: %v", err) + } + + projectID := cfg.ProjectID + if projectID == "" { + log.Fatal("Failed to find Project ID in configuration") + } + + fmt.Printf("Starting archive analysis for project: %s\n", projectID) + + // Get all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + log.Fatalf("Failed to list clusters: %v", err) + } + + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) + + // Connect to each cluster and analyze collections for archiving + failedArchives := 0 + skippedCandidates := 0 + totalCandidates := 0 + + // Create archive options with custom settings + opts := archive.DefaultOptions() + opts.DefaultRetentionMultiplier = 2 + opts.MinimumRetentionDays = 30 + opts.EnableDataExpiration = true + opts.ArchiveSchedule = "DAILY" + + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) + + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filters them. + // NOTE: In a real implementation, you would analyze collections based on size, age, + // access patterns, and other factors to determine candidates for archiving. + stats := archive.ListCollectionsWithCounts(ctx, client, projectID, clusterName) + candidates := make([]archive.Candidate, 0) + const docThreshold = 100000 + for _, s := range stats { + // Skip internal databases + if s.DatabaseName == "admin" || s.DatabaseName == "local" || s.DatabaseName == "config" { + continue + } + // Demo criterion: collections with >= 100k documents + if s.EstimatedCount >= docThreshold { + candidates = append(candidates, archive.Candidate{ + DatabaseName: s.DatabaseName, + CollectionName: s.CollectionName, + DateField: "createdAt", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + }) + } + } + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + len(candidates), clusterName) + + // Configure online archive for each candidate collection + for _, candidate := range candidates { + // Pre-validate candidate before attempting configuration + if err := archive.ValidateCandidate(candidate, opts); err != nil { + fmt.Printf("- Skipping %s.%s: invalid candidate: %v\n", + candidate.DatabaseName, candidate.CollectionName, err) + skippedCandidates++ + continue + } + + fmt.Printf("- Configuring archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) + if configureErr != nil { + fmt.Printf(" Failed to configure archive: %v\n", configureErr) + failedArchives++ + continue + } + + fmt.Printf(" Successfully configured online archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } + if failedArchives > 0 { + fmt.Printf("WARNING: %d of %d archive configurations failed (excluding skipped)\n", failedArchives, totalCandidates-skippedCandidates) + } + + fmt.Println("Archive analysis and configuration completed.") +} + diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/go.mod b/generated-usage-examples/go/atlas-sdk-go/project-copy/go.mod index 6b94c9f..dbb60e3 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/go.mod +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/go.mod @@ -1,9 +1,11 @@ module atlas-sdk-go go 1.24 + require ( github.com/joho/godotenv v1.5.1 go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 + go.mongodb.org/mongo-driver v1.17.4 ) require ( @@ -12,3 +14,16 @@ require ( golang.org/x/oauth2 v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +require ( + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/go.sum b/generated-usage-examples/go/atlas-sdk-go/project-copy/go.sum index 20ec82d..5d852eb 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/go.sum +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/go.sum @@ -1,19 +1,67 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/mongodb-forks/digest v1.1.0 h1:7eUdsR1BtqLv0mdNm4OXs6ddWvR4X2/OsLwdKksrOoc= github.com/mongodb-forks/digest v1.1.0/go.mod h1:rb+EX8zotClD5Dj4NdgxnJXG9nwrlx3NWKJ8xttz1Dg= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 h1:tm7d3xvbNFIpuvFcppXc1zdpM/dO7HwivpA+Y4np3uQ= go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0/go.mod h1:huR1gWJhExa60NIRhsLDdc7RmmqKJJwnbdlA1UUh8V4= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go new file mode 100644 index 0000000..d01c43d --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go @@ -0,0 +1,149 @@ +package archive + +import ( + "context" + "fmt" + "time" + + "atlas-sdk-go/internal/clusters" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Candidate represents a collection eligible for archiving +type Candidate struct { + DatabaseName string + CollectionName string + DateField string + DateFormat string + RetentionDays int + PartitionFields []string +} + +// Options defines configuration settings for archive operations +type Options struct { + // Default data retention period multiplier + DefaultRetentionMultiplier int + // Minimum retention days required before archiving + MinimumRetentionDays int + // Whether to enable data expiration + EnableDataExpiration bool + // Schedule for archive operations + ArchiveSchedule string +} + +// DefaultOptions returns a zero-value Options instance. Callers should explicitly +// set any desired values at the call site rather than relying on package defaults. +func DefaultOptions() Options { + return Options{} +} + +// ValidateCandidate demonstrates how to pre-validate the archiving candidate to +// ensure that it meets the designated minimum requirements, if any. +// NOTE: Customize criteria as needed to pre-validate candidates before attempting +// to configure an online archive. +func ValidateCandidate(candidate Candidate, opts Options) error { + if candidate.DatabaseName == "" || candidate.CollectionName == "" { + return fmt.Errorf("database name and collection name are required") + } + + if candidate.RetentionDays < opts.MinimumRetentionDays { + return fmt.Errorf("retention days must be at least %d", opts.MinimumRetentionDays) + } + + if len(candidate.PartitionFields) == 0 { + return fmt.Errorf("at least one partition field is required") + } + + // For date-based archiving, validate date field settings + if candidate.DateField != "" { + validFormats := map[string]bool{ + "DATE": true, + "EPOCH_SECONDS": true, + "EPOCH_MILLIS": true, + "EPOCH_NANOSECONDS": true, + "OBJECTID": true, + } + if !validFormats[candidate.DateFormat] { + return fmt.Errorf("invalid date format: %s", candidate.DateFormat) + } + + // Check if date field is included in partition fields + dateFieldIncluded := false + for _, field := range candidate.PartitionFields { + if field == candidate.DateField { + dateFieldIncluded = true + break + } + } + if !dateFieldIncluded { + return fmt.Errorf("date field %s must be included in partition fields", candidate.DateField) + } + } + + return nil +} + +// CollectionStat represents basic statistics about a collection. +type CollectionStat struct { + DatabaseName string + CollectionName string + EstimatedCount int64 +} + +// ListCollectionsWithCounts connects to the cluster with the MongoDB Go Driver and returns a flat list of collections +// with their estimated document counts. +// NOTE: This function intentionally applies no filtering or demo criteria to decide what qualifies as a candidate. +func ListCollectionsWithCounts(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []CollectionStat { + stats := make([]CollectionStat, 0) + + // Get the SRV connection string for the cluster + srv, err := clusters.GetClusterSRVConnectionString(ctx, sdk, projectID, clusterName) + if err != nil || srv == "" { + return stats + } + + ctxConn, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + clientOpts := options.Client().ApplyURI(srv). + SetServerSelectionTimeout(2 * time.Second). + SetConnectTimeout(2 * time.Second) + + // Connect to the cluster using the official MongoDB Go Driver + client, err := mongo.Connect(ctxConn, clientOpts) + if err != nil { + return stats + } + defer func() { _ = client.Disconnect(context.Background()) }() + + _ = client.Ping(ctxConn, nil) + + dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) + if err != nil { + return stats + } + + for _, dbName := range dbNames { + collNames, err := client.Database(dbName).ListCollectionNames(ctx, bson.D{}) + if err != nil { + continue + } + for _, collName := range collNames { + coll := client.Database(dbName).Collection(collName) + // Use EstimatedDocumentCount for speed + count, err := coll.EstimatedDocumentCount(ctx) + if err != nil { + continue + } + stats = append(stats, CollectionStat{ + DatabaseName: dbName, + CollectionName: collName, + EstimatedCount: count, + }) + } + } + return stats +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/configure.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/configure.go new file mode 100644 index 0000000..4a0f38b --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/configure.go @@ -0,0 +1,70 @@ +package archive + +import ( + "context" + "fmt" + + "atlas-sdk-go/internal/errors" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. +// It assumes the candidate has been pre-validated by the caller. +// It sets up partition fields and creates the archive request. If data expiration is enabled, +// it also configures the data expiration rule based on retention days. +func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string, candidate Candidate, opts Options) error { + + // Create partition fields configuration + var partitionFields []admin.PartitionField + for idx, field := range candidate.PartitionFields { + partitionFields = append(partitionFields, admin.PartitionField{ + FieldName: field, + Order: idx + 1, + }) + } + + // Setup data expiration if enabled + var dataExpiration *admin.OnlineArchiveSchedule + if opts.EnableDataExpiration && opts.DefaultRetentionMultiplier > 0 { + expirationDays := candidate.RetentionDays * opts.DefaultRetentionMultiplier + dataExpiration = &admin.OnlineArchiveSchedule{ + Type: opts.ArchiveSchedule, + } + + // Define request body + archiveReq := &admin.BackupOnlineArchiveCreate{ + CollName: candidate.CollectionName, + DbName: candidate.DatabaseName, + PartitionFields: &partitionFields, + } + + // Set expiration if configured + if dataExpiration != nil { + archiveReq.DataExpirationRule = &admin.DataExpirationRule{ + ExpireAfterDays: admin.PtrInt(expirationDays), + } + } + + // Configure date criteria if present + if candidate.DateField != "" { + archiveReq.Criteria = admin.Criteria{ + DateField: admin.PtrString(candidate.DateField), + DateFormat: admin.PtrString(candidate.DateFormat), + ExpireAfterDays: admin.PtrInt(candidate.RetentionDays), + } + } + + // Execute the request + _, _, err := sdk.OnlineArchiveApi.CreateOnlineArchive(ctx, projectID, clusterName, archiveReq).Execute() + + if err != nil { + return errors.FormatError("create online archive", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + } + + return nil +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/auth/client.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/auth/client.go index a0b81ce..c89bc16 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/auth/client.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/auth/client.go @@ -11,20 +11,19 @@ import ( // NewClient initializes and returns an authenticated Atlas API client using OAuth2 with service account credentials (recommended) // See: https://www.mongodb.com/docs/atlas/architecture/current/auth/#service-accounts -func NewClient(cfg *config.Config, secrets *config.Secrets) (*admin.APIClient, error) { - if cfg == nil { - return nil, &errors.ValidationError{Message: "config cannot be nil"} +func NewClient(ctx context.Context, cfg config.Config, secrets config.Secrets) (*admin.APIClient, error) { + if cfg == (config.Config{}) { + return nil, &errors.ValidationError{Message: "config cannot be empty"} } - - if secrets == nil { + if secrets.ServiceAccountID() == "" || secrets.ServiceAccountSecret() == "" { return nil, &errors.ValidationError{Message: "secrets cannot be nil"} } - sdk, err := admin.NewClient( admin.UseBaseURL(cfg.BaseURL), - admin.UseOAuthAuth(context.Background(), - secrets.ServiceAccountID, - secrets.ServiceAccountSecret, + admin.UseOAuthAuth( + ctx, + secrets.ServiceAccountID(), + secrets.ServiceAccountSecret(), ), ) if err != nil { diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/collector.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/collector.go index c17750d..7336ad1 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/collector.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/collector.go @@ -37,7 +37,7 @@ type ProjectInfo struct { // CollectLineItemBillingData retrieves all pending invoices for the specified organization, // transforms them into detailed billing records, and filters out items processed before lastProcessedDate. -// Returns a slice of billing Details or an error if no valid invoices or line items are found. +// Returns a slice of billing Details if pending invoices exists or an error if the operation fails. func CollectLineItemBillingData(ctx context.Context, sdk admin.InvoicesApi, orgSdk admin.OrganizationsApi, orgID string, lastProcessedDate *time.Time) ([]Detail, error) { req := sdk.ListPendingInvoices(ctx, orgID) r, _, err := req.Execute() @@ -46,11 +46,9 @@ func CollectLineItemBillingData(ctx context.Context, sdk admin.InvoicesApi, orgS return nil, errors.FormatError("list pending invoices", orgID, err) } if r == nil || !r.HasResults() || len(r.GetResults()) == 0 { - return nil, &errors.NotFoundError{Resource: "pending invoices", ID: orgID} + return nil, nil } - fmt.Printf("Found %d pending invoice(s)\n", len(r.GetResults())) - // Get organization name orgName, err := getOrganizationName(ctx, orgSdk, orgID) if err != nil { @@ -112,7 +110,7 @@ func processInvoices(invoices []admin.BillingInvoice, orgID, orgName string, las return billingDetails, nil } -// getOrganizationName fetches organization name from API or returns orgID if not found +// getOrganizationName fetches an organization's name for the given organization ID. func getOrganizationName(ctx context.Context, sdk admin.OrganizationsApi, orgID string) (string, error) { req := sdk.GetOrganization(ctx, orgID) org, _, err := req.Execute() diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/clusters/utils.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/clusters/utils.go new file mode 100644 index 0000000..6f1f51f --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/clusters/utils.go @@ -0,0 +1,71 @@ +package clusters + +import ( + "context" + "fmt" + + "atlas-sdk-go/internal/errors" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// ListClusterNames lists all clusters in a project and returns their names. +func ListClusterNames(ctx context.Context, sdk admin.ClustersApi, p *admin.ListClustersApiParams) ([]string, error) { + req := sdk.ListClusters(ctx, p.GroupId) + clusters, _, err := req.Execute() + if err != nil { + return nil, errors.FormatError("list clusters", p.GroupId, err) + } + + var names []string + if clusters != nil && clusters.Results != nil { + for _, cluster := range *clusters.Results { + if cluster.Name != nil { + names = append(names, *cluster.Name) + } + } + } + return names, nil +} + +// GetProcessIdForCluster retrieves the process ID for a given cluster +func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, + p *admin.ListAtlasProcessesApiParams, clusterName string) (string, error) { + + req := sdk.ListAtlasProcesses(ctx, p.GroupId) + r, _, err := req.Execute() + if err != nil { + return "", errors.FormatError("list atlas processes", p.GroupId, err) + } + if r == nil || !r.HasResults() || len(r.GetResults()) == 0 { + return "", nil + } + + // Find the process for the specified cluster + for _, process := range r.GetResults() { + hostName := process.GetUserAlias() + id := process.GetId() + if hostName != "" && hostName == clusterName { + if id != "" { + return id, nil + } + } + } + + return "", fmt.Errorf("no process found for cluster %s", clusterName) +} + +// GetClusterSRVConnectionString returns the standard SRV connection string for a cluster. +func GetClusterSRVConnectionString(ctx context.Context, client *admin.APIClient, projectID, clusterName string) (string, error) { + if client == nil { + return "", fmt.Errorf("nil atlas api client") + } + cluster, _, err := client.ClustersApi.GetCluster(ctx, projectID, clusterName).Execute() + if err != nil { + return "", errors.FormatError("get cluster", projectID, err) + } + if cluster == nil || cluster.ConnectionStrings == nil || cluster.ConnectionStrings.StandardSrv == nil { + return "", fmt.Errorf("no standard SRV connection string found for cluster %s", clusterName) + } + return *cluster.ConnectionStrings.StandardSrv, nil +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadall.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadall.go index d6b3110..3e0170a 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadall.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadall.go @@ -1,20 +1,40 @@ package config import ( + "os" + "strings" + "atlas-sdk-go/internal/errors" ) -// LoadAll loads secrets and config from the specified paths -func LoadAll(configPath string) (*Secrets, *Config, error) { - s, err := LoadSecrets() - if err != nil { - return nil, nil, errors.WithContext(err, "loading secrets") +const defaultConfigPath = "configs/config.json" // Default path if not specified in environment + +// LoadAll loads secrets from .env and configuration from the specified config file. +// If the configPath is empty, it falls back to the default config path. +// It returns both Secrets and Config, or an error if either loading fails. +func LoadAll(configPath string) (Secrets, Config, error) { + if strings.TrimSpace(configPath) == "" { + configPath = defaultConfigPath + } + + if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { + return Secrets{}, Config{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} } - c, err := LoadConfig(configPath) + secrets, err := LoadSecrets() + if err != nil { + return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") + } + cfg, err := LoadConfig(configPath) if err != nil { - return nil, nil, errors.WithContext(err, "loading config") + return Secrets{}, Config{}, errors.WithContext(err, "loading config") } + return secrets, cfg, nil +} - return s, c, nil +// LoadAllFromEnv resolves the configuration path from the CONFIG_PATH environment variable +// and delegates to LoadAll. If CONFIG_PATH is empty, LoadAll will apply its default path. +func LoadAllFromEnv() (Secrets, Config, error) { + configPath := os.Getenv("CONFIG_PATH") + return LoadAll(configPath) } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadconfig.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadconfig.go index 0cff7fc..efef784 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadconfig.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadconfig.go @@ -3,23 +3,27 @@ package config import ( "encoding/json" "os" + "strings" "atlas-sdk-go/internal/errors" ) +// Config holds the configuration for connecting to MongoDB Atlas type Config struct { BaseURL string `json:"MONGODB_ATLAS_BASE_URL"` OrgID string `json:"ATLAS_ORG_ID"` ProjectID string `json:"ATLAS_PROJECT_ID"` ClusterName string `json:"ATLAS_CLUSTER_NAME"` - HostName string + HostName string `json:"ATLAS_HOSTNAME"` ProcessID string `json:"ATLAS_PROCESS_ID"` } // LoadConfig reads a JSON configuration file and returns a Config struct -func LoadConfig(path string) (*Config, error) { +// It validates required fields and returns an error if any validation fails. +func LoadConfig(path string) (Config, error) { + var config Config if path == "" { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "configuration file path cannot be empty", } } @@ -27,21 +31,39 @@ func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - return nil, &errors.NotFoundError{Resource: "configuration file", ID: path} + return config, &errors.NotFoundError{Resource: "configuration file", ID: path} } - return nil, errors.WithContext(err, "reading configuration file") + return config, errors.WithContext(err, "reading configuration file") } - var config Config - if err := json.Unmarshal(data, &config); err != nil { - return nil, errors.WithContext(err, "parsing configuration file") + if err = json.Unmarshal(data, &config); err != nil { + return config, errors.WithContext(err, "parsing configuration file") } + if config.OrgID == "" { + return config, &errors.ValidationError{ + Message: "organization ID is required in configuration", + } + } if config.ProjectID == "" { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "project ID is required in configuration", } } - return &config, nil + if config.HostName == "" { + if host, _, ok := strings.Cut(config.ProcessID, ":"); ok { + config.HostName = host + } else { + return config, &errors.ValidationError{ + Message: "process ID must be in the format 'hostname:port'", + } + } + } + + if config.BaseURL == "" { + config.BaseURL = "https://cloud.mongodb.com" // Default base URL if not provided + } + + return config, nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadenv.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadenv.go deleted file mode 100644 index 2d31065..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadenv.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "os" - "strings" - - "atlas-sdk-go/internal/errors" -) - -const ( - EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" - EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" -) - -type Secrets struct { - ServiceAccountID string - ServiceAccountSecret string -} - -func LoadSecrets() (*Secrets, error) { - s := &Secrets{} - var missing []string - - look := func(key string, dest *string) { - if v, ok := os.LookupEnv(key); ok && v != "" { - *dest = v - } else { - missing = append(missing, key) - } - } - - look(EnvSAClientID, &s.ServiceAccountID) - look(EnvSAClientSecret, &s.ServiceAccountSecret) - - if len(missing) > 0 { - return nil, &errors.ValidationError{ - Message: "missing required environment variables: " + strings.Join(missing, ", "), - } - } - return s, nil -} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadsecrets.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadsecrets.go new file mode 100644 index 0000000..1e2605a --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadsecrets.go @@ -0,0 +1,52 @@ +package config + +import ( + "errors" + "fmt" + "os" +) + +const ( + envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" +) + +var errMissingEnv = errors.New("missing environment variable") + +// Secrets contains sensitive configuration loaded from environment variables +type Secrets struct { + serviceAccountID string + serviceAccountSecret string +} + +func (s Secrets) ServiceAccountID() string { + return s.serviceAccountID +} + +func (s Secrets) ServiceAccountSecret() string { + return s.serviceAccountSecret +} + +// LoadSecrets loads sensitive configuration from environment variables +// Returns error if any required environment variable is missing +func LoadSecrets() (Secrets, error) { + s := Secrets{} + var missing []string + + look := func(key string, dest *string) { + if v, ok := os.LookupEnv(key); ok && v != "" { + *dest = v + } else { + missing = append(missing, key) + } + } + + look(envServiceAccountID, &s.serviceAccountID) + look(envServiceAccountSecret, &s.serviceAccountSecret) + + if len(missing) > 0 { + return Secrets{}, fmt.Errorf("load secrets: %w (missing: %v)", errMissingEnv, missing) + } + return s, nil +} + diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/errors/utils.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/errors/utils.go index ca8130b..fd79f3c 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/errors/utils.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/errors/utils.go @@ -2,7 +2,6 @@ package errors import ( "fmt" - "log" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -40,9 +39,3 @@ type NotFoundError struct { func (e *NotFoundError) Error() string { return fmt.Sprintf("resource not found: %s [%s]", e.Resource, e.ID) } - -// ExitWithError prints an error message with context and exits the program -func ExitWithError(context string, err error) { - log.Fatalf("%s: %v", context, err) - // Note: log.Fatalf calls os.Exit(1) -} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/env.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/env.go new file mode 100644 index 0000000..e77e8ab --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/env.go @@ -0,0 +1,23 @@ +package fileutils + +import ( + "os" + "path/filepath" +) + +// DownloadsBaseDir returns the base directory for downloads as specified by the +// ATLAS_DOWNLOADS_DIR environment variable. If the variable is not set, it +// returns an empty string. +func DownloadsBaseDir() string { + return os.Getenv("ATLAS_DOWNLOADS_DIR") +} + +// ResolveWithDownloadsBase returns dir prefixed with the global downloads base +// directory when ATLAS_DOWNLOADS_DIR is set; otherwise returns dir as-is. +func ResolveWithDownloadsBase(dir string) string { + base := DownloadsBaseDir() + if base == "" { + return dir + } + return filepath.Join(base, dir) +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/path.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/path.go index 3e3e6a8..2a9605a 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/path.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/path.go @@ -13,10 +13,7 @@ import ( // NOTE: You can define a default global directory for all generated files by setting the ATLAS_DOWNLOADS_DIR environment variable. func GenerateOutputPath(dir, prefix, extension string) (string, error) { // If default download directory is set in .env, prepend it to the provided dir - defaultDir := os.Getenv("ATLAS_DOWNLOADS_DIR") - if defaultDir != "" { - dir = filepath.Join(defaultDir, dir) - } + dir = ResolveWithDownloadsBase(dir) // Create directory if it doesn't exist if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/usage-examples/go/atlas-sdk-go/.env.example b/usage-examples/go/atlas-sdk-go/.env.example index a1f8da0..66f7781 100644 --- a/usage-examples/go/atlas-sdk-go/.env.example +++ b/usage-examples/go/atlas-sdk-go/.env.example @@ -1,2 +1,4 @@ -MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id -MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret +MONGODB_ATLAS_SERVICE_ACCOUNT_ID= +MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= +ATLAS_DOWNLOADS_DIR=tmp/atlas_downloads # optional directory for downloads +CONFIG_PATH=./configs/config..json # path to corresponding config file \ No newline at end of file diff --git a/usage-examples/go/atlas-sdk-go/.gitignore b/usage-examples/go/atlas-sdk-go/.gitignore index 22cadb7..ac48429 100644 --- a/usage-examples/go/atlas-sdk-go/.gitignore +++ b/usage-examples/go/atlas-sdk-go/.gitignore @@ -1,6 +1,16 @@ -# Secrets +# Secrets (keep example) .env +!.env.example +.env.production -# Logs +# Configs (keep example) +configs/config.json +!configs/config.example.json + +# temporary files +tmp +temp + +# downloaded logs *.log *.gz diff --git a/usage-examples/go/atlas-sdk-go/CHANGELOG.md b/usage-examples/go/atlas-sdk-go/CHANGELOG.md index 08d81a3..55ed428 100644 --- a/usage-examples/go/atlas-sdk-go/CHANGELOG.md +++ b/usage-examples/go/atlas-sdk-go/CHANGELOG.md @@ -2,6 +2,10 @@ Notable changes to the project. +## v1.2 (2025-08-17) +### Added +- Example scripts for scaling and archiving clusters. + ## v1.1 (2025-06-17) ### Added - Example scripts for fetching cross-organization billing information. diff --git a/usage-examples/go/atlas-sdk-go/README.md b/usage-examples/go/atlas-sdk-go/README.md index b17f5cb..96b9817 100644 --- a/usage-examples/go/atlas-sdk-go/README.md +++ b/usage-examples/go/atlas-sdk-go/README.md @@ -16,8 +16,8 @@ Currently, the repository includes examples that demonstrate the following: - Download logs for a specific host - Pull and parse line-item-level billing data - Return all linked organizations from a specific billing organization -- Get historical invoices for an organization -- Programmatically manage Atlas resources +- Get historical invoices for an organization +- Programmatically archive Atlas cluster data As the Architecture Center documentation evolves, this repository will be updated with new examples and improvements to existing code. @@ -28,12 +28,15 @@ and improvements to existing code. . ├── examples # Runnable examples by category │ ├── billing/ -│ └── monitoring/ +│ ├── monitoring/ +│ └── performance/ ├── configs # Atlas configuration template -│ └── config.json +│ └── config.example.json ├── internal # Shared utilities and helpers +│ ├── archive/ │ ├── auth/ │ ├── billing/ +│ ├── clusters/ │ ├── config/ │ ├── data/ │ ├── errors/ @@ -56,16 +59,18 @@ and improvements to existing code. ## Setting Environment Variables -1. Create a `.env` file in the root directory with your MongoDB Atlas service account credentials: +1. Create a `.env.` file in the root directory with your MongoDB Atlas service account credentials. For example, create a `.env.development` file for your dev environment: ```dotenv - MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_service_account_id - MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_service_account_secret + MONGODB_ATLAS_SERVICE_ACCOUNT_ID= + MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional download directory + CONFIG_PATH="configs/config.development.json" # optional path to Atlas config file ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. > See [Secrets management](https://www.mongodb.com/docs/atlas/architecture/current/auth/#secrets-management). -2. Configure Atlas details in `configs/config.json`: +2. Create a `config..json` file in the `configs/` directory with your Atlas configuration details. For example, create a `configs/config.development.json` for your dev environment: ```json { "MONGODB_ATLAS_BASE_URL": "", @@ -121,6 +126,13 @@ go run examples/monitoring/metrics_disk/main.go go run examples/monitoring/metrics_process/main.go ``` +### Performance + +#### Archive Cluster Data +```bash +go run examples/performance/archiving/main.go +``` + ## Changelog For list of major changes to this project, see [CHANGELOG](CHANGELOG.md). diff --git a/usage-examples/go/atlas-sdk-go/configs/config.json b/usage-examples/go/atlas-sdk-go/configs/config.example.json similarity index 100% rename from usage-examples/go/atlas-sdk-go/configs/config.json rename to usage-examples/go/atlas-sdk-go/configs/config.example.json diff --git a/usage-examples/go/atlas-sdk-go/examples/billing/historical/main.go b/usage-examples/go/atlas-sdk-go/examples/billing/historical/main.go index 9f07f5e..80abfce 100644 --- a/usage-examples/go/atlas-sdk-go/examples/billing/historical/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/billing/historical/main.go @@ -14,7 +14,6 @@ import ( "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "github.com/joho/godotenv" @@ -22,25 +21,24 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } - fmt.Printf("Fetching historical invoices for organization: %s\n", p.OrgId) // Fetch invoices from the previous six months with the provided options @@ -49,7 +47,7 @@ func main() { billing.WithIncludeCount(true), billing.WithDateRange(time.Now().AddDate(0, -6, 0), time.Now())) if err != nil { - errors.ExitWithError("Failed to retrieve invoices", err) + log.Fatalf("Failed to retrieve invoices: %v", err) } if invoices.GetTotalCount() > 0 { @@ -63,32 +61,39 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("historical_%s", p.OrgId) - exportInvoicesToJSON(invoices, outDir, prefix) - exportInvoicesToCSV(invoices, outDir, prefix) + err = exportInvoicesToJSON(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(invoices, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } // :remove-start: // Clean up (internal-only function) - if err := fileutils.SafeDelete(outDir); err != nil { + if err = fileutils.SafeDelete(outDir); err != nil { log.Printf("Cleanup error: %v", err) } fmt.Println("Deleted generated files from", outDir) // :remove-end: } -func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToJSON(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(invoices.GetResults(), jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported invoice data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) { +func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -102,10 +107,11 @@ func exportInvoicesToCSV(invoices *admin.PaginatedApiInvoiceMetadata, outDir, pr } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported invoice data to %s\n", csvPath) + return nil } // :snippet-end: [historical-billing] diff --git a/usage-examples/go/atlas-sdk-go/examples/billing/line_items/main.go b/usage-examples/go/atlas-sdk-go/examples/billing/line_items/main.go index 5a96db1..5a178a2 100644 --- a/usage-examples/go/atlas-sdk-go/examples/billing/line_items/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/billing/line_items/main.go @@ -9,34 +9,33 @@ import ( "fmt" "log" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/data/export" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - - "github.com/joho/godotenv" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -45,7 +44,7 @@ func main() { details, err := billing.CollectLineItemBillingData(ctx, client.InvoicesApi, client.OrganizationsApi, p.OrgId, nil) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve pending invoices for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve pending invoices for %s: %v", p.OrgId, err) } if len(details) == 0 { @@ -58,33 +57,40 @@ func main() { outDir := "invoices" prefix := fmt.Sprintf("pending_%s", p.OrgId) - exportInvoicesToJSON(details, outDir, prefix) - exportInvoicesToCSV(details, outDir, prefix) + err = exportInvoicesToJSON(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to JSON: %v", err) + } + err = exportInvoicesToCSV(details, outDir, prefix) + if err != nil { + log.Fatalf("Failed to export invoices to CSV: %v", err) + } // :remove-start: // Clean up (internal-only function) - if err := fileutils.SafeDelete(outDir); err != nil { + if err = fileutils.SafeDelete(outDir); err != nil { log.Printf("Cleanup error: %v", err) } fmt.Println("Deleted generated files from", outDir) // :remove-end: } -func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToJSON(details []billing.Detail, outDir, prefix string) error { jsonPath, err := fileutils.GenerateOutputPath(outDir, prefix, "json") if err != nil { - errors.ExitWithError("Failed to generate JSON output path", err) + return fmt.Errorf("failed to generate JSON output path: %v", err) } if err := export.ToJSON(details, jsonPath); err != nil { - errors.ExitWithError("Failed to write JSON file", err) + return fmt.Errorf("failed to write JSON file: %v", err) } fmt.Printf("Exported billing data to %s\n", jsonPath) + return nil } -func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { +func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) error { csvPath, err := fileutils.GenerateOutputPath(outDir, prefix, "csv") if err != nil { - errors.ExitWithError("Failed to generate CSV output path", err) + return fmt.Errorf("failed to generate CSV output path: %v", err) } // Set the headers and mapped rows for the CSV export @@ -106,9 +112,10 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { } }) if err != nil { - errors.ExitWithError("Failed to write CSV file", err) + return fmt.Errorf("failed to write CSV file: %v", err) } fmt.Printf("Exported billing data to %s\n", csvPath) + return nil } // :snippet-end: [line-items] diff --git a/usage-examples/go/atlas-sdk-go/examples/billing/linked_orgs/main.go b/usage-examples/go/atlas-sdk-go/examples/billing/linked_orgs/main.go index 316c8d3..a21aa3c 100644 --- a/usage-examples/go/atlas-sdk-go/examples/billing/linked_orgs/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/billing/linked_orgs/main.go @@ -12,28 +12,28 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ OrgId: cfg.OrgID, } @@ -42,7 +42,7 @@ func main() { invoices, err := billing.GetCrossOrgBilling(ctx, client.InvoicesApi, p) if err != nil { - errors.ExitWithError(fmt.Sprintf("Failed to retrieve cross-organization billing data for %s", p.OrgId), err) + log.Fatalf("Failed to retrieve cross-organization billing data for %s: %v", p.OrgId, err) } displayLinkedOrganizations(invoices, p.OrgId) diff --git a/usage-examples/go/atlas-sdk-go/examples/monitoring/logs/main.go b/usage-examples/go/atlas-sdk-go/examples/monitoring/logs/main.go index b24a713..d19ab56 100644 --- a/usage-examples/go/atlas-sdk-go/examples/monitoring/logs/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/monitoring/logs/main.go @@ -11,7 +11,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" @@ -20,22 +19,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch logs with the provided parameters p := &admin.GetHostLogsApiParams{ GroupId: cfg.ProjectID, @@ -46,7 +45,7 @@ func main() { cfg.ProjectID, cfg.HostName, p.LogName) rc, err := logs.FetchHostLogs(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch logs", err) + log.Fatalf("Failed to fetch logs: %v", err) } defer fileutils.SafeClose(rc) @@ -56,22 +55,22 @@ func main() { prefix := fmt.Sprintf("%s_%s", p.HostName, p.LogName) gzPath, err := fileutils.GenerateOutputPath(outDir, prefix, "gz") if err != nil { - errors.ExitWithError("Failed to generate GZ output path", err) + log.Fatalf("Failed to generate GZ output path: %v", err) } txtPath, err := fileutils.GenerateOutputPath(outDir, prefix, "txt") if err != nil { - errors.ExitWithError("Failed to generate TXT output path", err) + log.Fatalf("Failed to generate TXT output path: %v", err) } // Save compressed logs if err := fileutils.WriteToFile(rc, gzPath); err != nil { - errors.ExitWithError("Failed to save compressed logs", err) + log.Fatalf("Failed to save compressed logs: %v", err) } fmt.Println("Saved compressed log to", gzPath) // Decompress logs if err := fileutils.DecompressGzip(gzPath, txtPath); err != nil { - errors.ExitWithError("Failed to decompress logs", err) + log.Fatalf("Failed to decompress logs: %v", err) } fmt.Println("Uncompressed log to", txtPath) // :remove-start: diff --git a/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_disk/main.go b/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_disk/main.go index 74fb9a8..77ffacc 100644 --- a/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_disk/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_disk/main.go @@ -12,7 +12,6 @@ import ( "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" "github.com/joho/godotenv" @@ -20,22 +19,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch disk metrics with the provided parameters p := &admin.GetDiskMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -47,13 +46,13 @@ func main() { } view, err := metrics.FetchDiskMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch disk metrics", err) + log.Fatalf("Failed to fetch disk metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_process/main.go b/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_process/main.go index 880d137..4190a71 100644 --- a/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_process/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/monitoring/metrics_process/main.go @@ -10,8 +10,6 @@ import ( "fmt" "log" - "atlas-sdk-go/internal/errors" - "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -22,22 +20,22 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - secrets, cfg, err := config.LoadAll("configs/config.json") + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + ctx := context.Background() + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { - errors.ExitWithError("Failed to initialize authentication client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - ctx := context.Background() - // Fetch process metrics with the provided parameters p := &admin.GetHostMeasurementsApiParams{ GroupId: cfg.ProjectID, @@ -55,13 +53,13 @@ func main() { view, err := metrics.FetchProcessMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - errors.ExitWithError("Failed to fetch process metrics", err) + log.Fatalf("Failed to fetch process metrics: %v", err) } // Output metrics out, err := json.MarshalIndent(view, "", " ") if err != nil { - errors.ExitWithError("Failed to format metrics data", err) + log.Fatalf("Failed to format metrics data: %v", err) } fmt.Println(string(out)) } diff --git a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go new file mode 100644 index 0000000..1e5c41b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -0,0 +1,144 @@ +// :snippet-start: archive-collections +// :state-remove-start: copy +// See entire project at https://github.com/mongodb/atlas-architecture-go-sdk +// :state-remove-end: [copy] +package main + +import ( + "context" + "fmt" + "log" + "time" + + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + + "github.com/joho/godotenv" +) + +func main() { + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) + } + + secrets, cfg, err := config.LoadAllFromEnv() + if err != nil { + log.Fatalf("Failed to load configuration %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + client, err := auth.NewClient(ctx, cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize authentication client: %v", err) + } + + projectID := cfg.ProjectID + if projectID == "" { + log.Fatal("Failed to find Project ID in configuration") + } + + fmt.Printf("Starting archive analysis for project: %s\n", projectID) + + // Get all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + log.Fatalf("Failed to list clusters: %v", err) + } + + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) + + // Connect to each cluster and analyze collections for archiving + failedArchives := 0 + skippedCandidates := 0 + totalCandidates := 0 + + // Create archive options with custom settings + opts := archive.DefaultOptions() + opts.DefaultRetentionMultiplier = 2 + opts.MinimumRetentionDays = 30 + opts.EnableDataExpiration = true + opts.ArchiveSchedule = "DAILY" + + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) + + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filters them. + // NOTE: In a real implementation, you would analyze collections based on size, age, + // access patterns, and other factors to determine candidates for archiving. + stats := archive.ListCollectionsWithCounts(ctx, client, projectID, clusterName) + candidates := make([]archive.Candidate, 0) + const docThreshold = 100000 + for _, s := range stats { + // Skip internal databases + if s.DatabaseName == "admin" || s.DatabaseName == "local" || s.DatabaseName == "config" { + continue + } + // Demo criterion: collections with >= 100k documents + if s.EstimatedCount >= docThreshold { + candidates = append(candidates, archive.Candidate{ + DatabaseName: s.DatabaseName, + CollectionName: s.CollectionName, + DateField: "createdAt", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + }) + } + } + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + len(candidates), clusterName) + + // Configure online archive for each candidate collection + for _, candidate := range candidates { + // Pre-validate candidate before attempting configuration + if err := archive.ValidateCandidate(candidate, opts); err != nil { + fmt.Printf("- Skipping %s.%s: invalid candidate: %v\n", + candidate.DatabaseName, candidate.CollectionName, err) + skippedCandidates++ + continue + } + + fmt.Printf("- Configuring archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) + if configureErr != nil { + fmt.Printf(" Failed to configure archive: %v\n", configureErr) + failedArchives++ + continue + } + + fmt.Printf(" Successfully configured online archive for %s.%s\n", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } + if failedArchives > 0 { + fmt.Printf("WARNING: %d of %d archive configurations failed (excluding skipped)\n", failedArchives, totalCandidates-skippedCandidates) + } + + fmt.Println("Archive analysis and configuration completed.") +} + +// :snippet-end: [archive-collections] +// :state-remove-start: copy +// NOTE: INTERNAL +// ** OUTPUT EXAMPLE ** +// +//Starting archive analysis for project: 634f249136136a2dd3f8d8f9 +// +//Found 1 clusters to analyze +// +//=== Analyzing cluster: Sandbox === +//Found 0 collections eligible for archiving in cluster Sandbox +//Archive analysis and configuration completed. +// :state-remove-end: [copy] diff --git a/usage-examples/go/atlas-sdk-go/go.mod b/usage-examples/go/atlas-sdk-go/go.mod index fd4200e..aac1478 100644 --- a/usage-examples/go/atlas-sdk-go/go.mod +++ b/usage-examples/go/atlas-sdk-go/go.mod @@ -1,6 +1,7 @@ module atlas-sdk-go go 1.24 + // :remove-start: // NOTE: confirm testify and indirect dependencies are removed in Bluehawk copy output // once copied, confirm project builds successfully in artifact repo @@ -9,13 +10,27 @@ require ( github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.10.0 // :remove: go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 + go.mongodb.org/mongo-driver v1.17.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mongodb-forks/digest v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect // :remove: - github.com/stretchr/objx v0.5.2 // indirect // :remove: + github.com/pmezard/go-difflib v1.0.0 // indirect; indirect // :remove: + github.com/stretchr/objx v0.5.2 // indirect; indirect // :remove: golang.org/x/oauth2 v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +require ( + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/usage-examples/go/atlas-sdk-go/go.sum b/usage-examples/go/atlas-sdk-go/go.sum index 20ec82d..5d852eb 100644 --- a/usage-examples/go/atlas-sdk-go/go.sum +++ b/usage-examples/go/atlas-sdk-go/go.sum @@ -1,19 +1,67 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/mongodb-forks/digest v1.1.0 h1:7eUdsR1BtqLv0mdNm4OXs6ddWvR4X2/OsLwdKksrOoc= github.com/mongodb-forks/digest v1.1.0/go.mod h1:rb+EX8zotClD5Dj4NdgxnJXG9nwrlx3NWKJ8xttz1Dg= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 h1:tm7d3xvbNFIpuvFcppXc1zdpM/dO7HwivpA+Y4np3uQ= go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0/go.mod h1:huR1gWJhExa60NIRhsLDdc7RmmqKJJwnbdlA1UUh8V4= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go new file mode 100644 index 0000000..d01c43d --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -0,0 +1,149 @@ +package archive + +import ( + "context" + "fmt" + "time" + + "atlas-sdk-go/internal/clusters" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Candidate represents a collection eligible for archiving +type Candidate struct { + DatabaseName string + CollectionName string + DateField string + DateFormat string + RetentionDays int + PartitionFields []string +} + +// Options defines configuration settings for archive operations +type Options struct { + // Default data retention period multiplier + DefaultRetentionMultiplier int + // Minimum retention days required before archiving + MinimumRetentionDays int + // Whether to enable data expiration + EnableDataExpiration bool + // Schedule for archive operations + ArchiveSchedule string +} + +// DefaultOptions returns a zero-value Options instance. Callers should explicitly +// set any desired values at the call site rather than relying on package defaults. +func DefaultOptions() Options { + return Options{} +} + +// ValidateCandidate demonstrates how to pre-validate the archiving candidate to +// ensure that it meets the designated minimum requirements, if any. +// NOTE: Customize criteria as needed to pre-validate candidates before attempting +// to configure an online archive. +func ValidateCandidate(candidate Candidate, opts Options) error { + if candidate.DatabaseName == "" || candidate.CollectionName == "" { + return fmt.Errorf("database name and collection name are required") + } + + if candidate.RetentionDays < opts.MinimumRetentionDays { + return fmt.Errorf("retention days must be at least %d", opts.MinimumRetentionDays) + } + + if len(candidate.PartitionFields) == 0 { + return fmt.Errorf("at least one partition field is required") + } + + // For date-based archiving, validate date field settings + if candidate.DateField != "" { + validFormats := map[string]bool{ + "DATE": true, + "EPOCH_SECONDS": true, + "EPOCH_MILLIS": true, + "EPOCH_NANOSECONDS": true, + "OBJECTID": true, + } + if !validFormats[candidate.DateFormat] { + return fmt.Errorf("invalid date format: %s", candidate.DateFormat) + } + + // Check if date field is included in partition fields + dateFieldIncluded := false + for _, field := range candidate.PartitionFields { + if field == candidate.DateField { + dateFieldIncluded = true + break + } + } + if !dateFieldIncluded { + return fmt.Errorf("date field %s must be included in partition fields", candidate.DateField) + } + } + + return nil +} + +// CollectionStat represents basic statistics about a collection. +type CollectionStat struct { + DatabaseName string + CollectionName string + EstimatedCount int64 +} + +// ListCollectionsWithCounts connects to the cluster with the MongoDB Go Driver and returns a flat list of collections +// with their estimated document counts. +// NOTE: This function intentionally applies no filtering or demo criteria to decide what qualifies as a candidate. +func ListCollectionsWithCounts(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []CollectionStat { + stats := make([]CollectionStat, 0) + + // Get the SRV connection string for the cluster + srv, err := clusters.GetClusterSRVConnectionString(ctx, sdk, projectID, clusterName) + if err != nil || srv == "" { + return stats + } + + ctxConn, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + clientOpts := options.Client().ApplyURI(srv). + SetServerSelectionTimeout(2 * time.Second). + SetConnectTimeout(2 * time.Second) + + // Connect to the cluster using the official MongoDB Go Driver + client, err := mongo.Connect(ctxConn, clientOpts) + if err != nil { + return stats + } + defer func() { _ = client.Disconnect(context.Background()) }() + + _ = client.Ping(ctxConn, nil) + + dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) + if err != nil { + return stats + } + + for _, dbName := range dbNames { + collNames, err := client.Database(dbName).ListCollectionNames(ctx, bson.D{}) + if err != nil { + continue + } + for _, collName := range collNames { + coll := client.Database(dbName).Collection(collName) + // Use EstimatedDocumentCount for speed + count, err := coll.EstimatedDocumentCount(ctx) + if err != nil { + continue + } + stats = append(stats, CollectionStat{ + DatabaseName: dbName, + CollectionName: collName, + EstimatedCount: count, + }) + } + } + return stats +} diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go new file mode 100644 index 0000000..e586e31 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go @@ -0,0 +1,102 @@ +package archive + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateCandidate_ReturnsError_WhenDatabaseOrCollectionNameMissing(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) + err = ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) +} + +func TestValidateCandidate_ReturnsError_WhenRetentionDaysTooLow(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 10, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) +} + +func TestValidateCandidate_ReturnsError_WhenNoPartitionFields(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) +} + +func TestValidateCandidate_ReturnsError_WhenInvalidDateFormat(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "INVALID", + }, opts) + assert.Error(t, err) +} + +func TestValidateCandidate_ReturnsError_WhenDateFieldNotInPartitionFields(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"otherField"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) +} + +func TestValidateCandidate_Succeeds_WithValidInput(t *testing.T) { + opts := Options{MinimumRetentionDays: 30} + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.NoError(t, err) +} + +func TestDefaultOptions_ReturnsExpectedDefaults(t *testing.T) { + opts := DefaultOptions() + assert.Equal(t, 0, opts.DefaultRetentionMultiplier) + assert.Equal(t, 0, opts.MinimumRetentionDays) + assert.False(t, opts.EnableDataExpiration) + assert.Equal(t, "", opts.ArchiveSchedule) +} diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/configure.go b/usage-examples/go/atlas-sdk-go/internal/archive/configure.go new file mode 100644 index 0000000..4a0f38b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure.go @@ -0,0 +1,70 @@ +package archive + +import ( + "context" + "fmt" + + "atlas-sdk-go/internal/errors" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. +// It assumes the candidate has been pre-validated by the caller. +// It sets up partition fields and creates the archive request. If data expiration is enabled, +// it also configures the data expiration rule based on retention days. +func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string, candidate Candidate, opts Options) error { + + // Create partition fields configuration + var partitionFields []admin.PartitionField + for idx, field := range candidate.PartitionFields { + partitionFields = append(partitionFields, admin.PartitionField{ + FieldName: field, + Order: idx + 1, + }) + } + + // Setup data expiration if enabled + var dataExpiration *admin.OnlineArchiveSchedule + if opts.EnableDataExpiration && opts.DefaultRetentionMultiplier > 0 { + expirationDays := candidate.RetentionDays * opts.DefaultRetentionMultiplier + dataExpiration = &admin.OnlineArchiveSchedule{ + Type: opts.ArchiveSchedule, + } + + // Define request body + archiveReq := &admin.BackupOnlineArchiveCreate{ + CollName: candidate.CollectionName, + DbName: candidate.DatabaseName, + PartitionFields: &partitionFields, + } + + // Set expiration if configured + if dataExpiration != nil { + archiveReq.DataExpirationRule = &admin.DataExpirationRule{ + ExpireAfterDays: admin.PtrInt(expirationDays), + } + } + + // Configure date criteria if present + if candidate.DateField != "" { + archiveReq.Criteria = admin.Criteria{ + DateField: admin.PtrString(candidate.DateField), + DateFormat: admin.PtrString(candidate.DateFormat), + ExpireAfterDays: admin.PtrInt(candidate.RetentionDays), + } + } + + // Execute the request + _, _, err := sdk.OnlineArchiveApi.CreateOnlineArchive(ctx, projectID, clusterName, archiveReq).Execute() + + if err != nil { + return errors.FormatError("create online archive", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + } + + return nil +} diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go b/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go new file mode 100644 index 0000000..61b9211 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go @@ -0,0 +1,143 @@ +package archive + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// helper to build an SDK client that targets a test server +func testClient(baseURL string, t *testing.T) *admin.APIClient { + t.Helper() + sdk, err := admin.NewClient( + admin.UseBaseURL(baseURL), + ) + if err != nil { + t.Fatalf("failed to create sdk client: %v", err) + } + return sdk +} + +func TestConfigureOnlineArchive_SendsExpectedRequest_WhenExpirationEnabled(t *testing.T) { + // Capture request and validate JSON body + var capturedMethod string + var capturedPath string + var capturedBody []byte + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedMethod = r.Method + capturedPath = r.URL.Path + body, _ := io.ReadAll(r.Body) + _ = r.Body.Close() + capturedBody = append([]byte(nil), body...) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + })) + defer srv.Close() + + sdk := testClient(srv.URL, t) + + ctx := context.Background() + candidate := Candidate{ + DatabaseName: "sales", + CollectionName: "orders", + RetentionDays: 90, + PartitionFields: []string{"createdAt", "tenantId"}, + DateField: "createdAt", + DateFormat: "DATE", + } + opts := Options{ + MinimumRetentionDays: 30, + EnableDataExpiration: true, + DefaultRetentionMultiplier: 2, + ArchiveSchedule: "DAILY", + } + + err := ConfigureOnlineArchive(ctx, sdk, "1234567890abcdef", "Cluster0", candidate, opts) + assert.NoError(t, err) + + assert.Equal(t, http.MethodPost, capturedMethod) + // Path should include groups/{projectId}/clusters/{clusterName}/onlineArchives + assert.Contains(t, capturedPath, "/groups/1234567890abcdef/") + assert.Contains(t, capturedPath, "/clusters/Cluster0/") + assert.Contains(t, capturedPath, "/onlineArchives") + + // Validate JSON payload has expected fields + var payload map[string]any + dec := json.NewDecoder(bytes.NewReader(capturedBody)) + dec.UseNumber() + err = dec.Decode(&payload) + assert.NoError(t, err) + + assert.Equal(t, "orders", payload["collName"]) + assert.Equal(t, "sales", payload["dbName"]) + + // partitionFields should be an array with order values starting at 1 + pfs, ok := payload["partitionFields"].([]any) + if assert.True(t, ok, "partitionFields should be an array") { + if assert.Len(t, pfs, 2) { + pf0 := pfs[0].(map[string]any) + pf1 := pfs[1].(map[string]any) + assert.Equal(t, "createdAt", pf0["fieldName"]) // JSON field names are lower-camel + assert.Equal(t, json.Number("1"), pf0["order"]) // numbers may be json.Number + assert.Equal(t, "tenantId", pf1["fieldName"]) + assert.Equal(t, json.Number("2"), pf1["order"]) + } + } + + // Data expiration rule should be present and equal to RetentionDays * multiplier + der, ok := payload["dataExpirationRule"].(map[string]any) + if assert.True(t, ok, "dataExpirationRule should be present") { + assert.Equal(t, json.Number("180"), der["expireAfterDays"]) // 90 * 2 + } + + // Criteria should be included because DateField is provided + crit, ok := payload["criteria"].(map[string]any) + if assert.True(t, ok, "criteria should be present") { + assert.Equal(t, "createdAt", crit["dateField"]) + assert.Equal(t, "DATE", crit["dateFormat"]) + assert.Equal(t, json.Number("90"), crit["expireAfterDays"]) // candidate.RetentionDays + } +} + +func TestConfigureOnlineArchive_NoOp_WhenExpirationDisabled(t *testing.T) { + var hit int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&hit, 1) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer srv.Close() + + sdk := testClient(srv.URL, t) + + ctx := context.Background() + candidate := Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + } + + // Case 1: disabled flag + opts := Options{MinimumRetentionDays: 30, EnableDataExpiration: false, DefaultRetentionMultiplier: 2} + err := ConfigureOnlineArchive(ctx, sdk, "g", "c", candidate, opts) + assert.NoError(t, err) + + // Case 2: zero multiplier + opts2 := Options{MinimumRetentionDays: 30, EnableDataExpiration: true, DefaultRetentionMultiplier: 0} + err = ConfigureOnlineArchive(ctx, sdk, "g", "c", candidate, opts2) + assert.NoError(t, err) + + // No HTTP requests should have been made in either case + assert.Equal(t, int32(0), atomic.LoadInt32(&hit)) +} diff --git a/usage-examples/go/atlas-sdk-go/internal/auth/client.go b/usage-examples/go/atlas-sdk-go/internal/auth/client.go index a0b81ce..c89bc16 100644 --- a/usage-examples/go/atlas-sdk-go/internal/auth/client.go +++ b/usage-examples/go/atlas-sdk-go/internal/auth/client.go @@ -11,20 +11,19 @@ import ( // NewClient initializes and returns an authenticated Atlas API client using OAuth2 with service account credentials (recommended) // See: https://www.mongodb.com/docs/atlas/architecture/current/auth/#service-accounts -func NewClient(cfg *config.Config, secrets *config.Secrets) (*admin.APIClient, error) { - if cfg == nil { - return nil, &errors.ValidationError{Message: "config cannot be nil"} +func NewClient(ctx context.Context, cfg config.Config, secrets config.Secrets) (*admin.APIClient, error) { + if cfg == (config.Config{}) { + return nil, &errors.ValidationError{Message: "config cannot be empty"} } - - if secrets == nil { + if secrets.ServiceAccountID() == "" || secrets.ServiceAccountSecret() == "" { return nil, &errors.ValidationError{Message: "secrets cannot be nil"} } - sdk, err := admin.NewClient( admin.UseBaseURL(cfg.BaseURL), - admin.UseOAuthAuth(context.Background(), - secrets.ServiceAccountID, - secrets.ServiceAccountSecret, + admin.UseOAuthAuth( + ctx, + secrets.ServiceAccountID(), + secrets.ServiceAccountSecret(), ), ) if err != nil { diff --git a/usage-examples/go/atlas-sdk-go/internal/auth/client_test.go b/usage-examples/go/atlas-sdk-go/internal/auth/client_test.go index d7adf44..39b7e3f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/auth/client_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/auth/client_test.go @@ -1,7 +1,7 @@ package auth_test import ( - "errors" + "context" "testing" "github.com/stretchr/testify/assert" @@ -14,13 +14,10 @@ import ( func TestNewClient_Success(t *testing.T) { t.Parallel() - cfg := &config.Config{BaseURL: "https://example.com"} - secrets := &config.Secrets{ - ServiceAccountID: "validID", - ServiceAccountSecret: "validSecret", - } + cfg := config.Config{BaseURL: "https://example.com"} + secrets := config.NewSecrets("validID", "validSecret") - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(context.Background(), cfg, secrets) require.NoError(t, err) require.NotNil(t, client) @@ -28,29 +25,32 @@ func TestNewClient_Success(t *testing.T) { func TestNewClient_returnsErrorWhenConfigIsNil(t *testing.T) { t.Parallel() - secrets := &config.Secrets{ - ServiceAccountID: "validID", - ServiceAccountSecret: "validSecret", - } + secrets := config.NewSecrets("validID", "validSecret") - client, err := auth.NewClient(nil, secrets) + // Zero value config + var cfg config.Config + + client, err := auth.NewClient(context.Background(), cfg, secrets) require.Error(t, err) require.Nil(t, client) var validationErr *internalerrors.ValidationError - require.True(t, errors.As(err, &validationErr), "expected error to be *errors.ValidationError") - assert.Equal(t, "config cannot be nil", validationErr.Message) + require.True(t, assert.ErrorAs(t, err, &validationErr), "expected error to be *errors.ValidationError") + assert.Equal(t, "config cannot be empty", validationErr.Message) } func TestNewClient_returnsErrorWhenSecretsAreNil(t *testing.T) { t.Parallel() - cfg := &config.Config{BaseURL: "https://example.com"} + cfg := config.Config{BaseURL: "https://example.com"} + + // Zero value secrets + var secrets config.Secrets - client, err := auth.NewClient(cfg, nil) + client, err := auth.NewClient(context.Background(), cfg, secrets) require.Error(t, err) require.Nil(t, client) var validationErr *internalerrors.ValidationError - require.True(t, errors.As(err, &validationErr), "expected error to be *errors.ValidationError") + require.True(t, assert.ErrorAs(t, err, &validationErr), "expected error to be *errors.ValidationError") assert.Equal(t, "secrets cannot be nil", validationErr.Message) } diff --git a/usage-examples/go/atlas-sdk-go/internal/billing/collector_test.go b/usage-examples/go/atlas-sdk-go/internal/billing/collector_test.go index b3aca90..cf073ef 100644 --- a/usage-examples/go/atlas-sdk-go/internal/billing/collector_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/billing/collector_test.go @@ -135,8 +135,8 @@ func TestCollectLineItemBillingData_NoInvoices(t *testing.T) { result, err := CollectLineItemBillingData(ctx, mockInvoiceSvc, mockOrgSvc, orgID, nil) // Assertions - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") + require.NoError(t, err) // Invoice lookup returning no results is not an error + require.Len(t, result, 0) assert.Nil(t, result) } diff --git a/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go new file mode 100644 index 0000000..6f1f51f --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go @@ -0,0 +1,71 @@ +package clusters + +import ( + "context" + "fmt" + + "atlas-sdk-go/internal/errors" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// ListClusterNames lists all clusters in a project and returns their names. +func ListClusterNames(ctx context.Context, sdk admin.ClustersApi, p *admin.ListClustersApiParams) ([]string, error) { + req := sdk.ListClusters(ctx, p.GroupId) + clusters, _, err := req.Execute() + if err != nil { + return nil, errors.FormatError("list clusters", p.GroupId, err) + } + + var names []string + if clusters != nil && clusters.Results != nil { + for _, cluster := range *clusters.Results { + if cluster.Name != nil { + names = append(names, *cluster.Name) + } + } + } + return names, nil +} + +// GetProcessIdForCluster retrieves the process ID for a given cluster +func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, + p *admin.ListAtlasProcessesApiParams, clusterName string) (string, error) { + + req := sdk.ListAtlasProcesses(ctx, p.GroupId) + r, _, err := req.Execute() + if err != nil { + return "", errors.FormatError("list atlas processes", p.GroupId, err) + } + if r == nil || !r.HasResults() || len(r.GetResults()) == 0 { + return "", nil + } + + // Find the process for the specified cluster + for _, process := range r.GetResults() { + hostName := process.GetUserAlias() + id := process.GetId() + if hostName != "" && hostName == clusterName { + if id != "" { + return id, nil + } + } + } + + return "", fmt.Errorf("no process found for cluster %s", clusterName) +} + +// GetClusterSRVConnectionString returns the standard SRV connection string for a cluster. +func GetClusterSRVConnectionString(ctx context.Context, client *admin.APIClient, projectID, clusterName string) (string, error) { + if client == nil { + return "", fmt.Errorf("nil atlas api client") + } + cluster, _, err := client.ClustersApi.GetCluster(ctx, projectID, clusterName).Execute() + if err != nil { + return "", errors.FormatError("get cluster", projectID, err) + } + if cluster == nil || cluster.ConnectionStrings == nil || cluster.ConnectionStrings.StandardSrv == nil { + return "", fmt.Errorf("no standard SRV connection string found for cluster %s", clusterName) + } + return *cluster.ConnectionStrings.StandardSrv, nil +} diff --git a/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go new file mode 100644 index 0000000..06b9e7f --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go @@ -0,0 +1,241 @@ +package clusters + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" +) + +func TestListClusterNames_Success(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + clusterName := "ClusterA" + + mockResponse := &admin.PaginatedClusterDescription20240805{ + Results: &[]admin.ClusterDescription20240805{ + { + Name: admin.PtrString("ClusterA"), + GroupId: &groupID, + }, + }, + } + + mockSvc := mockadmin.NewClustersApi(t) + mockSvc.EXPECT(). + ListClusters(mock.Anything, groupID). + Return(admin.ListClustersApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListClustersExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + params := &admin.ListClustersApiParams{GroupId: groupID} + result, err := ListClusterNames(ctx, mockSvc, params) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result, 1, "Should return one cluster name") + require.Equal(t, clusterName, result[0], "Cluster name should match expected") +} + +func TestListClusterNames_ApiError(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + + mockSvc := mockadmin.NewClustersApi(t) + mockSvc.EXPECT(). + ListClusters(mock.Anything, groupID). + Return(admin.ListClustersApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListClustersExecute(mock.Anything). + Return(nil, nil, assert.AnError).Once() + + params := &admin.ListClustersApiParams{GroupId: groupID} + result, err := ListClusterNames(ctx, mockSvc, params) + + require.Error(t, err) + require.Nil(t, result) + assert.Contains(t, err.Error(), "list clusters", "Should return formatted error when API call fails") +} + +func TestListClusterNames_NoClusters(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + + // Create empty response + mockResponse := &admin.PaginatedClusterDescription20240805{ + Results: &[]admin.ClusterDescription20240805{}, + } + + mockSvc := mockadmin.NewClustersApi(t) + mockSvc.EXPECT(). + ListClusters(mock.Anything, groupID). + Return(admin.ListClustersApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListClustersExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListClustersApiParams{GroupId: groupID} + result, err := ListClusterNames(ctx, mockSvc, params) + + require.Nil(t, result) + require.NoError(t, err, "No error should be returned when no clusters exist") +} + +func TestGetProcessIdForCluster_Success(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + clusterName := "host" + processID := "host:27017" + + mockResponse := &admin.PaginatedHostViewAtlas{ + Results: &[]admin.ApiHostViewAtlas{ + { + GroupId: admin.PtrString(groupID), + Id: admin.PtrString(processID), + UserAlias: admin.PtrString(clusterName), + }, + }, + } + + mockSvc := mockadmin.NewMonitoringAndLogsApi(t) + mockSvc.EXPECT(). + ListAtlasProcesses(mock.Anything, groupID). + Return(admin.ListAtlasProcessesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListAtlasProcessesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListAtlasProcessesApiParams{GroupId: groupID} + result, err := GetProcessIdForCluster(ctx, mockSvc, params, clusterName) + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, processID, result, "Process ID should match expected value") +} + +func TestGetProcessIdForCluster_ApiError(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + clusterName := "host" + + mockSvc := mockadmin.NewMonitoringAndLogsApi(t) + mockSvc.EXPECT(). + ListAtlasProcesses(mock.Anything, groupID). + Return(admin.ListAtlasProcessesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListAtlasProcessesExecute(mock.Anything). + Return(nil, nil, assert.AnError).Once() + + params := &admin.ListAtlasProcessesApiParams{GroupId: groupID} + result, err := GetProcessIdForCluster(ctx, mockSvc, params, clusterName) + + require.Error(t, err) + require.Emptyf(t, result, "Process ID should be empty on error") + assert.Contains(t, err.Error(), "list atlas processes", "Should return formatted error when API call fails") +} + +func TestGetProcessIdForCluster_NoProcesses(t *testing.T) { + t.Parallel() + ctx := context.Background() + groupID := "group123" + clusterName := "host" + + // Create empty response + mockResponse := &admin.PaginatedHostViewAtlas{ + Results: &[]admin.ApiHostViewAtlas{}, + } + + mockSvc := mockadmin.NewMonitoringAndLogsApi(t) + mockSvc.EXPECT(). + ListAtlasProcesses(mock.Anything, groupID). + Return(admin.ListAtlasProcessesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListAtlasProcessesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListAtlasProcessesApiParams{GroupId: groupID} + result, err := GetProcessIdForCluster(ctx, mockSvc, params, clusterName) + + require.Emptyf(t, result, "Process ID should be empty when no processes exist") + require.NoError(t, err, "No error should be returned when no processes exist") +} + +func newTestAtlasClient(t *testing.T, handler http.HandlerFunc) *admin.APIClient { + t.Helper() + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + client, err := admin.NewClient(admin.UseBaseURL(server.URL)) + require.NoError(t, err) + return client +} + +func TestGetClusterSRVConnectionString_Success(t *testing.T) { + t.Parallel() + projectID := "proj1" + clusterName := "Cluster0" + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "connectionStrings": { + "standardSrv": "mongodb+srv://cluster0.example.net" + } + }`)) + } + client := newTestAtlasClient(t, handler) + + srv, err := GetClusterSRVConnectionString(context.Background(), client, projectID, clusterName) + + require.NoError(t, err) + assert.Equal(t, "mongodb+srv://cluster0.example.net", srv) +} + +func TestGetClusterSRVConnectionString_MissingField(t *testing.T) { + t.Parallel() + projectID := "proj1" + clusterName := "Cluster0" + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // connectionStrings present but standardSrv missing + _, _ = w.Write([]byte(`{"connectionStrings": {}}`)) + } + client := newTestAtlasClient(t, handler) + + srv, err := GetClusterSRVConnectionString(context.Background(), client, projectID, clusterName) + + require.Error(t, err) + assert.Empty(t, srv) + assert.Contains(t, err.Error(), "no standard SRV") +} + +func TestGetClusterSRVConnectionString_ApiError(t *testing.T) { + t.Parallel() + projectID := "proj1" + clusterName := "Cluster0" + + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"detail": "server error"}`)) + } + client := newTestAtlasClient(t, handler) + + srv, err := GetClusterSRVConnectionString(context.Background(), client, projectID, clusterName) + + require.Error(t, err) + assert.Empty(t, srv) + assert.Contains(t, err.Error(), "get cluster") +} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go index d6b3110..3e0170a 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -1,20 +1,40 @@ package config import ( + "os" + "strings" + "atlas-sdk-go/internal/errors" ) -// LoadAll loads secrets and config from the specified paths -func LoadAll(configPath string) (*Secrets, *Config, error) { - s, err := LoadSecrets() - if err != nil { - return nil, nil, errors.WithContext(err, "loading secrets") +const defaultConfigPath = "configs/config.json" // Default path if not specified in environment + +// LoadAll loads secrets from .env and configuration from the specified config file. +// If the configPath is empty, it falls back to the default config path. +// It returns both Secrets and Config, or an error if either loading fails. +func LoadAll(configPath string) (Secrets, Config, error) { + if strings.TrimSpace(configPath) == "" { + configPath = defaultConfigPath + } + + if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { + return Secrets{}, Config{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} } - c, err := LoadConfig(configPath) + secrets, err := LoadSecrets() + if err != nil { + return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") + } + cfg, err := LoadConfig(configPath) if err != nil { - return nil, nil, errors.WithContext(err, "loading config") + return Secrets{}, Config{}, errors.WithContext(err, "loading config") } + return secrets, cfg, nil +} - return s, c, nil +// LoadAllFromEnv resolves the configuration path from the CONFIG_PATH environment variable +// and delegates to LoadAll. If CONFIG_PATH is empty, LoadAll will apply its default path. +func LoadAllFromEnv() (Secrets, Config, error) { + configPath := os.Getenv("CONFIG_PATH") + return LoadAll(configPath) } diff --git a/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go b/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go index c6621b9..efef784 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go @@ -8,19 +8,22 @@ import ( "atlas-sdk-go/internal/errors" ) +// Config holds the configuration for connecting to MongoDB Atlas type Config struct { BaseURL string `json:"MONGODB_ATLAS_BASE_URL"` OrgID string `json:"ATLAS_ORG_ID"` ProjectID string `json:"ATLAS_PROJECT_ID"` ClusterName string `json:"ATLAS_CLUSTER_NAME"` - HostName string + HostName string `json:"ATLAS_HOSTNAME"` ProcessID string `json:"ATLAS_PROCESS_ID"` } // LoadConfig reads a JSON configuration file and returns a Config struct -func LoadConfig(path string) (*Config, error) { +// It validates required fields and returns an error if any validation fails. +func LoadConfig(path string) (Config, error) { + var config Config if path == "" { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "configuration file path cannot be empty", } } @@ -28,23 +31,22 @@ func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - return nil, &errors.NotFoundError{Resource: "configuration file", ID: path} + return config, &errors.NotFoundError{Resource: "configuration file", ID: path} } - return nil, errors.WithContext(err, "reading configuration file") + return config, errors.WithContext(err, "reading configuration file") } - var config Config - if err := json.Unmarshal(data, &config); err != nil { - return nil, errors.WithContext(err, "parsing configuration file") + if err = json.Unmarshal(data, &config); err != nil { + return config, errors.WithContext(err, "parsing configuration file") } if config.OrgID == "" { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "organization ID is required in configuration", } } if config.ProjectID == "" { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "project ID is required in configuration", } } @@ -53,11 +55,15 @@ func LoadConfig(path string) (*Config, error) { if host, _, ok := strings.Cut(config.ProcessID, ":"); ok { config.HostName = host } else { - return nil, &errors.ValidationError{ + return config, &errors.ValidationError{ Message: "process ID must be in the format 'hostname:port'", } } } - return &config, nil + if config.BaseURL == "" { + config.BaseURL = "https://cloud.mongodb.com" // Default base URL if not provided + } + + return config, nil } diff --git a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go deleted file mode 100644 index 2d31065..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "os" - "strings" - - "atlas-sdk-go/internal/errors" -) - -const ( - EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" - EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" -) - -type Secrets struct { - ServiceAccountID string - ServiceAccountSecret string -} - -func LoadSecrets() (*Secrets, error) { - s := &Secrets{} - var missing []string - - look := func(key string, dest *string) { - if v, ok := os.LookupEnv(key); ok && v != "" { - *dest = v - } else { - missing = append(missing, key) - } - } - - look(EnvSAClientID, &s.ServiceAccountID) - look(EnvSAClientSecret, &s.ServiceAccountSecret) - - if len(missing) > 0 { - return nil, &errors.ValidationError{ - Message: "missing required environment variables: " + strings.Join(missing, ", "), - } - } - return s, nil -} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/loadsecrets.go b/usage-examples/go/atlas-sdk-go/internal/config/loadsecrets.go new file mode 100644 index 0000000..0778633 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadsecrets.go @@ -0,0 +1,61 @@ +package config + +import ( + "errors" + "fmt" + "os" +) + +const ( + envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" +) + +var errMissingEnv = errors.New("missing environment variable") + +// Secrets contains sensitive configuration loaded from environment variables +type Secrets struct { + serviceAccountID string + serviceAccountSecret string +} + +func (s Secrets) ServiceAccountID() string { + return s.serviceAccountID +} + +func (s Secrets) ServiceAccountSecret() string { + return s.serviceAccountSecret +} + +// LoadSecrets loads sensitive configuration from environment variables +// Returns error if any required environment variable is missing +func LoadSecrets() (Secrets, error) { + s := Secrets{} + var missing []string + + look := func(key string, dest *string) { + if v, ok := os.LookupEnv(key); ok && v != "" { + *dest = v + } else { + missing = append(missing, key) + } + } + + look(envServiceAccountID, &s.serviceAccountID) + look(envServiceAccountSecret, &s.serviceAccountSecret) + + if len(missing) > 0 { + return Secrets{}, fmt.Errorf("load secrets: %w (missing: %v)", errMissingEnv, missing) + } + return s, nil +} + +// :remove-start: + +// NewSecrets creates a new Secrets instance with the provided service account ID and secret +// Used for testing or to set secrets programmatically. +func NewSecrets(id, secret string) Secrets { + return Secrets{serviceAccountID: id, serviceAccountSecret: secret} +} + +// :remove-end: diff --git a/usage-examples/go/atlas-sdk-go/internal/data/export/formats_test.go b/usage-examples/go/atlas-sdk-go/internal/data/export/formats_test.go index e2ce586..4050bc8 100644 --- a/usage-examples/go/atlas-sdk-go/internal/data/export/formats_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/data/export/formats_test.go @@ -23,7 +23,7 @@ func TestToJSON(t *testing.T) { t.Run("Successfully writes JSON to file", func(t *testing.T) { tempDir := t.TempDir() - filePath := filepath.Join(tempDir, "test.json") + filePath := filepath.Join(tempDir, "config.test.json") testData := TestStruct{ID: 1, Name: "Test", Value: 99.99} err := ToJSON(testData, filePath) @@ -42,7 +42,7 @@ func TestToJSON(t *testing.T) { }) t.Run("Returns error for nil data", func(t *testing.T) { - err := ToJSON(nil, "test.json") + err := ToJSON(nil, "config.test.json") require.Error(t, err) assert.Contains(t, err.Error(), "data cannot be nil") }) @@ -56,7 +56,7 @@ func TestToJSON(t *testing.T) { t.Run("Creates directory structure if needed", func(t *testing.T) { tempDir := t.TempDir() dirPath := filepath.Join(tempDir, "subdir1", "subdir2") - filePath := filepath.Join(dirPath, "test.json") + filePath := filepath.Join(dirPath, "config.test.json") testData := TestStruct{ID: 1, Name: "Test", Value: 99.99} err := ToJSON(testData, filePath) diff --git a/usage-examples/go/atlas-sdk-go/internal/errors/utils.go b/usage-examples/go/atlas-sdk-go/internal/errors/utils.go index ca8130b..fd79f3c 100644 --- a/usage-examples/go/atlas-sdk-go/internal/errors/utils.go +++ b/usage-examples/go/atlas-sdk-go/internal/errors/utils.go @@ -2,7 +2,6 @@ package errors import ( "fmt" - "log" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -40,9 +39,3 @@ type NotFoundError struct { func (e *NotFoundError) Error() string { return fmt.Sprintf("resource not found: %s [%s]", e.Resource, e.ID) } - -// ExitWithError prints an error message with context and exits the program -func ExitWithError(context string, err error) { - log.Fatalf("%s: %v", context, err) - // Note: log.Fatalf calls os.Exit(1) -} diff --git a/usage-examples/go/atlas-sdk-go/internal/fileutils/env.go b/usage-examples/go/atlas-sdk-go/internal/fileutils/env.go new file mode 100644 index 0000000..e77e8ab --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/fileutils/env.go @@ -0,0 +1,23 @@ +package fileutils + +import ( + "os" + "path/filepath" +) + +// DownloadsBaseDir returns the base directory for downloads as specified by the +// ATLAS_DOWNLOADS_DIR environment variable. If the variable is not set, it +// returns an empty string. +func DownloadsBaseDir() string { + return os.Getenv("ATLAS_DOWNLOADS_DIR") +} + +// ResolveWithDownloadsBase returns dir prefixed with the global downloads base +// directory when ATLAS_DOWNLOADS_DIR is set; otherwise returns dir as-is. +func ResolveWithDownloadsBase(dir string) string { + base := DownloadsBaseDir() + if base == "" { + return dir + } + return filepath.Join(base, dir) +} diff --git a/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go b/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go index 3e04d27..64de73c 100644 --- a/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go +++ b/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go @@ -52,13 +52,10 @@ func SafeCopy(dst io.Writer, src io.Reader) error { // :remove-start: // SafeDelete removes files generated in the specified directory -// NOTE: INTERNAL ONLY FUNCTION; before running `bluehawk.sh`, ensure this this and "path/filepath" import are marked for removal +// NOTE: INTERNAL ONLY FUNCTION; before running `bluehawk.sh`, ensure that this and "path/filepath" import are marked for removal func SafeDelete(dir string) error { // Check for global downloads directory - defaultDir := os.Getenv("ATLAS_DOWNLOADS_DIR") - if defaultDir != "" { - dir = filepath.Join(defaultDir, dir) - } + dir = ResolveWithDownloadsBase(dir) // Check if directory exists before attempting to walk it if _, err := os.Stat(dir); os.IsNotExist(err) { return errors.WithContext(err, "directory does not exist") diff --git a/usage-examples/go/atlas-sdk-go/internal/fileutils/path.go b/usage-examples/go/atlas-sdk-go/internal/fileutils/path.go index 3e3e6a8..2a9605a 100644 --- a/usage-examples/go/atlas-sdk-go/internal/fileutils/path.go +++ b/usage-examples/go/atlas-sdk-go/internal/fileutils/path.go @@ -13,10 +13,7 @@ import ( // NOTE: You can define a default global directory for all generated files by setting the ATLAS_DOWNLOADS_DIR environment variable. func GenerateOutputPath(dir, prefix, extension string) (string, error) { // If default download directory is set in .env, prepend it to the provided dir - defaultDir := os.Getenv("ATLAS_DOWNLOADS_DIR") - if defaultDir != "" { - dir = filepath.Join(defaultDir, dir) - } + dir = ResolveWithDownloadsBase(dir) // Create directory if it doesn't exist if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/usage-examples/go/atlas-sdk-go/internal/metrics/disk_test.go b/usage-examples/go/atlas-sdk-go/internal/metrics/disk_test.go index f89fc5f..177b9bf 100644 --- a/usage-examples/go/atlas-sdk-go/internal/metrics/disk_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/metrics/disk_test.go @@ -4,12 +4,24 @@ import ( "context" "testing" + "time" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.mongodb.org/atlas-sdk/v20250219001/admin" "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" ) +// Fixed timestamp for testing +const fixedTS = "2023-04-01T12:00:00Z" + +// parseTS parses a timestamp string into a time.Time object +func parseTS(t *testing.T, ts string) time.Time { + parsed, err := time.Parse(time.RFC3339, ts) + require.NoError(t, err) + return parsed +} + // ----------------------------------------------------------------------------- // Integration tests against test HTTP server // ----------------------------------------------------------------------------- diff --git a/usage-examples/go/atlas-sdk-go/internal/metrics/process_test.go b/usage-examples/go/atlas-sdk-go/internal/metrics/process_test.go index bca7b79..842ae52 100644 --- a/usage-examples/go/atlas-sdk-go/internal/metrics/process_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/metrics/process_test.go @@ -3,7 +3,6 @@ package metrics import ( "context" "testing" - "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -11,17 +10,6 @@ import ( "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" ) -// fixed timestamp for tests -var fixedTS = "2025-01-01T12:00:00Z" - -// parseTS wraps time.Parse and flags error on test failure -func parseTS(t *testing.T, ts string) time.Time { - t.Helper() - parsed, err := time.Parse(time.RFC3339, ts) - require.NoError(t, err) - return parsed -} - // ----------------------------------------------------------------------------- // Integration tests against test HTTP server // ----------------------------------------------------------------------------- diff --git a/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh b/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh index b103389..623bfd5 100755 --- a/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh +++ b/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh @@ -23,6 +23,7 @@ STATE="" IGNORE_PATTERNS=( "internal_*.*" # for INTERNAL_README.md "scripts/" + "tmp/" ".idea" "*_test.go" # we're not including test files in artifact repo ".env" @@ -125,6 +126,20 @@ if [[ "$CMD" == "copy" ]] && [[ ${#RENAME_PATTERNS[@]} -gt 0 ]]; then done fi +# Before running Bluehawk, format the Go module to keep examples tidy +# - goimports fixes imports and grouping (using local prefix atlas-sdk-go/) +# - go fmt formats the code +echo "Formatting Go code (goimports, go fmt) in $INPUT_DIR ..." +( + cd "$INPUT_DIR" + if ! command -v goimports >/dev/null 2>&1; then + echo "Error: goimports is not installed. Install with: go install golang.org/x/tools/cmd/goimports@latest" >&2 + exit 1 + fi + goimports -w -local atlas-sdk-go/ . + go fmt ./... +) + # Check for errors first echo "Checking for Bluehawk parsing errors..." if ! check_output=$(bluehawk check "${IGNORE_ARGS[@]}" "$INPUT_DIR" 2>&1); then