From 9c9b69ebbaa50fea03a7ae87261d97830a828afc Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 6 Aug 2025 08:59:02 -0400 Subject: [PATCH 01/19] update existing to use new load function and add archiving example for scalability page --- usage-examples/go/atlas-sdk-go/.gitignore | 9 + .../configs/config.development.json | 12 + .../{config.json => config.example.json} | 0 .../configs/config.production.json | 12 + .../examples/billing/historical/main.go | 9 +- .../examples/billing/line_items/main.go | 24 +- .../examples/billing/linked_orgs/main.go | 15 +- .../examples/monitoring/logs/main.go | 8 +- .../examples/monitoring/metrics_disk/main.go | 17 +- .../monitoring/metrics_process/main.go | 13 +- .../examples/performance/archiving/main.go | 89 ++++ .../examples/performance/scaling/main.go | 261 +++++++++++ usage-examples/go/atlas-sdk-go/go.mod | 5 +- .../atlas-sdk-go/internal/archive/analyze.go | 298 +++++++++++++ .../internal/archive/analyze_test.go | 409 ++++++++++++++++++ .../internal/config/appcontext.go | 357 +++++++++++++++ .../atlas-sdk-go/internal/config/loadall.go | 27 +- .../internal/config/loadconfig.go | 16 + .../atlas-sdk-go/internal/config/loadenv.go | 5 - .../go/atlas-sdk-go/internal/config/utils.go | 1 + 20 files changed, 1528 insertions(+), 59 deletions(-) create mode 100644 usage-examples/go/atlas-sdk-go/configs/config.development.json rename usage-examples/go/atlas-sdk-go/configs/{config.json => config.example.json} (100%) create mode 100644 usage-examples/go/atlas-sdk-go/configs/config.production.json create mode 100644 usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go create mode 100644 usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/archive/analyze.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/config/appcontext.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/config/utils.go diff --git a/usage-examples/go/atlas-sdk-go/.gitignore b/usage-examples/go/atlas-sdk-go/.gitignore index 22cadb7..7ade94e 100644 --- a/usage-examples/go/atlas-sdk-go/.gitignore +++ b/usage-examples/go/atlas-sdk-go/.gitignore @@ -1,5 +1,14 @@ # Secrets .env +!.env.example +.env.development +.env.production +configs/config.example.json +!configs/config.development.json +!configs/config.production.json + +tmp + # Logs *.log diff --git a/usage-examples/go/atlas-sdk-go/configs/config.development.json b/usage-examples/go/atlas-sdk-go/configs/config.development.json new file mode 100644 index 0000000..dc9dd77 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/configs/config.development.json @@ -0,0 +1,12 @@ +{ + "ENVIRONMENT": "dev", + "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", + "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", + "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", + "ATLAS_PROJECT_NAME": "Customer Portal - Dev", + "ATLAS_PROCESS_ID": "CustomerPortalDev-shard-00-00.ajlj3.mongodb.net:27017", + "CLOUD_PROVIDER": "AWS", + "AUTO_SCALING_DISK_GB": true, + "AUTO_SCALING_COMPUTE": true, + "DISK_SIZE_GB": 10000 +} 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/configs/config.production.json b/usage-examples/go/atlas-sdk-go/configs/config.production.json new file mode 100644 index 0000000..3ffad1e --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/configs/config.production.json @@ -0,0 +1,12 @@ +{ + "ENVIRONMENT": "prod", + "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", + "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", + "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", + "ATLAS_PROJECT_NAME": "Customer Portal - Prod", + "ATLAS_PROCESS_ID": "CustomerPortalProd-shard-00-00.ajlj3.mongodb.net:27017", + "CLOUD_PROVIDER": "AWS", + "AUTO_SCALING_DISK_GB": true, + "AUTO_SCALING_COMPUTE": true, + "DISK_SIZE_GB": 40000 +} 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..0627c11 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 @@ -17,16 +17,13 @@ import ( "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..c35a690 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 @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "time" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -17,28 +18,25 @@ import ( "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) - } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - secrets, cfg, err := config.LoadAll("configs/config.json") + // Use the context-aware version + appCtx, err := config.LoadAppContextWithContext(ctx, "", false) if err != nil { errors.ExitWithError("Failed to load configuration", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) if err != nil { errors.ExitWithError("Failed to initialize authentication client", err) } - ctx := context.Background() p := &admin.ListInvoicesApiParams{ - OrgId: cfg.OrgID, + OrgId: appCtx.Config.OrgID, } fmt.Printf("Fetching pending invoices for organization: %s\n", p.OrgId) @@ -122,3 +120,11 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { // Exported billing data to invoices/pending_5f7a9ec7d78fc03b42959328.json // Exported billing data to invoices/pending_5f7a9ec7d78fc03b42959328.csv // :state-remove-end: [copy] +// With timeout +func loadConfigWithTimeout() (*config.AppContext, error) { + // Create a context with a 5-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return config.LoadAppContextWithContext(ctx, "", false) +} 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..ff8f044 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 @@ -5,25 +5,20 @@ package main import ( - "context" - "fmt" - "log" - "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" + "context" + "fmt" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..d8be97c 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 @@ -20,11 +20,9 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..03babe6 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 @@ -5,26 +5,21 @@ package main import ( - "context" - "encoding/json" - "fmt" - "log" - "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" + "context" + "encoding/json" + "fmt" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "development" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..0fce6da 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 @@ -5,28 +5,23 @@ package main import ( + "atlas-sdk-go/internal/errors" "context" "encoding/json" "fmt" - "log" - - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "production" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..326d5e2 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + "atlas-sdk-go/internal/errors" + "context" + "log" + "time" +) + +// This program demonstrates an automated approach to: +// 1. Discover all clusters in an Atlas project +// 2. Analyze collections within each cluster for archiving candidates +// 3. Configure Online Archive for eligible collections +// +// In a production scenario, you would customize the collection analysis +// logic in CollectionsForArchiving() to match your specific data patterns. +func main() { + // Set up context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Load application context with configuration + appCtx, err := config.LoadAppContextWithContext(ctx, "internal", false) + if err != nil { + errors.ExitWithError("Failed to load configuration", err) + } + + // Initialize the Atlas API client + client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + if err != nil { + errors.ExitWithError("Failed to initialize Atlas client", err) + } + + // Get the project ID from configuration + projectID := appCtx.Config.ProjectID + if projectID == "" { + errors.ExitWithError("Project ID not found in configuration", nil) + } + + log.Println("Starting archive analysis for project:", projectID) + + // Step 1: List all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + errors.ExitWithError("Failed to list clusters", err) + } + + log.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + + // Step 2: Process each cluster + failedArchives := 0 + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + log.Printf("Analyzing cluster: %s", clusterName) + + // Step 3: Find collections suitable for archiving + // Note: Partition fields are ordered by query frequency - most frequently + // queried field should be first for optimal query performance against + // archived data. This significantly impacts cost and performance. + candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + log.Printf("Found %d collections eligible for archiving in cluster %s", + len(candidates), clusterName) + + // Step 4: Configure online archive for each candidate collection + for _, candidate := range candidates { + log.Printf("Configuring archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) + if configureErr != nil { + log.Printf("Failed to configure archive: %v", configureErr) + failedArchives++ + continue + } + + log.Printf("Successfully configured online archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if failedArchives > 0 { + log.Printf("Warning: %d archive configurations failed", failedArchives) + } + + log.Println("Archive analysis and configuration completed") +} diff --git a/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go b/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go new file mode 100644 index 0000000..4dac64b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go @@ -0,0 +1,261 @@ +package main + +import ( + "atlas-sdk-go/internal/clusters" + "atlas-sdk-go/internal/scale" + "context" + "fmt" + "log" + "time" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + "atlas-sdk-go/internal/errors" +) + +// Constants for the scaling thresholds and instance sizes +const ( + currentInstanceSize = "M30" + targetInstanceSize = "M40" + cpuMonitoringPeriod = "P1D" // Look at last 24 hours of CPU data + scaleUpThreshold = 70.0 // Scale up if CPU utilization is above 70% + scaleDownThreshold = 30.0 // Scale down if CPU utilization is below 30% +) + +// CPUMetrics represents CPU utilization metrics for a cluster +type CPUMetrics struct { + AverageCPUUsage float64 + MaxCPUUsage float64 + SampleCount int +} + +// CPUThresholds defines the thresholds for scaling decisions +type CPUThresholds struct { + ScaleUpThreshold float64 + ScaleDownThreshold float64 +} + +// ScalingDecision represents a decision on whether to scale a cluster +type ScalingDecision struct { + ShouldScale bool + Direction string + Reason string +} + +func main() { + // Set up context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Load application context with configuration + appCtx, err := config.LoadAppContextWithContext(ctx, "internal", false) + if err != nil { + errors.ExitWithError("Failed to load configuration", err) + } + + // Initialize the Atlas API client + client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + if err != nil { + errors.ExitWithError("Failed to initialize Atlas client", err) + } + // Get the project ID from configuration + projectId := appCtx.Config.ProjectID + if projectId == "" { + errors.ExitWithError("Project ID not found in configuration", nil) + } + + // Set up CPU thresholds based on scalability recommendations + cpuThresholds := CPUThresholds{ + ScaleUpThreshold: scaleUpThreshold, + ScaleDownThreshold: scaleDownThreshold, + } + log.Printf("Using CPU thresholds - Scale up: %.1f%%, Scale down: %.1f%%", + cpuThresholds.ScaleUpThreshold, cpuThresholds.ScaleDownThreshold) + + // Get list of all clusters in the project + clusterNames, err = clusters.ListClusterNames(ctx, client, projectId) + if err != nil { + errors.ExitWithError("Failed to list clusters", err) + } + + if len(clusterNames) == 0 { + fmt.Printf("No clusters found for the project ID: %s\n", projectId) + return + } + + // Evaluate each cluster's details to determine eligibility for scaling + for _, clusterName := range clusterNames { + clusterDetails, resp, err := client.ClustersApi.GetCluster(ctx, projectId, clusterName).Execute() + if err != nil { + log.Printf("Error getting details for cluster %s: %v", clusterName, err) + continue + } + + // Check if the cluster matches the target instance size + if !isEligibleForScaling(clusterDetails, currentInstanceSize) { + log.Printf("Cluster %s instance size doesn't match criteria (%s), skipping", + clusterName, currentInstanceSize) + continue + } + + // Get CPU metrics for the cluster + processId, err := clusters.GetProcessIdForCluster(ctx, client, projectId, clusterName) + if err != nil { + log.Printf("Could not get process ID for cluster %s: %v", clusterName, err) + continue + } + + cpuMetrics, err := getClusterCPUMetrics(ctx, client, projectId, processId, cpuMonitoringPeriod) + if err != nil { + log.Printf("Could not fetch CPU metrics for cluster %s: %v", clusterName, err) + continue + } + + // Evaluate scaling decision based on CPU usage + scalingDecision := evaluateCPUBasedScaling(cpuMetrics, cpuThresholds) + + log.Printf("Cluster %s - CPU: avg=%.2f%%, max=%.2f%%, samples=%d", + clusterName, cpuMetrics.AverageCPUUsage, cpuMetrics.MaxCPUUsage, cpuMetrics.SampleCount) + log.Printf("Scaling decision: %s", scalingDecision.Reason) + + // Perform scaling if needed + if scalingDecision.ShouldScale && scalingDecision.Direction == "up" { + log.Printf("Scaling cluster %s UP from %s to %s due to high CPU usage", + clusterName, currentInstanceSize, targetInstanceSize) + err := scale.UpdateClusterSize(ctx, client, projectId, clusterName, clusterDetails, targetInstanceSize) + if err != nil { + log.Printf("Error during scaling: %v", err) + } + } else if scalingDecision.ShouldScale && scalingDecision.Direction == "down" { + // Define a smaller instance size for scale down + scaleDownSize := getScaleDownSize(currentInstanceSize) + log.Printf("Scaling cluster %s DOWN from %s to %s due to low CPU usage", + clusterName, currentInstanceSize, scaleDownSize) + err := scale.UpdateClusterSize(ctx, client, projectId, clusterName, clusterDetails, scaleDownSize) + if err != nil { + log.Printf("Error during scaling: %v", err) + } + } else { + log.Printf("No scaling needed for cluster %s", clusterName) + } + } + + log.Println("Cluster scaling process completed successfully.") +} + +func getClusterCPUMetrics(ctx context.Context, client *admin.APIClient, projectID, processID, period string) (CPUMetrics, error) { + // Configure time window for metrics + end := time.Now().UTC() + start := end.Add(-24 * time.Hour) // Default to 1 day + granularity := "PT1H" // 1-hour granularity + + startStr := start.Format(time.RFC3339) + endStr := end.Format(time.RFC3339) + + request := client.MonitoringAndLogsApi.GetProcessMeasurements(ctx, projectID, processID) + request = request.M("CPU_USAGE") + request = request.Granularity(granularity) + request = request.Period(period) + request = request.Start(startStr) + request = request.End(endStr) + + metrics, httpResp, err := request.Execute() + if err != nil { + return CPUMetrics{}, fmt.Errorf("failed to get CPU metrics: %w", err) + } + + var totalCPU float64 + var maxCPU float64 + var sampleCount int + + // Calculate average and max CPU usage + if metrics.Measurements != nil { + for _, measurement := range *metrics.Measurements { + if measurement.DataPoints != nil { + for _, dataPoint := range *measurement.DataPoints { + if dataPoint.Value != nil { + cpuValue := *dataPoint.Value + totalCPU += cpuValue + if cpuValue > maxCPU { + maxCPU = cpuValue + } + sampleCount++ + } + } + } + } + } + + if sampleCount == 0 { + return CPUMetrics{}, fmt.Errorf("no CPU metrics available") + } + + return CPUMetrics{ + AverageCPUUsage: totalCPU / float64(sampleCount), + MaxCPUUsage: maxCPU, + SampleCount: sampleCount, + }, nil +} + +func evaluateCPUBasedScaling(metrics CPUMetrics, thresholds CPUThresholds) ScalingDecision { + if metrics.AverageCPUUsage > thresholds.ScaleUpThreshold { + return ScalingDecision{ + ShouldScale: true, + Direction: "up", + Reason: fmt.Sprintf("Average CPU usage (%.2f%%) exceeds scale-up threshold (%.2f%%)", + metrics.AverageCPUUsage, thresholds.ScaleUpThreshold), + } + } + + if metrics.AverageCPUUsage < thresholds.ScaleDownThreshold { + return ScalingDecision{ + ShouldScale: true, + Direction: "down", + Reason: fmt.Sprintf("Average CPU usage (%.2f%%) is below scale-down threshold (%.2f%%)", + metrics.AverageCPUUsage, thresholds.ScaleDownThreshold), + } + } + + return ScalingDecision{ + ShouldScale: false, + Direction: "", + Reason: fmt.Sprintf("CPU usage (%.2f%%) is within normal range (%.2f%% - %.2f%%)", + metrics.AverageCPUUsage, thresholds.ScaleDownThreshold, thresholds.ScaleUpThreshold), + } +} + +func isEligibleForScaling(cluster *admin.ClusterDescription, currentSize string) bool { + if cluster.ReplicationSpecs == nil || len(*cluster.ReplicationSpecs) == 0 { + return false + } + + replicationSpec := (*cluster.ReplicationSpecs)[0] + if replicationSpec.RegionConfigs == nil || len(*replicationSpec.RegionConfigs) == 0 { + return false + } + + regionConfig := (*replicationSpec.RegionConfigs)[0] + if regionConfig.ElectableSpecs == nil || regionConfig.ElectableSpecs.InstanceSize == nil { + return false + } + + return *regionConfig.ElectableSpecs.InstanceSize == currentSize +} + +func getScaleDownSize(currentSize string) string { + scaleDownMap := map[string]string{ + "M40": "M30", + "M30": "M20", + "M20": "M10", + "M50": "M40", + "M60": "M50", + "M80": "M60", + } + + if downSize, exists := scaleDownMap[currentSize]; exists { + return downSize + } + return currentSize +} diff --git a/usage-examples/go/atlas-sdk-go/go.mod b/usage-examples/go/atlas-sdk-go/go.mod index fd4200e..0c084dd 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 @@ -14,8 +15,8 @@ require ( 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 ) 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..b4e653e --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -0,0 +1,298 @@ +package archive + +import ( + "atlas-sdk-go/internal/errors" + "context" + "fmt" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// 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 provides sensible defaults for archiving +func DefaultOptions() Options { + return Options{ + DefaultRetentionMultiplier: 2, + MinimumRetentionDays: 30, + EnableDataExpiration: true, + ArchiveSchedule: "DAILY", + } +} + +// CollectionsForArchiving identifies collections suitable for archiving based on data patterns +// func CollectionsForArchivingFull(ctx context.Context, sdk *admin.APIClient, +// +// projectID, clusterName string) ([]Candidate, error) { +// +// // Get all databases in the cluster +// databases, err := listDatabases(ctx, sdk, projectID, clusterName) +// if err != nil { +// return nil, errors.FormatError("list databases", err) +// } +// +// var candidates []Candidate +// +// // For each database, analyze collections +// for _, dbName := range databases { +// // Skip system databases +// if dbName == "admin" || dbName == "local" || dbName == "config" { +// continue +// } +// +// collections, err := listCollections(ctx, sdk, projectID, clusterName, dbName) +// if err != nil { +// log.Printf("Error listing collections for %s: %v", dbName, err) +// continue +// } +// +// for _, collName := range collections { +// // Get collection stats and metadata +// stats, err := getCollectionStats(ctx, sdk, projectID, clusterName, dbName, collName) +// if err != nil { +// log.Printf("Error getting stats for %s.%s: %v", dbName, collName, err) +// continue +// } +// +// // Skip collections smaller than threshold (e.g., 1GB) +// if stats.Size < 1_000_000_000 { +// continue +// } +// +// // Analyze data age distribution +// dateField, dateFormat, err := identifyDateField(ctx, sdk, projectID, clusterName, dbName, collName) +// if err != nil || dateField == "" { +// log.Printf("No suitable date field found in %s.%s", dbName, collName) +// continue +// } +// +// // Calculate appropriate retention period based on data distribution +// retentionDays := calculateRetentionDays(stats.AgeDistribution) +// +// // Identify optimal partition fields based on index usage statistics +// partitionFields := identifyPartitionFields(ctx, sdk, projectID, clusterName, dbName, collName) +// +// // Create candidate if it meets minimum requirements +// if retentionDays >= 30 && len(partitionFields) > 0 { +// candidates = append(candidates, Candidate{ +// DatabaseName: dbName, +// CollectionName: collName, +// DateField: dateField, +// DateFormat: dateFormat, +// RetentionDays: retentionDays, +// PartitionFields: partitionFields, +// }) +// } +// } +// } +// +// return candidates, nil +// } +// +// // Helper functions would include: +// +// func listDatabases(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) ([]string, error) { +// // Use Atlas API or direct MongoDB connection to list databases +// // ... +// } +// +// func listCollections(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName string) ([]string, error) { +// // Use Atlas API or direct MongoDB connection to list collections +// // ... +// } +// +// func getCollectionStats(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) (*CollectionStats, error) { +// // Get collection statistics including size, document count, etc. +// // ... +// } +// +// func identifyDateField(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) (string, string, error) { +// // Sample documents to identify fields with date values +// // Determine the format (ISO date, epoch timestamp, etc.) +// // ... +// } +// +// func calculateRetentionDays(ageDistribution map[string]float64) int { +// // Analyze age distribution to determine optimal retention period +// // Balance between keeping recent data in live collection and archiving older data +// // ... +// } +// +// func identifyPartitionFields(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) []string { +// // Analyze index usage statistics to determine most frequently queried fields +// // Review existing indexes to understand query patterns +// // ... +// } +// +// CollectionsForArchiving Simplified function to identify collections suitable for archiving +// In a real implementation, you would analyze collection data patterns +func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string) []Candidate { + + // This would normally analyze collection data patterns + // Discovers all databases and collections in the cluster + // Analyzes collection statistics (size, document count, growth rate) + // Identifies date fields for time-based archiving + // Calculates appropriate retention periods based on data age distribution + // Determines optimal partition fields based on query patterns + // Returns only collections that meet minimum size and access pattern requirements for archiving + + // For demo purposes, we'll return some example candidates + return []Candidate{ + { + DatabaseName: "sample_analytics", + CollectionName: "transactions", + DateField: "transaction_date", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"customer_id", "merchant"}, + }, + { + DatabaseName: "sample_logs", + CollectionName: "application_logs", + DateField: "timestamp", + DateFormat: "EPOCH_MILLIS", + RetentionDays: 30, + PartitionFields: []string{"service_name", "log_level"}, + }, + } +} + +type ExpireAfterDays struct { + // This struct can be extended to include more complex rules if needed + // For now, it serves as a placeholder for the data expiration rule + ExpireAfterDays int `json:"expireAfterDays,omitempty"` +} + +// ValidateCandidate ensures the archiving candidate meets requirements +func ValidateCandidate(candidate Candidate, opts Options) error { + // Validate required fields + if candidate.DatabaseName == "" || candidate.CollectionName == "" { + return fmt.Errorf("database name and collection name are required") + } + + // Validate retention days + if candidate.RetentionDays < opts.MinimumRetentionDays { + return fmt.Errorf("retention days must be at least %d", opts.MinimumRetentionDays) + } + + // Validate partition fields + 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 != "" { + // Validate date format + 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 +} + +// ConfigureOnlineArchive configures online archive for a collection +func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string, candidate Candidate) error { + + // Use default options if not specified + opts := DefaultOptions() + + // Validate the candidate + if err := ValidateCandidate(candidate, opts); err != nil { + return errors.FormatError("validate archive candidate", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + + // 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/analyze_test.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go new file mode 100644 index 0000000..4c41e96 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go @@ -0,0 +1,409 @@ +package archive + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +func TestDefaultOptions_ReturnsExpectedDefaults(t *testing.T) { + t.Parallel() + + opts := DefaultOptions() + + assert.Equal(t, 2, opts.DefaultRetentionMultiplier) + assert.Equal(t, 30, opts.MinimumRetentionDays) + assert.True(t, opts.EnableDataExpiration) + assert.Equal(t, "DAILY", opts.ArchiveSchedule) +} + +func TestCollectionsForArchiving_ReturnsExpectedCandidates(t *testing.T) { + t.Parallel() + ctx := context.Background() + var client *admin.APIClient + + candidates := CollectionsForArchiving(ctx, client, "project123", "cluster456") + + require.Len(t, candidates, 2) + + analyticsCandidate := candidates[0] + assert.Equal(t, "sample_analytics", analyticsCandidate.DatabaseName) + assert.Equal(t, "transactions", analyticsCandidate.CollectionName) + assert.Equal(t, "transaction_date", analyticsCandidate.DateField) + assert.Equal(t, "DATE", analyticsCandidate.DateFormat) + assert.Equal(t, 90, analyticsCandidate.RetentionDays) + assert.Equal(t, []string{"customer_id", "merchant"}, analyticsCandidate.PartitionFields) + + logsCandidate := candidates[1] + assert.Equal(t, "sample_logs", logsCandidate.DatabaseName) + assert.Equal(t, "application_logs", logsCandidate.CollectionName) + assert.Equal(t, "timestamp", logsCandidate.DateField) + assert.Equal(t, "EPOCH_MILLIS", logsCandidate.DateFormat) + assert.Equal(t, 30, logsCandidate.RetentionDays) + assert.Equal(t, []string{"service_name", "log_level"}, logsCandidate.PartitionFields) +} + +func TestCollectionsForArchiving_HandlesNilClientGracefully(t *testing.T) { + t.Parallel() + ctx := context.Background() + + candidates := CollectionsForArchiving(ctx, nil, "project123", "cluster456") + + assert.Len(t, candidates, 2) +} + +func TestCollectionsForArchiving_HandlesEmptyProjectIDAndClusterName(t *testing.T) { + t.Parallel() + ctx := context.Background() + var client *admin.APIClient + + candidates := CollectionsForArchiving(ctx, client, "", "") + + assert.Len(t, candidates, 2) +} + +func TestValidateCandidate_SucceedsWithValidCandidate(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"created_at", "user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_FailsWhenDatabaseNameIsEmpty(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "database name and collection name are required") +} + +func TestValidateCandidate_FailsWhenCollectionNameIsEmpty(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "database name and collection name are required") +} + +func TestValidateCandidate_FailsWhenBothDatabaseAndCollectionNamesAreEmpty(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "", + CollectionName: "", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "database name and collection name are required") +} + +func TestValidateCandidate_FailsWhenRetentionDaysBelowMinimum(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 15, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "retention days must be at least 30") +} + +func TestValidateCandidate_FailsWhenRetentionDaysEqualsZero(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 0, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "retention days must be at least 30") +} + +func TestValidateCandidate_FailsWhenPartitionFieldsAreEmpty(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "at least one partition field is required") +} + +func TestValidateCandidate_FailsWhenPartitionFieldsAreNil(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: nil, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "at least one partition field is required") +} + +func TestValidateCandidate_SucceedsWithCustomMinimumRetentionDays(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 45, + PartitionFields: []string{"created_at", "user_id"}, + } + opts := Options{ + MinimumRetentionDays: 40, + } + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_FailsWhenRetentionDaysBelowCustomMinimum(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 35, + PartitionFields: []string{"user_id"}, + } + opts := Options{ + MinimumRetentionDays: 40, + } + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "retention days must be at least 40") +} + +func TestValidateCandidate_SucceedsWithMultiplePartitionFields(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"created_at", "user_id", "tenant_id", "category"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_SucceedsWithEpochMillisDateFormat(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "timestamp", + DateFormat: "EPOCH_MILLIS", + RetentionDays: 90, + PartitionFields: []string{"timestamp", "service_name"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_SucceedsWhenRetentionDaysEqualsMinimum(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 30, + PartitionFields: []string{"created_at", "user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_FailsWhenDateFieldNotInPartitionFields(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"user_id", "tenant_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "date field created_at must be included in partition fields") +} + +func TestValidateCandidate_FailsWithInvalidDateFormat(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "INVALID_FORMAT", + RetentionDays: 60, + PartitionFields: []string{"created_at", "user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid date format: INVALID_FORMAT") +} + +func TestValidateCandidate_SucceedsWithValidDateFormats(t *testing.T) { + t.Parallel() + validFormats := []string{"DATE", "EPOCH_SECONDS", "EPOCH_MILLIS", "EPOCH_NANOSECONDS", "OBJECTID"} + + for _, format := range validFormats { + t.Run("format_"+format, func(t *testing.T) { + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "timestamp", + DateFormat: format, + RetentionDays: 60, + PartitionFields: []string{"timestamp", "user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) + }) + } +} + +func TestValidateCandidate_SucceedsWhenDateFieldIsEmpty(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "", + DateFormat: "", + RetentionDays: 60, + PartitionFields: []string{"user_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_SucceedsWhenDateFieldIsFirstInPartitionFields(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"created_at", "user_id", "tenant_id"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} + +func TestValidateCandidate_SucceedsWhenDateFieldIsLastInPartitionFields(t *testing.T) { + t.Parallel() + candidate := Candidate{ + DatabaseName: "testdb", + CollectionName: "testcoll", + DateField: "created_at", + DateFormat: "DATE", + RetentionDays: 60, + PartitionFields: []string{"user_id", "tenant_id", "created_at"}, + } + opts := DefaultOptions() + + err := ValidateCandidate(candidate, opts) + + assert.NoError(t, err) +} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go new file mode 100644 index 0000000..ca7092f --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go @@ -0,0 +1,357 @@ +package config + +import ( + "context" + "fmt" + "log" + "os" + "sync" + "time" + + "atlas-sdk-go/internal/errors" + "github.com/joho/godotenv" +) + +var ( + cachedAppContext *AppContext + cachedAppContextTime time.Time + cacheTTL = 5 * time.Minute + cacheMutex sync.RWMutex +) + +// Constants for environment variables and default paths +const ( + EnvAppEnv = "APP_ENV" + EnvConfigPath = "ATLAS_CONFIG_PATH" + EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" + DefaultConfigFormat = "configs/config.%s.json" +) + +// AppContext contains all environment-specific configurations +type AppContext struct { + Environment string + Config *Config + Secrets *Secrets +} + +// LoadAppContext initializes application context with environment-specific configuration +// If explicitEnv is provided, it overrides the APP_ENV environment variable +// If strictValidation is true, invalid environments will return an error +func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { + // Environment resolution priority: + // 1. Explicitly passed environment parameter + // 2. APP_ENV environment variable + // 3. Default to "development" + // + // Special environments: + // - "test": Used for automated testing, loads from .env.test and configs/config.test.json + + // Determine environment + env := explicitEnv + if env == "" { + env = os.Getenv(EnvAppEnv) + if env == "" { + env = "development" + } + } + + // Check cache first using the resolved environment + cacheMutex.RLock() + if cachedAppContext != nil && + cachedAppContext.Environment == env && + time.Since(cachedAppContextTime) < cacheTTL { + cached := cachedAppContext + cacheMutex.RUnlock() + return cached, nil + } + cacheMutex.RUnlock() + + // Validate environment + if !ValidateEnvironment(env) { + if strictValidation { + return nil, fmt.Errorf("invalid environment: %s", env) + } + log.Printf("Warning: Unexpected environment '%s' may cause issues", env) + } + // :state-remove-start: copy + // Special handling for test environment + if env == "test" { + log.Printf("Using test environment - ensure test fixtures are available") + + // If TEST_MOCK_CONFIG is set, use mock configuration + if os.Getenv("TEST_MOCK_CONFIG") == "true" { + mockConfig, mockSecrets, err := getTestConfiguration() + if err != nil { + return nil, errors.WithContext(err, "loading test configuration") + } + + appCtx := &AppContext{ + Environment: env, + Config: mockConfig, + Secrets: mockSecrets, + } + + // Cache the result + cacheMutex.Lock() + cachedAppContext = appCtx + cachedAppContextTime = time.Now() + cacheMutex.Unlock() + + return appCtx, nil + } + } + // :state-remove-end: + // Load environment files + envFiles := []string{ + fmt.Sprintf(".env.%s", env), + ".env", + } + + loaded := false + for _, file := range envFiles { + if err := godotenv.Load(file); err == nil { + log.Printf("Loaded environment from %s", file) + loaded = true + break + } + } + + if !loaded { + log.Printf("Warning: No environment files found, using system environment variables only") + } + + // Get config path from env var or use default + configPath := os.Getenv(EnvConfigPath) + if configPath == "" { + configPath = fmt.Sprintf(DefaultConfigFormat, env) + } + + log.Printf("Loading configuration for environment: %s", env) + log.Printf("Using config file: %s", configPath) + + // Load secrets and config + secrets, err := LoadSecrets() + if err != nil { + return nil, errors.WithContext(err, "loading secrets") + } + + config, err := LoadConfig(configPath) + if err != nil { + return nil, errors.WithContext(err, "loading config") + } + + // Validate config with environment context + if err := config.Validate(env); err != nil { + return nil, errors.WithContext(err, "validating config") + } + + log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", + env, config.BaseURL, config.OrgID) + + // Create and initialize the AppContext + appCtx := &AppContext{ + Environment: env, + Config: config, + Secrets: secrets, + } + + // Cache the result + cacheMutex.Lock() + cachedAppContext = appCtx + cachedAppContextTime = time.Now() + cacheMutex.Unlock() + + return appCtx, nil +} + +// LoadAppContextWithContext Add context support to handle timeouts and cancellation +func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { + // Use context for potential operations that may need cancellation + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled while loading configuration: %w", ctx.Err()) + default: + // Continue with loading + } + + // Determine environment + env := explicitEnv + if env == "" { + env = os.Getenv(EnvAppEnv) + if env == "" { + env = "development" + } + } + + // Check cache first using the resolved environment + cacheMutex.RLock() + if cachedAppContext != nil && + cachedAppContext.Environment == env && + time.Since(cachedAppContextTime) < cacheTTL { + cached := cachedAppContext + cacheMutex.RUnlock() + return cached, nil + } + cacheMutex.RUnlock() + + // Rest of implementation mirrors LoadAppContext but with context checks + if !ValidateEnvironment(env) { + if strictValidation { + return nil, fmt.Errorf("invalid environment: %s", env) + } + log.Printf("Warning: Unexpected environment '%s' may cause issues", env) + } + + // Special handling for test environment + if env == "test" { + log.Printf("Using test environment - ensure test fixtures are available") + } + + // Add context check before expensive operations + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled while loading environment files: %w", ctx.Err()) + default: + } + + // Load environment files with improved approach + envFiles := []string{ + fmt.Sprintf(".env.%s", env), + ".env", + } + + loaded := false + for _, file := range envFiles { + if err := godotenv.Load(file); err == nil { + log.Printf("Loaded environment from %s", file) + loaded = true + break + } + } + + if !loaded { + log.Printf("Warning: No environment files found, using system environment variables only") + } + + // Get config path from env var or use default + configPath := os.Getenv(EnvConfigPath) + if configPath == "" { + configPath = fmt.Sprintf(DefaultConfigFormat, env) + } + + // Add context check before loading secrets and config + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled before loading secrets/config: %w", ctx.Err()) + default: + } + + log.Printf("Loading configuration for environment: %s", env) + log.Printf("Using config file: %s", configPath) + + // Load secrets and config + secrets, err := LoadSecrets() + if err != nil { + return nil, errors.WithContext(err, "loading secrets") + } + + config, err := LoadConfig(configPath) + if err != nil { + return nil, errors.WithContext(err, "loading config") + } + + // Validate config with environment context + if err := config.Validate(env); err != nil { + return nil, errors.WithContext(err, "validating config") + } + + log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", + env, config.BaseURL, config.OrgID) + + // Create and initialize the AppContext + appCtx := &AppContext{ + Environment: env, + Config: config, + Secrets: secrets, + } + + // Cache the result + cacheMutex.Lock() + cachedAppContext = appCtx + cachedAppContextTime = time.Now() + cacheMutex.Unlock() + + return appCtx, nil +} + +// getTestConfiguration provides specialized test configuration +func getTestConfiguration() (*Config, *Secrets, error) { + // Check if specific test config file exists + testConfigFile := "configs/config.test.json" + if _, err := os.Stat(testConfigFile); err == nil { + config, err := LoadConfig(testConfigFile) + if err != nil { + return nil, nil, err + } + + // Still use mock secrets to avoid requiring real credentials + mockSecrets := &Secrets{ + ServiceAccountID: "test-service-account-id", + ServiceAccountSecret: "test-service-account-secret", + } + + return config, mockSecrets, nil + } + + // Fall back to fully mocked configuration + return &Config{ + BaseURL: "https://cloud-mock.mongodb.com", + OrgID: "test-org-id", + ProjectID: "test-project-id", + ClusterName: "TestCluster", + ProcessID: "test-cluster-shard-00-00.test.mongodb.net:27017", + HostName: "test-cluster-shard-00-00.test.mongodb.net", + }, &Secrets{ + ServiceAccountID: "test-service-account-id", + ServiceAccountSecret: "test-service-account-secret", + }, nil +} + +// Add diff support for testing +func (a *AppContext) Diff(other *AppContext) []string { + var differences []string + + if a.Environment != other.Environment { + differences = append(differences, fmt.Sprintf("Environment: %s vs %s", + a.Environment, other.Environment)) + } + + // Compare important config fields + if a.Config.BaseURL != other.Config.BaseURL { + differences = append(differences, fmt.Sprintf("BaseURL: %s vs %s", + a.Config.BaseURL, other.Config.BaseURL)) + } + + if a.Config.OrgID != other.Config.OrgID { + differences = append(differences, fmt.Sprintf("OrgID: %s vs %s", + a.Config.OrgID, other.Config.OrgID)) + } + + if a.Config.ProjectID != other.Config.ProjectID { + differences = append(differences, fmt.Sprintf("ProjectID: %s vs %s", + a.Config.ProjectID, other.Config.ProjectID)) + } + + if a.Config.ClusterName != other.Config.ClusterName { + differences = append(differences, fmt.Sprintf("ClusterName: %s vs %s", + a.Config.ClusterName, other.Config.ClusterName)) + } + + if a.Config.ProcessID != other.Config.ProcessID { + differences = append(differences, fmt.Sprintf("ProcessID: %s vs %s", + a.Config.ProcessID, other.Config.ProcessID)) + } + + return differences +} 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..1bcc917 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -4,8 +4,20 @@ import ( "atlas-sdk-go/internal/errors" ) -// LoadAll loads secrets and config from the specified paths -func LoadAll(configPath string) (*Secrets, *Config, error) { +// LoadAll loads secrets and config +// If configPath is empty, uses environment-specific loading +// If explicitEnv is provided, it overrides the APP_ENV environment variable +func LoadAll(configPath string, explicitEnv string) (*Secrets, *Config, error) { + if configPath == "" { + // Use environment-based loading + appCtx, err := LoadAppContext(explicitEnv, false) // Use non-strict validation by default + if err != nil { + return nil, nil, err + } + return appCtx.Secrets, appCtx.Config, nil + } + + // Legacy path-specific loading s, err := LoadSecrets() if err != nil { return nil, nil, errors.WithContext(err, "loading secrets") @@ -18,3 +30,14 @@ func LoadAll(configPath string) (*Secrets, *Config, error) { return s, c, nil } + +// ValidateEnvironment checks if the provided environment is valid +func ValidateEnvironment(env string) bool { + validEnvs := map[string]bool{ + "development": true, + "staging": true, + "production": true, + "test": true, + } + return validEnvs[env] +} 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..4e9c1dc 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go @@ -61,3 +61,19 @@ func LoadConfig(path string) (*Config, error) { return &config, nil } + +func (c *Config) Validate(env string) error { + if c.BaseURL == "" { + return &errors.ValidationError{Message: "BaseURL is required"} + } + + // Add environment-specific validation + if env == "production" { + // Stricter validation for production + if strings.Contains(c.BaseURL, "dev") || strings.Contains(c.BaseURL, "test") { + return &errors.ValidationError{Message: "Production environment cannot use development URLs"} + } + } + + return 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 index 2d31065..6f5ba17 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go @@ -7,11 +7,6 @@ import ( "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 diff --git a/usage-examples/go/atlas-sdk-go/internal/config/utils.go b/usage-examples/go/atlas-sdk-go/internal/config/utils.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/config/utils.go @@ -0,0 +1 @@ +package config From e86d99c5a7c410f84403113c1f02f8fed5d01dba Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 6 Aug 2025 15:53:51 -0400 Subject: [PATCH 02/19] Add programmatic scaling example for Scalability page --- .../examples/performance/archiving/main.go | 8 +- .../examples/performance/scaling/main.go | 95 +++++++++++-------- .../atlas-sdk-go/internal/clusters/utils.go | 56 +++++++++++ .../go/atlas-sdk-go/internal/scale/cluster.go | 50 ++++++++++ 4 files changed, 163 insertions(+), 46 deletions(-) create mode 100644 usage-examples/go/atlas-sdk-go/internal/clusters/utils.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/scale/cluster.go 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 index 326d5e2..af1054c 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -18,23 +18,21 @@ import ( // In a production scenario, you would customize the collection analysis // logic in CollectionsForArchiving() to match your specific data patterns. func main() { - // Set up context with timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration - appCtx, err := config.LoadAppContextWithContext(ctx, "internal", false) + // Load application context with configuration and secrets for the specified environment + explicitEnv := "internal" + appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) if err != nil { errors.ExitWithError("Failed to load configuration", err) } - // Initialize the Atlas API client client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) if err != nil { errors.ExitWithError("Failed to initialize Atlas client", err) } - // Get the project ID from configuration projectID := appCtx.Config.ProjectID if projectID == "" { errors.ExitWithError("Project ID not found in configuration", nil) diff --git a/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go b/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go index 4dac64b..c8a4361 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go @@ -2,6 +2,7 @@ package main import ( "atlas-sdk-go/internal/clusters" + "atlas-sdk-go/internal/metrics" "atlas-sdk-go/internal/scale" "context" "fmt" @@ -26,15 +27,15 @@ const ( // CPUMetrics represents CPU utilization metrics for a cluster type CPUMetrics struct { - AverageCPUUsage float64 - MaxCPUUsage float64 + AverageCPUUsage float32 + MaxCPUUsage float32 SampleCount int } // CPUThresholds defines the thresholds for scaling decisions type CPUThresholds struct { - ScaleUpThreshold float64 - ScaleDownThreshold float64 + ScaleUpThreshold float32 + ScaleDownThreshold float32 } // ScalingDecision represents a decision on whether to scale a cluster @@ -49,8 +50,8 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration - appCtx, err := config.LoadAppContextWithContext(ctx, "internal", false) + explicitEnv := "internal" + appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) if err != nil { errors.ExitWithError("Failed to load configuration", err) } @@ -62,7 +63,11 @@ func main() { } // Get the project ID from configuration projectId := appCtx.Config.ProjectID - if projectId == "" { + + clusterParams := &admin.ListClustersApiParams{ + GroupId: projectId, + } + if clusterParams.GroupId == "" { errors.ExitWithError("Project ID not found in configuration", nil) } @@ -75,19 +80,23 @@ func main() { cpuThresholds.ScaleUpThreshold, cpuThresholds.ScaleDownThreshold) // Get list of all clusters in the project - clusterNames, err = clusters.ListClusterNames(ctx, client, projectId) + var clusterNames []string + clusterNameParams := &admin.ListClustersApiParams{ + GroupId: projectId, + } + clusterNames, err = clusters.ListClusterNames(ctx, client.ClustersApi, clusterNameParams) if err != nil { errors.ExitWithError("Failed to list clusters", err) } if len(clusterNames) == 0 { - fmt.Printf("No clusters found for the project ID: %s\n", projectId) + fmt.Printf("No clusters found for the project ID: %s\n", clusterNameParams.GroupId) return } // Evaluate each cluster's details to determine eligibility for scaling for _, clusterName := range clusterNames { - clusterDetails, resp, err := client.ClustersApi.GetCluster(ctx, projectId, clusterName).Execute() + clusterDetails, _, err := client.ClustersApi.GetCluster(ctx, projectId, clusterName).Execute() if err != nil { log.Printf("Error getting details for cluster %s: %v", clusterName, err) continue @@ -100,31 +109,39 @@ func main() { continue } + processParams := &admin.ListAtlasProcessesApiParams{ + GroupId: projectId, + } // Get CPU metrics for the cluster - processId, err := clusters.GetProcessIdForCluster(ctx, client, projectId, clusterName) + processId, err := clusters.GetProcessIdForCluster(ctx, client.MonitoringAndLogsApi, processParams, clusterName) if err != nil { - log.Printf("Could not get process ID for cluster %s: %v", clusterName, err) + log.Printf("Error fetching process ID for cluster %s: %v", clusterName, err) continue } - cpuMetrics, err := getClusterCPUMetrics(ctx, client, projectId, processId, cpuMonitoringPeriod) + cpuParams := &admin.GetHostMeasurementsApiParams{ + GroupId: projectId, + ProcessId: processId, + } + cpuMetrics, err := getClusterCPUMetrics(ctx, client, cpuParams) if err != nil { - log.Printf("Could not fetch CPU metrics for cluster %s: %v", clusterName, err) + log.Printf("Error fetching CPU metrics for cluster %s: %v", clusterName, err) continue } + cpuUsage := cpuMetrics // Evaluate scaling decision based on CPU usage - scalingDecision := evaluateCPUBasedScaling(cpuMetrics, cpuThresholds) + scalingDecision := evaluateCPUBasedScaling(cpuUsage, cpuThresholds) log.Printf("Cluster %s - CPU: avg=%.2f%%, max=%.2f%%, samples=%d", - clusterName, cpuMetrics.AverageCPUUsage, cpuMetrics.MaxCPUUsage, cpuMetrics.SampleCount) + clusterName, cpuUsage.AverageCPUUsage, cpuUsage.MaxCPUUsage, cpuUsage.SampleCount) log.Printf("Scaling decision: %s", scalingDecision.Reason) // Perform scaling if needed if scalingDecision.ShouldScale && scalingDecision.Direction == "up" { log.Printf("Scaling cluster %s UP from %s to %s due to high CPU usage", clusterName, currentInstanceSize, targetInstanceSize) - err := scale.UpdateClusterSize(ctx, client, projectId, clusterName, clusterDetails, targetInstanceSize) + err := scale.UpdateClusterSize(ctx, client.ClustersApi, projectId, clusterName, clusterDetails, targetInstanceSize) if err != nil { log.Printf("Error during scaling: %v", err) } @@ -133,7 +150,7 @@ func main() { scaleDownSize := getScaleDownSize(currentInstanceSize) log.Printf("Scaling cluster %s DOWN from %s to %s due to low CPU usage", clusterName, currentInstanceSize, scaleDownSize) - err := scale.UpdateClusterSize(ctx, client, projectId, clusterName, clusterDetails, scaleDownSize) + err := scale.UpdateClusterSize(ctx, client.ClustersApi, projectId, clusterName, clusterDetails, scaleDownSize) if err != nil { log.Printf("Error during scaling: %v", err) } @@ -145,34 +162,30 @@ func main() { log.Println("Cluster scaling process completed successfully.") } -func getClusterCPUMetrics(ctx context.Context, client *admin.APIClient, projectID, processID, period string) (CPUMetrics, error) { +func getClusterCPUMetrics(ctx context.Context, client *admin.APIClient, p *admin.GetHostMeasurementsApiParams) (CPUMetrics, error) { // Configure time window for metrics - end := time.Now().UTC() - start := end.Add(-24 * time.Hour) // Default to 1 day - granularity := "PT1H" // 1-hour granularity - - startStr := start.Format(time.RFC3339) - endStr := end.Format(time.RFC3339) - - request := client.MonitoringAndLogsApi.GetProcessMeasurements(ctx, projectID, processID) - request = request.M("CPU_USAGE") - request = request.Granularity(granularity) - request = request.Period(period) - request = request.Start(startStr) - request = request.End(endStr) - - metrics, httpResp, err := request.Execute() + p = &admin.GetHostMeasurementsApiParams{ + GroupId: p.GroupId, + ProcessId: p.ProcessId, + M: &[]string{"CPU_USAGE"}, + Granularity: admin.PtrString("PT1H"), + Period: admin.PtrString(cpuMonitoringPeriod), + } + cpuMetrics, err := metrics.FetchProcessMetrics(ctx, client.MonitoringAndLogsApi, p) if err != nil { - return CPUMetrics{}, fmt.Errorf("failed to get CPU metrics: %w", err) + log.Printf("Error fetching CPU metrics for process ID %s: %v", p.ProcessId, err) + } + if cpuMetrics == nil || !cpuMetrics.HasMeasurements() || len(cpuMetrics.GetMeasurements()) == 0 { + return CPUMetrics{}, fmt.Errorf("no CPU metrics available for process ID %s", p.ProcessId) } - var totalCPU float64 - var maxCPU float64 + var totalCPU float32 + var maxCPU float32 var sampleCount int // Calculate average and max CPU usage - if metrics.Measurements != nil { - for _, measurement := range *metrics.Measurements { + if cpuMetrics.Measurements != nil { + for _, measurement := range *cpuMetrics.Measurements { if measurement.DataPoints != nil { for _, dataPoint := range *measurement.DataPoints { if dataPoint.Value != nil { @@ -193,7 +206,7 @@ func getClusterCPUMetrics(ctx context.Context, client *admin.APIClient, projectI } return CPUMetrics{ - AverageCPUUsage: totalCPU / float64(sampleCount), + AverageCPUUsage: totalCPU / float32(sampleCount), MaxCPUUsage: maxCPU, SampleCount: sampleCount, }, nil @@ -226,7 +239,7 @@ func evaluateCPUBasedScaling(metrics CPUMetrics, thresholds CPUThresholds) Scali } } -func isEligibleForScaling(cluster *admin.ClusterDescription, currentSize string) bool { +func isEligibleForScaling(cluster *admin.ClusterDescription20240805, currentSize string) bool { if cluster.ReplicationSpecs == nil || len(*cluster.ReplicationSpecs) == 0 { return false } 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..45101c3 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go @@ -0,0 +1,56 @@ +package clusters + +import ( + "atlas-sdk-go/internal/errors" + "context" + "fmt" + + "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) + // List all processes in the project + 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) +} diff --git a/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go b/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go new file mode 100644 index 0000000..6c429dc --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go @@ -0,0 +1,50 @@ +package scale + +import ( + "context" + "fmt" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// UpdateClusterSize handles the actual scaling operation by calling Atlas API +func UpdateClusterSize(ctx context.Context, api admin.ClustersApi, groupId, clusterName string, + cluster *admin.ClusterDescription20240805, targetSize string) error { + if cluster.ReplicationSpecs == nil || len(*cluster.ReplicationSpecs) == 0 { + return fmt.Errorf("no replication specs found for cluster %s", clusterName) + } + + replicationSpec := (*cluster.ReplicationSpecs)[0] + if replicationSpec.RegionConfigs == nil || len(*replicationSpec.RegionConfigs) == 0 { + return fmt.Errorf("no region configs found for cluster %s", clusterName) + } + + regionConfig := (*replicationSpec.RegionConfigs)[0] + + // Create update request + updateRequest := &admin.ClusterDescription20240805{ + ReplicationSpecs: &[]admin.ReplicationSpec20240805{ + { + Id: replicationSpec.Id, + RegionConfigs: &[]admin.CloudRegionConfig20240805{ + { + // Keep the same provider and region names + ProviderName: regionConfig.ProviderName, + RegionName: regionConfig.RegionName, + ElectableSpecs: &admin.HardwareSpec20240805{ + // Update the instance size to the target size + InstanceSize: admin.PtrString(targetSize), + }, + }, + }, + }, + }, + } + + // Execute the update API call + _, _, err := api.UpdateCluster(ctx, groupId, clusterName, updateRequest).Execute() + if err != nil { + return fmt.Errorf("failed to scale cluster %s: %w", clusterName, err) + } + + return nil +} From 340ce32546e2ad9a9b2a582c813903e8adee63ee Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 14 Aug 2025 13:41:04 -0400 Subject: [PATCH 03/19] Add archive example for Scalability page --- .../examples/billing/historical/main.go | 2 +- .../examples/performance/archiving/main.go | 59 ++-- .../examples/performance/scaling/main.go | 274 ------------------ .../atlas-sdk-go/internal/archive/analyze.go | 129 +-------- .../internal/billing/collector_test.go | 2 +- .../internal/clusters/utils_test.go | 169 +++++++++++ .../internal/metrics/process_test.go | 2 +- .../internal/scale/cluster_test.go | 183 ++++++++++++ 8 files changed, 400 insertions(+), 420 deletions(-) delete mode 100644 usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go 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 0627c11..69aa8f7 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 @@ -64,7 +64,7 @@ func main() { exportInvoicesToCSV(invoices, outDir, prefix) // :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) 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 index af1054c..34eb373 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -1,3 +1,7 @@ +// :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 ( @@ -6,17 +10,10 @@ import ( "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" "context" - "log" + "fmt" "time" ) -// This program demonstrates an automated approach to: -// 1. Discover all clusters in an Atlas project -// 2. Analyze collections within each cluster for archiving candidates -// 3. Configure Online Archive for eligible collections -// -// In a production scenario, you would customize the collection analysis -// logic in CollectionsForArchiving() to match your specific data patterns. func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() @@ -38,7 +35,7 @@ func main() { errors.ExitWithError("Project ID not found in configuration", nil) } - log.Println("Starting archive analysis for project:", projectID) + fmt.Println("Starting archive analysis for project:", projectID) // Step 1: List all clusters in the project clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() @@ -46,42 +43,64 @@ func main() { errors.ExitWithError("Failed to list clusters", err) } - log.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) // Step 2: Process each cluster failedArchives := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - log.Printf("Analyzing cluster: %s", clusterName) + fmt.Printf("Analyzing cluster: %s", clusterName) // Step 3: Find collections suitable for archiving - // Note: Partition fields are ordered by query frequency - most frequently - // queried field should be first for optimal query performance against - // archived data. This significantly impacts cost and performance. + // NOTE: In a production scenario, you would customize the collection analysis logic to match your specific data patterns. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - log.Printf("Found %d collections eligible for archiving in cluster %s", + fmt.Printf("Found %d collections eligible for archiving in cluster %s", len(candidates), clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - log.Printf("Configuring archive for %s.%s", + fmt.Printf("Configuring archive for %s.%s", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - log.Printf("Failed to configure archive: %v", configureErr) + fmt.Printf("Failed to configure archive: %v", configureErr) failedArchives++ continue } - log.Printf("Successfully configured online archive for %s.%s", + fmt.Printf("Successfully configured online archive for %s.%s", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - log.Printf("Warning: %d archive configurations failed", failedArchives) + fmt.Printf("Warning: %d archive configurations failed", failedArchives) } - log.Println("Archive analysis and configuration completed") + fmt.Println("Archive analysis and configuration completed") } + +// :snippet-end: [archive-collections] +// :state-remove-start: copy +// NOTE: INTERNAL +// ** OUTPUT EXAMPLE ** +// +// Configuration loaded successfully: env=production, baseURL=https://cloud.mongodb.com, orgID=5bfda007553855125605a5cf +// Starting archive analysis for project: 5f60207f14dfb25d24511201 +// Found 2 clusters to analyze +// Analyzing cluster: Cluster0 +// Found 2 collections eligible for archiving in cluster Cluster0 +// Configuring archive for sample_analytics.transactions +// Successfully configured online archive for sample_analytics.transactions +// Configuring archive for sample_analytics.users +// Successfully configured online archive for sample_analytics.users +// Analyzing cluster: Cluster1 +// Found 1 collections eligible for archiving in cluster Cluster1 +// Configuring archive for sample_analytics.orders +// Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields +// Configuring archive for sample_logs.application_logs +// Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields +// Warning: 2 archive configurations failed +// Archive analysis and configuration completed +// :state-remove-end: [copy] diff --git a/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go b/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go deleted file mode 100644 index c8a4361..0000000 --- a/usage-examples/go/atlas-sdk-go/examples/performance/scaling/main.go +++ /dev/null @@ -1,274 +0,0 @@ -package main - -import ( - "atlas-sdk-go/internal/clusters" - "atlas-sdk-go/internal/metrics" - "atlas-sdk-go/internal/scale" - "context" - "fmt" - "log" - "time" - - "go.mongodb.org/atlas-sdk/v20250219001/admin" - - "atlas-sdk-go/internal/auth" - "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" -) - -// Constants for the scaling thresholds and instance sizes -const ( - currentInstanceSize = "M30" - targetInstanceSize = "M40" - cpuMonitoringPeriod = "P1D" // Look at last 24 hours of CPU data - scaleUpThreshold = 70.0 // Scale up if CPU utilization is above 70% - scaleDownThreshold = 30.0 // Scale down if CPU utilization is below 30% -) - -// CPUMetrics represents CPU utilization metrics for a cluster -type CPUMetrics struct { - AverageCPUUsage float32 - MaxCPUUsage float32 - SampleCount int -} - -// CPUThresholds defines the thresholds for scaling decisions -type CPUThresholds struct { - ScaleUpThreshold float32 - ScaleDownThreshold float32 -} - -// ScalingDecision represents a decision on whether to scale a cluster -type ScalingDecision struct { - ShouldScale bool - Direction string - Reason string -} - -func main() { - // Set up context with timeout - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - explicitEnv := "internal" - appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) - if err != nil { - errors.ExitWithError("Failed to load configuration", err) - } - - // Initialize the Atlas API client - client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) - if err != nil { - errors.ExitWithError("Failed to initialize Atlas client", err) - } - // Get the project ID from configuration - projectId := appCtx.Config.ProjectID - - clusterParams := &admin.ListClustersApiParams{ - GroupId: projectId, - } - if clusterParams.GroupId == "" { - errors.ExitWithError("Project ID not found in configuration", nil) - } - - // Set up CPU thresholds based on scalability recommendations - cpuThresholds := CPUThresholds{ - ScaleUpThreshold: scaleUpThreshold, - ScaleDownThreshold: scaleDownThreshold, - } - log.Printf("Using CPU thresholds - Scale up: %.1f%%, Scale down: %.1f%%", - cpuThresholds.ScaleUpThreshold, cpuThresholds.ScaleDownThreshold) - - // Get list of all clusters in the project - var clusterNames []string - clusterNameParams := &admin.ListClustersApiParams{ - GroupId: projectId, - } - clusterNames, err = clusters.ListClusterNames(ctx, client.ClustersApi, clusterNameParams) - if err != nil { - errors.ExitWithError("Failed to list clusters", err) - } - - if len(clusterNames) == 0 { - fmt.Printf("No clusters found for the project ID: %s\n", clusterNameParams.GroupId) - return - } - - // Evaluate each cluster's details to determine eligibility for scaling - for _, clusterName := range clusterNames { - clusterDetails, _, err := client.ClustersApi.GetCluster(ctx, projectId, clusterName).Execute() - if err != nil { - log.Printf("Error getting details for cluster %s: %v", clusterName, err) - continue - } - - // Check if the cluster matches the target instance size - if !isEligibleForScaling(clusterDetails, currentInstanceSize) { - log.Printf("Cluster %s instance size doesn't match criteria (%s), skipping", - clusterName, currentInstanceSize) - continue - } - - processParams := &admin.ListAtlasProcessesApiParams{ - GroupId: projectId, - } - // Get CPU metrics for the cluster - processId, err := clusters.GetProcessIdForCluster(ctx, client.MonitoringAndLogsApi, processParams, clusterName) - if err != nil { - log.Printf("Error fetching process ID for cluster %s: %v", clusterName, err) - continue - } - - cpuParams := &admin.GetHostMeasurementsApiParams{ - GroupId: projectId, - ProcessId: processId, - } - cpuMetrics, err := getClusterCPUMetrics(ctx, client, cpuParams) - if err != nil { - log.Printf("Error fetching CPU metrics for cluster %s: %v", clusterName, err) - continue - } - cpuUsage := cpuMetrics - - // Evaluate scaling decision based on CPU usage - scalingDecision := evaluateCPUBasedScaling(cpuUsage, cpuThresholds) - - log.Printf("Cluster %s - CPU: avg=%.2f%%, max=%.2f%%, samples=%d", - clusterName, cpuUsage.AverageCPUUsage, cpuUsage.MaxCPUUsage, cpuUsage.SampleCount) - log.Printf("Scaling decision: %s", scalingDecision.Reason) - - // Perform scaling if needed - if scalingDecision.ShouldScale && scalingDecision.Direction == "up" { - log.Printf("Scaling cluster %s UP from %s to %s due to high CPU usage", - clusterName, currentInstanceSize, targetInstanceSize) - err := scale.UpdateClusterSize(ctx, client.ClustersApi, projectId, clusterName, clusterDetails, targetInstanceSize) - if err != nil { - log.Printf("Error during scaling: %v", err) - } - } else if scalingDecision.ShouldScale && scalingDecision.Direction == "down" { - // Define a smaller instance size for scale down - scaleDownSize := getScaleDownSize(currentInstanceSize) - log.Printf("Scaling cluster %s DOWN from %s to %s due to low CPU usage", - clusterName, currentInstanceSize, scaleDownSize) - err := scale.UpdateClusterSize(ctx, client.ClustersApi, projectId, clusterName, clusterDetails, scaleDownSize) - if err != nil { - log.Printf("Error during scaling: %v", err) - } - } else { - log.Printf("No scaling needed for cluster %s", clusterName) - } - } - - log.Println("Cluster scaling process completed successfully.") -} - -func getClusterCPUMetrics(ctx context.Context, client *admin.APIClient, p *admin.GetHostMeasurementsApiParams) (CPUMetrics, error) { - // Configure time window for metrics - p = &admin.GetHostMeasurementsApiParams{ - GroupId: p.GroupId, - ProcessId: p.ProcessId, - M: &[]string{"CPU_USAGE"}, - Granularity: admin.PtrString("PT1H"), - Period: admin.PtrString(cpuMonitoringPeriod), - } - cpuMetrics, err := metrics.FetchProcessMetrics(ctx, client.MonitoringAndLogsApi, p) - if err != nil { - log.Printf("Error fetching CPU metrics for process ID %s: %v", p.ProcessId, err) - } - if cpuMetrics == nil || !cpuMetrics.HasMeasurements() || len(cpuMetrics.GetMeasurements()) == 0 { - return CPUMetrics{}, fmt.Errorf("no CPU metrics available for process ID %s", p.ProcessId) - } - - var totalCPU float32 - var maxCPU float32 - var sampleCount int - - // Calculate average and max CPU usage - if cpuMetrics.Measurements != nil { - for _, measurement := range *cpuMetrics.Measurements { - if measurement.DataPoints != nil { - for _, dataPoint := range *measurement.DataPoints { - if dataPoint.Value != nil { - cpuValue := *dataPoint.Value - totalCPU += cpuValue - if cpuValue > maxCPU { - maxCPU = cpuValue - } - sampleCount++ - } - } - } - } - } - - if sampleCount == 0 { - return CPUMetrics{}, fmt.Errorf("no CPU metrics available") - } - - return CPUMetrics{ - AverageCPUUsage: totalCPU / float32(sampleCount), - MaxCPUUsage: maxCPU, - SampleCount: sampleCount, - }, nil -} - -func evaluateCPUBasedScaling(metrics CPUMetrics, thresholds CPUThresholds) ScalingDecision { - if metrics.AverageCPUUsage > thresholds.ScaleUpThreshold { - return ScalingDecision{ - ShouldScale: true, - Direction: "up", - Reason: fmt.Sprintf("Average CPU usage (%.2f%%) exceeds scale-up threshold (%.2f%%)", - metrics.AverageCPUUsage, thresholds.ScaleUpThreshold), - } - } - - if metrics.AverageCPUUsage < thresholds.ScaleDownThreshold { - return ScalingDecision{ - ShouldScale: true, - Direction: "down", - Reason: fmt.Sprintf("Average CPU usage (%.2f%%) is below scale-down threshold (%.2f%%)", - metrics.AverageCPUUsage, thresholds.ScaleDownThreshold), - } - } - - return ScalingDecision{ - ShouldScale: false, - Direction: "", - Reason: fmt.Sprintf("CPU usage (%.2f%%) is within normal range (%.2f%% - %.2f%%)", - metrics.AverageCPUUsage, thresholds.ScaleDownThreshold, thresholds.ScaleUpThreshold), - } -} - -func isEligibleForScaling(cluster *admin.ClusterDescription20240805, currentSize string) bool { - if cluster.ReplicationSpecs == nil || len(*cluster.ReplicationSpecs) == 0 { - return false - } - - replicationSpec := (*cluster.ReplicationSpecs)[0] - if replicationSpec.RegionConfigs == nil || len(*replicationSpec.RegionConfigs) == 0 { - return false - } - - regionConfig := (*replicationSpec.RegionConfigs)[0] - if regionConfig.ElectableSpecs == nil || regionConfig.ElectableSpecs.InstanceSize == nil { - return false - } - - return *regionConfig.ElectableSpecs.InstanceSize == currentSize -} - -func getScaleDownSize(currentSize string) string { - scaleDownMap := map[string]string{ - "M40": "M30", - "M30": "M20", - "M20": "M10", - "M50": "M40", - "M60": "M50", - "M80": "M60", - } - - if downSize, exists := scaleDownMap[currentSize]; exists { - return downSize - } - return currentSize -} diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go index b4e653e..89b98be 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -39,124 +39,14 @@ func DefaultOptions() Options { } } -// CollectionsForArchiving identifies collections suitable for archiving based on data patterns -// func CollectionsForArchivingFull(ctx context.Context, sdk *admin.APIClient, -// -// projectID, clusterName string) ([]Candidate, error) { -// -// // Get all databases in the cluster -// databases, err := listDatabases(ctx, sdk, projectID, clusterName) -// if err != nil { -// return nil, errors.FormatError("list databases", err) -// } -// -// var candidates []Candidate -// -// // For each database, analyze collections -// for _, dbName := range databases { -// // Skip system databases -// if dbName == "admin" || dbName == "local" || dbName == "config" { -// continue -// } -// -// collections, err := listCollections(ctx, sdk, projectID, clusterName, dbName) -// if err != nil { -// log.Printf("Error listing collections for %s: %v", dbName, err) -// continue -// } -// -// for _, collName := range collections { -// // Get collection stats and metadata -// stats, err := getCollectionStats(ctx, sdk, projectID, clusterName, dbName, collName) -// if err != nil { -// log.Printf("Error getting stats for %s.%s: %v", dbName, collName, err) -// continue -// } -// -// // Skip collections smaller than threshold (e.g., 1GB) -// if stats.Size < 1_000_000_000 { -// continue -// } -// -// // Analyze data age distribution -// dateField, dateFormat, err := identifyDateField(ctx, sdk, projectID, clusterName, dbName, collName) -// if err != nil || dateField == "" { -// log.Printf("No suitable date field found in %s.%s", dbName, collName) -// continue -// } -// -// // Calculate appropriate retention period based on data distribution -// retentionDays := calculateRetentionDays(stats.AgeDistribution) -// -// // Identify optimal partition fields based on index usage statistics -// partitionFields := identifyPartitionFields(ctx, sdk, projectID, clusterName, dbName, collName) -// -// // Create candidate if it meets minimum requirements -// if retentionDays >= 30 && len(partitionFields) > 0 { -// candidates = append(candidates, Candidate{ -// DatabaseName: dbName, -// CollectionName: collName, -// DateField: dateField, -// DateFormat: dateFormat, -// RetentionDays: retentionDays, -// PartitionFields: partitionFields, -// }) -// } -// } -// } -// -// return candidates, nil -// } -// -// // Helper functions would include: -// -// func listDatabases(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) ([]string, error) { -// // Use Atlas API or direct MongoDB connection to list databases -// // ... -// } -// -// func listCollections(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName string) ([]string, error) { -// // Use Atlas API or direct MongoDB connection to list collections -// // ... -// } -// -// func getCollectionStats(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) (*CollectionStats, error) { -// // Get collection statistics including size, document count, etc. -// // ... -// } -// -// func identifyDateField(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) (string, string, error) { -// // Sample documents to identify fields with date values -// // Determine the format (ISO date, epoch timestamp, etc.) -// // ... -// } -// -// func calculateRetentionDays(ageDistribution map[string]float64) int { -// // Analyze age distribution to determine optimal retention period -// // Balance between keeping recent data in live collection and archiving older data -// // ... -// } -// -// func identifyPartitionFields(ctx context.Context, sdk *admin.APIClient, projectID, clusterName, dbName, collName string) []string { -// // Analyze index usage statistics to determine most frequently queried fields -// // Review existing indexes to understand query patterns -// // ... -// } -// -// CollectionsForArchiving Simplified function to identify collections suitable for archiving +// CollectionsForArchiving demonstrates how to identify collections suitable for archiving // In a real implementation, you would analyze collection data patterns +// and determine which collections are eligible based on criteria such as size, age, and access patterns. +// This function returns a list of candidates that meet the archiving criteria +// Note: This is a simplified example and should be customized for your specific use case func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { - - // This would normally analyze collection data patterns - // Discovers all databases and collections in the cluster - // Analyzes collection statistics (size, document count, growth rate) - // Identifies date fields for time-based archiving - // Calculates appropriate retention periods based on data age distribution - // Determines optimal partition fields based on query patterns - // Returns only collections that meet minimum size and access pattern requirements for archiving - - // For demo purposes, we'll return some example candidates + // For demonstration purposes, we specify example candidates return []Candidate{ { DatabaseName: "sample_analytics", @@ -178,31 +68,26 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, } type ExpireAfterDays struct { - // This struct can be extended to include more complex rules if needed - // For now, it serves as a placeholder for the data expiration rule + // NOTE: this placeholder struct can be extended to include more complex rules if needed ExpireAfterDays int `json:"expireAfterDays,omitempty"` } // ValidateCandidate ensures the archiving candidate meets requirements func ValidateCandidate(candidate Candidate, opts Options) error { - // Validate required fields if candidate.DatabaseName == "" || candidate.CollectionName == "" { return fmt.Errorf("database name and collection name are required") } - // Validate retention days if candidate.RetentionDays < opts.MinimumRetentionDays { return fmt.Errorf("retention days must be at least %d", opts.MinimumRetentionDays) } - // Validate partition fields 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 != "" { - // Validate date format validFormats := map[string]bool{ "DATE": true, "EPOCH_SECONDS": true, @@ -234,10 +119,8 @@ func ValidateCandidate(candidate Candidate, opts Options) error { func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string, candidate Candidate) error { - // Use default options if not specified opts := DefaultOptions() - // Validate the candidate if err := ValidateCandidate(candidate, opts); err != nil { return errors.FormatError("validate archive candidate", fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), 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..a609d49 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 @@ -1,4 +1,4 @@ -package billing +package billing_test import ( "context" 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..021b850 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go @@ -0,0 +1,169 @@ +package clusters + +import ( + "context" + "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" + "testing" +) + +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") +} 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..fd17263 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 @@ -1,4 +1,4 @@ -package metrics +package metrics_test import ( "context" diff --git a/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go b/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go new file mode 100644 index 0000000..094e427 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go @@ -0,0 +1,183 @@ +package scale_test + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// MockClustersApi mocks the ClustersApi interface +type MockClustersApi struct { + mock.Mock +} + +// UpdateCluster mocks the UpdateCluster method +func (m *MockClustersApi) UpdateCluster(ctx context.Context, groupId string, clusterName string, clusterRequest *admin.ClusterDescription20240805) admin.ApiUpdateClusterRequest { + args := m.Called(ctx, groupId, clusterName, clusterRequest) + return args.Get(0).(admin.ApiUpdateClusterRequest) +} + +// MockApiUpdateClusterRequest mocks the ApiUpdateClusterRequest +type MockApiUpdateClusterRequest struct { + mock.Mock +} + +// Execute mocks the Execute method +func (m *MockApiUpdateClusterRequest) Execute() (*admin.ClusterDescription20240805, *http.Response, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Get(1).(*http.Response), args.Error(2) + } + return args.Get(0).(*admin.ClusterDescription20240805), args.Get(1).(*http.Response), args.Error(2) +} + +func SuccessfulClusterSizeUpdate(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + mockRequest := new(MockApiUpdateClusterRequest) + + replicationSpecs := []admin.ReplicationSpec20240805{ + { + Id: admin.PtrString("rs1"), + RegionConfigs: &[]admin.CloudRegionConfig20240805{ + { + ProviderName: admin.PtrString("AWS"), + RegionName: admin.PtrString("US_EAST_1"), + ElectableSpecs: &admin.HardwareSpec20240805{ + InstanceSize: admin.PtrString("M10"), + }, + }, + }, + }, + } + + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: &replicationSpecs, + } + + mockRequest.On("Execute").Return(&admin.ClusterDescription20240805{}, nil, nil) + mockApi.On("UpdateCluster", mock.Anything, "groupId", "clusterName", mock.Anything).Return(mockRequest) + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.NoError(t, err) + mockApi.AssertExpectations(t) + mockRequest.AssertExpectations(t) +} + +func FailsWhenNoReplicationSpecs(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: nil, + } + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "no replication specs found") +} + +func FailsWhenEmptyReplicationSpecs(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + emptySpecs := []admin.ReplicationSpec20240805{} + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: &emptySpecs, + } + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "no replication specs found") +} + +func FailsWhenNoRegionConfigs(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + replicationSpecs := []admin.ReplicationSpec20240805{ + { + Id: admin.PtrString("rs1"), + RegionConfigs: nil, + }, + } + + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: &replicationSpecs, + } + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "no region configs found") +} + +func FailsWhenEmptyRegionConfigs(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + emptyRegions := []admin.CloudRegionConfig20240805{} + replicationSpecs := []admin.ReplicationSpec20240805{ + { + Id: admin.PtrString("rs1"), + RegionConfigs: &emptyRegions, + }, + } + + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: &replicationSpecs, + } + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "no region configs found") +} + +func FailsWhenApiCallErrors(t *testing.T) { + // Setup + mockApi := new(MockClustersApi) + mockRequest := new(MockApiUpdateClusterRequest) + + replicationSpecs := []admin.ReplicationSpec20240805{ + { + Id: admin.PtrString("rs1"), + RegionConfigs: &[]admin.CloudRegionConfig20240805{ + { + ProviderName: admin.PtrString("AWS"), + RegionName: admin.PtrString("US_EAST_1"), + }, + }, + }, + } + + cluster := &admin.ClusterDescription20240805{ + ReplicationSpecs: &replicationSpecs, + } + + mockRequest.On("Execute").Return(nil, nil, errors.New("API error")) + mockApi.On("UpdateCluster", mock.Anything, "groupId", "clusterName", mock.Anything).Return(mockRequest) + + // Act + err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to scale cluster") + mockApi.AssertExpectations(t) + mockRequest.AssertExpectations(t) +} From 26ce74e744103bd47850e33fa3cb07b7adcaa249 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 14 Aug 2025 13:52:08 -0400 Subject: [PATCH 04/19] Snip and copy files --- .../main.snippet.archive-collections.go | 81 +++++ .../go/atlas-sdk-go/main.snippet.get-logs.go | 12 +- .../main.snippet.get-metrics-dev.go | 10 +- .../main.snippet.get-metrics-prod.go | 10 +- .../main.snippet.historical-billing.go | 9 +- .../atlas-sdk-go/main.snippet.line-items.go | 11 +- .../main.snippet.linked-billing.go | 10 +- .../go/atlas-sdk-go/project-copy/.gitignore | 9 + .../go/atlas-sdk-go/project-copy/CHANGELOG.md | 2 +- .../configs/config.development.json | 12 + .../project-copy/configs/config.json | 7 - .../configs/config.production.json | 12 + .../examples/billing/historical/main.go | 9 +- .../examples/billing/line_items/main.go | 19 +- .../examples/billing/linked_orgs/main.go | 10 +- .../examples/monitoring/logs/main.go | 12 +- .../examples/monitoring/metrics_disk/main.go | 10 +- .../monitoring/metrics_process/main.go | 10 +- .../examples/performance/archiving/main.go | 80 +++++ .../go/atlas-sdk-go/project-copy/go.mod | 1 + .../project-copy/internal/archive/analyze.go | 183 ++++++++++ .../internal/billing/collector.go | 8 +- .../project-copy/internal/clusters/utils.go | 57 +++ .../internal/config/appcontext.go | 330 ++++++++++++++++++ .../project-copy/internal/config/loadall.go | 27 +- .../internal/config/loadconfig.go | 32 ++ .../project-copy/internal/config/loadenv.go | 5 - .../project-copy/internal/config/utils.go | 1 + .../examples/billing/line_items/main.go | 13 +- .../examples/billing/linked_orgs/main.go | 5 +- .../examples/monitoring/logs/main.go | 1 - .../examples/monitoring/metrics_disk/main.go | 7 +- .../monitoring/metrics_process/main.go | 3 +- .../examples/performance/archiving/main.go | 13 +- .../atlas-sdk-go/internal/archive/analyze.go | 4 +- .../internal/billing/collector_test.go | 2 +- .../atlas-sdk-go/internal/clusters/utils.go | 3 +- .../internal/clusters/utils_test.go | 3 +- .../internal/config/appcontext.go | 3 +- .../internal/metrics/disk_test.go | 12 + .../internal/metrics/process_test.go | 4 +- .../go/atlas-sdk-go/internal/scale/cluster.go | 50 --- .../internal/scale/cluster_test.go | 183 ---------- 43 files changed, 926 insertions(+), 359 deletions(-) create mode 100644 generated-usage-examples/go/atlas-sdk-go/main.snippet.archive-collections.go create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.json create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/examples/performance/archiving/main.go create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/clusters/utils.go create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/scale/cluster.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go 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..e9c85e1 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.archive-collections.go @@ -0,0 +1,81 @@ +// See entire project at https://github.com/mongodb/atlas-architecture-go-sdk +package main + +import ( + "context" + "fmt" + "time" + + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + "atlas-sdk-go/internal/errors" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Load application context with configuration and secrets for the environment + explicitEnv := "production" + appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) + if err != nil { + errors.ExitWithError("Failed to load configuration", err) + } + + client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + if err != nil { + errors.ExitWithError("Failed to initialize Atlas client", err) + } + + projectID := appCtx.Config.ProjectID + if projectID == "" { + errors.ExitWithError("Project ID not found in configuration", nil) + } + + fmt.Println("Starting archive analysis for project:", projectID) + + // Step 1: List all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + errors.ExitWithError("Failed to list clusters", err) + } + + fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + + // Step 2: Process each cluster + failedArchives := 0 + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + fmt.Printf("Analyzing cluster: %s", clusterName) + + // Step 3: Find collections suitable for archiving + // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + fmt.Printf("Found %d collections eligible for archiving in cluster %s", + len(candidates), clusterName) + + // Step 4: Configure online archive for each candidate collection + for _, candidate := range candidates { + fmt.Printf("Configuring archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) + if configureErr != nil { + fmt.Printf("Failed to configure archive: %v", configureErr) + failedArchives++ + continue + } + + fmt.Printf("Successfully configured online archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if failedArchives > 0 { + fmt.Printf("Warning: %d archive configurations failed", failedArchives) + } + + 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..012696b 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 @@ -12,16 +12,13 @@ import ( "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } @@ -39,6 +36,8 @@ func main() { 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) @@ -46,6 +45,7 @@ func main() { 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") 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..fedc664 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 @@ -5,23 +5,19 @@ import ( "context" "encoding/json" "fmt" - "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "development" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..49fd66b 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 @@ -5,25 +5,21 @@ import ( "context" "encoding/json" "fmt" - "log" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "production" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..6db1e56 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 @@ -14,16 +14,13 @@ import ( "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..4894754 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "time" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -14,16 +15,12 @@ import ( "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..0820414 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 @@ -4,23 +4,19 @@ package main import ( "context" "fmt" - "log" "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..7ade94e 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore @@ -1,5 +1,14 @@ # Secrets .env +!.env.example +.env.development +.env.production +configs/config.example.json +!configs/config.development.json +!configs/config.production.json + +tmp + # Logs *.log 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..08d81a3 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 @@ -5,7 +5,7 @@ Notable changes to the project. ## 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/configs/config.development.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json new file mode 100644 index 0000000..dc9dd77 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json @@ -0,0 +1,12 @@ +{ + "ENVIRONMENT": "dev", + "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", + "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", + "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", + "ATLAS_PROJECT_NAME": "Customer Portal - Dev", + "ATLAS_PROCESS_ID": "CustomerPortalDev-shard-00-00.ajlj3.mongodb.net:27017", + "CLOUD_PROVIDER": "AWS", + "AUTO_SCALING_DISK_GB": true, + "AUTO_SCALING_COMPUTE": true, + "DISK_SIZE_GB": 10000 +} 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.json deleted file mode 100644 index 9034951..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "MONGODB_ATLAS_BASE_URL": "https://cloud.mongodb.com", - "ATLAS_ORG_ID": "", - "ATLAS_PROJECT_ID": "", - "ATLAS_CLUSTER_NAME": "", - "ATLAS_PROCESS_ID": "" -} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json new file mode 100644 index 0000000..3ffad1e --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json @@ -0,0 +1,12 @@ +{ + "ENVIRONMENT": "prod", + "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", + "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", + "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", + "ATLAS_PROJECT_NAME": "Customer Portal - Prod", + "ATLAS_PROCESS_ID": "CustomerPortalProd-shard-00-00.ajlj3.mongodb.net:27017", + "CLOUD_PROVIDER": "AWS", + "AUTO_SCALING_DISK_GB": true, + "AUTO_SCALING_COMPUTE": true, + "DISK_SIZE_GB": 40000 +} 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..17253a5 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 @@ -13,16 +13,13 @@ import ( "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/fileutils" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..db8005a 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "time" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -13,16 +14,12 @@ import ( "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } @@ -100,3 +97,11 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { fmt.Printf("Exported billing data to %s\n", csvPath) } +// With timeout +func loadConfigWithTimeout() (*config.AppContext, error) { + // Create a context with a 5-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return config.LoadAppContextWithContext(ctx, "", false) +} 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..f58a601 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 @@ -3,23 +3,19 @@ package main import ( "context" "fmt" - "log" "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..5441b1a 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 @@ -11,16 +11,13 @@ import ( "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } @@ -38,6 +35,8 @@ func main() { 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) @@ -45,6 +44,7 @@ func main() { 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") 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..fff535e 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 @@ -4,23 +4,19 @@ import ( "context" "encoding/json" "fmt" - "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" - "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) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "development" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..db83955 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 @@ -4,25 +4,21 @@ import ( "context" "encoding/json" "fmt" - "log" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: .env file not loaded: %v", err) - } - - secrets, cfg, err := config.LoadAll("configs/config.json") + configPath := "" // Use default config path for environment + explicitEnv := "production" + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } 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..1d7e8b4 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/performance/archiving/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "fmt" + "time" + + "atlas-sdk-go/internal/archive" + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/config" + "atlas-sdk-go/internal/errors" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Load application context with configuration and secrets for the environment + explicitEnv := "production" + appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) + if err != nil { + errors.ExitWithError("Failed to load configuration", err) + } + + client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + if err != nil { + errors.ExitWithError("Failed to initialize Atlas client", err) + } + + projectID := appCtx.Config.ProjectID + if projectID == "" { + errors.ExitWithError("Project ID not found in configuration", nil) + } + + fmt.Println("Starting archive analysis for project:", projectID) + + // Step 1: List all clusters in the project + clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() + if err != nil { + errors.ExitWithError("Failed to list clusters", err) + } + + fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + + // Step 2: Process each cluster + failedArchives := 0 + for _, cluster := range clusters.GetResults() { + clusterName := cluster.GetName() + fmt.Printf("Analyzing cluster: %s", clusterName) + + // Step 3: Find collections suitable for archiving + // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + fmt.Printf("Found %d collections eligible for archiving in cluster %s", + len(candidates), clusterName) + + // Step 4: Configure online archive for each candidate collection + for _, candidate := range candidates { + fmt.Printf("Configuring archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) + if configureErr != nil { + fmt.Printf("Failed to configure archive: %v", configureErr) + failedArchives++ + continue + } + + fmt.Printf("Successfully configured online archive for %s.%s", + candidate.DatabaseName, candidate.CollectionName) + } + } + + if failedArchives > 0 { + fmt.Printf("Warning: %d archive configurations failed", failedArchives) + } + + 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..e7afd81 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,6 +1,7 @@ module atlas-sdk-go go 1.24 + require ( github.com/joho/godotenv v1.5.1 go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 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..e64b56f --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go @@ -0,0 +1,183 @@ +package archive + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal/errors" +) + +// 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 provides sensible defaults for archiving +func DefaultOptions() Options { + return Options{ + DefaultRetentionMultiplier: 2, + MinimumRetentionDays: 30, + EnableDataExpiration: true, + ArchiveSchedule: "DAILY", + } +} + +// CollectionsForArchiving demonstrates how to identify collections suitable for archiving +// In a real implementation, you would analyze collection data patterns +// and determine which collections are eligible based on criteria such as size, age, and access patterns. +// This function returns a list of candidates that meet the archiving criteria +// Note: This is a simplified example and should be customized for your specific use case +func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string) []Candidate { + // For demonstration purposes, we specify example candidates + return []Candidate{ + { + DatabaseName: "sample_analytics", + CollectionName: "transactions", + DateField: "transaction_date", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"customer_id", "merchant"}, + }, + { + DatabaseName: "sample_logs", + CollectionName: "application_logs", + DateField: "timestamp", + DateFormat: "EPOCH_MILLIS", + RetentionDays: 30, + PartitionFields: []string{"service_name", "log_level"}, + }, + } +} + +type ExpireAfterDays struct { + // NOTE: this placeholder struct can be extended to include more complex rules if needed + ExpireAfterDays int `json:"expireAfterDays,omitempty"` +} + +// ValidateCandidate ensures the archiving candidate meets requirements +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 +} + +// ConfigureOnlineArchive configures online archive for a collection +func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, + projectID, clusterName string, candidate Candidate) error { + + opts := DefaultOptions() + + if err := ValidateCandidate(candidate, opts); err != nil { + return errors.FormatError("validate archive candidate", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + + // 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/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..693d032 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/clusters/utils.go @@ -0,0 +1,57 @@ +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) + // List all processes in the project + 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) +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go new file mode 100644 index 0000000..6164d93 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go @@ -0,0 +1,330 @@ +package config + +import ( + "context" + "fmt" + "log" + "os" + "sync" + "time" + + "github.com/joho/godotenv" + + "atlas-sdk-go/internal/errors" +) + +var ( + cachedAppContext *AppContext + cachedAppContextTime time.Time + cacheTTL = 5 * time.Minute + cacheMutex sync.RWMutex +) + +// Constants for environment variables and default paths +const ( + EnvAppEnv = "APP_ENV" + EnvConfigPath = "ATLAS_CONFIG_PATH" + EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" + DefaultConfigFormat = "configs/config.%s.json" +) + +// AppContext contains all environment-specific configurations +type AppContext struct { + Environment string + Config *Config + Secrets *Secrets +} + +// LoadAppContext initializes application context with environment-specific configuration +// If explicitEnv is provided, it overrides the APP_ENV environment variable +// If strictValidation is true, invalid environments will return an error +func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { + // Environment resolution priority: + // 1. Explicitly passed environment parameter + // 2. APP_ENV environment variable + // 3. Default to "development" + // + // Special environments: + // - "test": Used for automated testing, loads from .env.test and configs/config.test.json + + // Determine environment + env := explicitEnv + if env == "" { + env = os.Getenv(EnvAppEnv) + if env == "" { + env = "development" + } + } + + // Check cache first using the resolved environment + cacheMutex.RLock() + if cachedAppContext != nil && + cachedAppContext.Environment == env && + time.Since(cachedAppContextTime) < cacheTTL { + cached := cachedAppContext + cacheMutex.RUnlock() + return cached, nil + } + cacheMutex.RUnlock() + + // Validate environment + if !ValidateEnvironment(env) { + if strictValidation { + return nil, fmt.Errorf("invalid environment: %s", env) + } + log.Printf("Warning: Unexpected environment '%s' may cause issues", env) + } + // Load environment files + envFiles := []string{ + fmt.Sprintf(".env.%s", env), + ".env", + } + + loaded := false + for _, file := range envFiles { + if err := godotenv.Load(file); err == nil { + log.Printf("Loaded environment from %s", file) + loaded = true + break + } + } + + if !loaded { + log.Printf("Warning: No environment files found, using system environment variables only") + } + + // Get config path from env var or use default + configPath := os.Getenv(EnvConfigPath) + if configPath == "" { + configPath = fmt.Sprintf(DefaultConfigFormat, env) + } + + log.Printf("Loading configuration for environment: %s", env) + log.Printf("Using config file: %s", configPath) + + // Load secrets and config + secrets, err := LoadSecrets() + if err != nil { + return nil, errors.WithContext(err, "loading secrets") + } + + config, err := LoadConfig(configPath) + if err != nil { + return nil, errors.WithContext(err, "loading config") + } + + // Validate config with environment context + if err := config.Validate(env); err != nil { + return nil, errors.WithContext(err, "validating config") + } + + log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", + env, config.BaseURL, config.OrgID) + + // Create and initialize the AppContext + appCtx := &AppContext{ + Environment: env, + Config: config, + Secrets: secrets, + } + + // Cache the result + cacheMutex.Lock() + cachedAppContext = appCtx + cachedAppContextTime = time.Now() + cacheMutex.Unlock() + + return appCtx, nil +} + +// LoadAppContextWithContext Add context support to handle timeouts and cancellation +func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { + // Use context for potential operations that may need cancellation + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled while loading configuration: %w", ctx.Err()) + default: + // Continue with loading + } + + // Determine environment + env := explicitEnv + if env == "" { + env = os.Getenv(EnvAppEnv) + if env == "" { + env = "development" + } + } + + // Check cache first using the resolved environment + cacheMutex.RLock() + if cachedAppContext != nil && + cachedAppContext.Environment == env && + time.Since(cachedAppContextTime) < cacheTTL { + cached := cachedAppContext + cacheMutex.RUnlock() + return cached, nil + } + cacheMutex.RUnlock() + + // Rest of implementation mirrors LoadAppContext but with context checks + if !ValidateEnvironment(env) { + if strictValidation { + return nil, fmt.Errorf("invalid environment: %s", env) + } + log.Printf("Warning: Unexpected environment '%s' may cause issues", env) + } + + // Special handling for test environment + if env == "test" { + log.Printf("Using test environment - ensure test fixtures are available") + } + + // Add context check before expensive operations + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled while loading environment files: %w", ctx.Err()) + default: + } + + // Load environment files with improved approach + envFiles := []string{ + fmt.Sprintf(".env.%s", env), + ".env", + } + + loaded := false + for _, file := range envFiles { + if err := godotenv.Load(file); err == nil { + log.Printf("Loaded environment from %s", file) + loaded = true + break + } + } + + if !loaded { + log.Printf("Warning: No environment files found, using system environment variables only") + } + + // Get config path from env var or use default + configPath := os.Getenv(EnvConfigPath) + if configPath == "" { + configPath = fmt.Sprintf(DefaultConfigFormat, env) + } + + // Add context check before loading secrets and config + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context cancelled before loading secrets/config: %w", ctx.Err()) + default: + } + + log.Printf("Loading configuration for environment: %s", env) + log.Printf("Using config file: %s", configPath) + + // Load secrets and config + secrets, err := LoadSecrets() + if err != nil { + return nil, errors.WithContext(err, "loading secrets") + } + + config, err := LoadConfig(configPath) + if err != nil { + return nil, errors.WithContext(err, "loading config") + } + + // Validate config with environment context + if err := config.Validate(env); err != nil { + return nil, errors.WithContext(err, "validating config") + } + + log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", + env, config.BaseURL, config.OrgID) + + // Create and initialize the AppContext + appCtx := &AppContext{ + Environment: env, + Config: config, + Secrets: secrets, + } + + // Cache the result + cacheMutex.Lock() + cachedAppContext = appCtx + cachedAppContextTime = time.Now() + cacheMutex.Unlock() + + return appCtx, nil +} + +// getTestConfiguration provides specialized test configuration +func getTestConfiguration() (*Config, *Secrets, error) { + // Check if specific test config file exists + testConfigFile := "configs/config.test.json" + if _, err := os.Stat(testConfigFile); err == nil { + config, err := LoadConfig(testConfigFile) + if err != nil { + return nil, nil, err + } + + // Still use mock secrets to avoid requiring real credentials + mockSecrets := &Secrets{ + ServiceAccountID: "test-service-account-id", + ServiceAccountSecret: "test-service-account-secret", + } + + return config, mockSecrets, nil + } + + // Fall back to fully mocked configuration + return &Config{ + BaseURL: "https://cloud-mock.mongodb.com", + OrgID: "test-org-id", + ProjectID: "test-project-id", + ClusterName: "TestCluster", + ProcessID: "test-cluster-shard-00-00.test.mongodb.net:27017", + HostName: "test-cluster-shard-00-00.test.mongodb.net", + }, &Secrets{ + ServiceAccountID: "test-service-account-id", + ServiceAccountSecret: "test-service-account-secret", + }, nil +} + +// Add diff support for testing +func (a *AppContext) Diff(other *AppContext) []string { + var differences []string + + if a.Environment != other.Environment { + differences = append(differences, fmt.Sprintf("Environment: %s vs %s", + a.Environment, other.Environment)) + } + + // Compare important config fields + if a.Config.BaseURL != other.Config.BaseURL { + differences = append(differences, fmt.Sprintf("BaseURL: %s vs %s", + a.Config.BaseURL, other.Config.BaseURL)) + } + + if a.Config.OrgID != other.Config.OrgID { + differences = append(differences, fmt.Sprintf("OrgID: %s vs %s", + a.Config.OrgID, other.Config.OrgID)) + } + + if a.Config.ProjectID != other.Config.ProjectID { + differences = append(differences, fmt.Sprintf("ProjectID: %s vs %s", + a.Config.ProjectID, other.Config.ProjectID)) + } + + if a.Config.ClusterName != other.Config.ClusterName { + differences = append(differences, fmt.Sprintf("ClusterName: %s vs %s", + a.Config.ClusterName, other.Config.ClusterName)) + } + + if a.Config.ProcessID != other.Config.ProcessID { + differences = append(differences, fmt.Sprintf("ProcessID: %s vs %s", + a.Config.ProcessID, other.Config.ProcessID)) + } + + return differences +} 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..1bcc917 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 @@ -4,8 +4,20 @@ import ( "atlas-sdk-go/internal/errors" ) -// LoadAll loads secrets and config from the specified paths -func LoadAll(configPath string) (*Secrets, *Config, error) { +// LoadAll loads secrets and config +// If configPath is empty, uses environment-specific loading +// If explicitEnv is provided, it overrides the APP_ENV environment variable +func LoadAll(configPath string, explicitEnv string) (*Secrets, *Config, error) { + if configPath == "" { + // Use environment-based loading + appCtx, err := LoadAppContext(explicitEnv, false) // Use non-strict validation by default + if err != nil { + return nil, nil, err + } + return appCtx.Secrets, appCtx.Config, nil + } + + // Legacy path-specific loading s, err := LoadSecrets() if err != nil { return nil, nil, errors.WithContext(err, "loading secrets") @@ -18,3 +30,14 @@ func LoadAll(configPath string) (*Secrets, *Config, error) { return s, c, nil } + +// ValidateEnvironment checks if the provided environment is valid +func ValidateEnvironment(env string) bool { + validEnvs := map[string]bool{ + "development": true, + "staging": true, + "production": true, + "test": true, + } + return validEnvs[env] +} 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..4e9c1dc 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,6 +3,7 @@ package config import ( "encoding/json" "os" + "strings" "atlas-sdk-go/internal/errors" ) @@ -37,11 +38,42 @@ func LoadConfig(path string) (*Config, error) { return nil, errors.WithContext(err, "parsing configuration file") } + if config.OrgID == "" { + return nil, &errors.ValidationError{ + Message: "organization ID is required in configuration", + } + } if config.ProjectID == "" { return nil, &errors.ValidationError{ Message: "project ID is required in configuration", } } + if config.HostName == "" { + if host, _, ok := strings.Cut(config.ProcessID, ":"); ok { + config.HostName = host + } else { + return nil, &errors.ValidationError{ + Message: "process ID must be in the format 'hostname:port'", + } + } + } + return &config, nil } + +func (c *Config) Validate(env string) error { + if c.BaseURL == "" { + return &errors.ValidationError{Message: "BaseURL is required"} + } + + // Add environment-specific validation + if env == "production" { + // Stricter validation for production + if strings.Contains(c.BaseURL, "dev") || strings.Contains(c.BaseURL, "test") { + return &errors.ValidationError{Message: "Production environment cannot use development URLs"} + } + } + + return 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 index 2d31065..6f5ba17 100644 --- 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 @@ -7,11 +7,6 @@ import ( "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 diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go @@ -0,0 +1 @@ +package config 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 c35a690..1f89cc0 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 @@ -21,22 +21,21 @@ import ( ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // Use the context-aware version - appCtx, err := config.LoadAppContextWithContext(ctx, "", false) + configPath := "" // Use default config path for environment + explicitEnv := "" // Use default environment + secrets, cfg, err := config.LoadAll(configPath, explicitEnv) if err != nil { errors.ExitWithError("Failed to load configuration", err) } - client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + client, err := auth.NewClient(cfg, secrets) if err != nil { errors.ExitWithError("Failed to initialize authentication client", err) } + ctx := context.Background() p := &admin.ListInvoicesApiParams{ - OrgId: appCtx.Config.OrgID, + OrgId: cfg.OrgID, } fmt.Printf("Fetching pending invoices for organization: %s\n", p.OrgId) 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 ff8f044..4adf6df 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 @@ -5,12 +5,13 @@ package main import ( + "context" + "fmt" + "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" - "context" - "fmt" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) 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 d8be97c..e683200 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 @@ -15,7 +15,6 @@ import ( "atlas-sdk-go/internal/fileutils" "atlas-sdk-go/internal/logs" - "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) 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 03babe6..84725a9 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 @@ -5,13 +5,14 @@ package main import ( + "context" + "encoding/json" + "fmt" + "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" - "context" - "encoding/json" - "fmt" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) 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 0fce6da..c8bfbc7 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 @@ -5,11 +5,12 @@ package main import ( - "atlas-sdk-go/internal/errors" "context" "encoding/json" "fmt" + "atlas-sdk-go/internal/errors" + "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" 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 index 34eb373..b039319 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -5,21 +5,22 @@ package main import ( + "context" + "fmt" + "time" + "atlas-sdk-go/internal/archive" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" "atlas-sdk-go/internal/errors" - "context" - "fmt" - "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration and secrets for the specified environment - explicitEnv := "internal" + // Load application context with configuration and secrets for the environment + explicitEnv := "production" appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) if err != nil { errors.ExitWithError("Failed to load configuration", err) @@ -52,7 +53,7 @@ func main() { fmt.Printf("Analyzing cluster: %s", clusterName) // Step 3: Find collections suitable for archiving - // NOTE: In a production scenario, you would customize the collection analysis logic to match your specific data patterns. + // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) fmt.Printf("Found %d collections eligible for archiving in cluster %s", len(candidates), clusterName) diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go index 89b98be..e64b56f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -1,10 +1,12 @@ package archive import ( - "atlas-sdk-go/internal/errors" "context" "fmt" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal/errors" ) // Candidate represents a collection eligible for archiving 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 a609d49..b3aca90 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 @@ -1,4 +1,4 @@ -package billing_test +package billing import ( "context" diff --git a/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go index 45101c3..693d032 100644 --- a/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go @@ -1,10 +1,11 @@ package clusters import ( - "atlas-sdk-go/internal/errors" "context" "fmt" + "atlas-sdk-go/internal/errors" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) 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 index 021b850..bf0103f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go @@ -2,12 +2,13 @@ package clusters import ( "context" + "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" - "testing" ) func TestListClusterNames_Success(t *testing.T) { diff --git a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go index ca7092f..d390a38 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go @@ -8,8 +8,9 @@ import ( "sync" "time" - "atlas-sdk-go/internal/errors" "github.com/joho/godotenv" + + "atlas-sdk-go/internal/errors" ) var ( 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 fd17263..6e64c77 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 @@ -5,6 +5,8 @@ import ( "testing" "time" + "atlas-sdk-go/internal/metrics" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -78,7 +80,7 @@ func TestFetchProcessMetrics_Unit(t *testing.T) { GetHostMeasurementsExecute(mock.Anything). Return(tc.view, nil, nil).Once() - result, err := FetchProcessMetrics(ctx, mockSvc, &baseProcess) + result, err := metrics.FetchProcessMetrics(ctx, mockSvc, &baseProcess) if tc.wantErr { require.Error(t, err) diff --git a/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go b/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go deleted file mode 100644 index 6c429dc..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/scale/cluster.go +++ /dev/null @@ -1,50 +0,0 @@ -package scale - -import ( - "context" - "fmt" - "go.mongodb.org/atlas-sdk/v20250219001/admin" -) - -// UpdateClusterSize handles the actual scaling operation by calling Atlas API -func UpdateClusterSize(ctx context.Context, api admin.ClustersApi, groupId, clusterName string, - cluster *admin.ClusterDescription20240805, targetSize string) error { - if cluster.ReplicationSpecs == nil || len(*cluster.ReplicationSpecs) == 0 { - return fmt.Errorf("no replication specs found for cluster %s", clusterName) - } - - replicationSpec := (*cluster.ReplicationSpecs)[0] - if replicationSpec.RegionConfigs == nil || len(*replicationSpec.RegionConfigs) == 0 { - return fmt.Errorf("no region configs found for cluster %s", clusterName) - } - - regionConfig := (*replicationSpec.RegionConfigs)[0] - - // Create update request - updateRequest := &admin.ClusterDescription20240805{ - ReplicationSpecs: &[]admin.ReplicationSpec20240805{ - { - Id: replicationSpec.Id, - RegionConfigs: &[]admin.CloudRegionConfig20240805{ - { - // Keep the same provider and region names - ProviderName: regionConfig.ProviderName, - RegionName: regionConfig.RegionName, - ElectableSpecs: &admin.HardwareSpec20240805{ - // Update the instance size to the target size - InstanceSize: admin.PtrString(targetSize), - }, - }, - }, - }, - }, - } - - // Execute the update API call - _, _, err := api.UpdateCluster(ctx, groupId, clusterName, updateRequest).Execute() - if err != nil { - return fmt.Errorf("failed to scale cluster %s: %w", clusterName, err) - } - - return nil -} diff --git a/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go b/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go deleted file mode 100644 index 094e427..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/scale/cluster_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package scale_test - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "go.mongodb.org/atlas-sdk/v20250219001/admin" -) - -// MockClustersApi mocks the ClustersApi interface -type MockClustersApi struct { - mock.Mock -} - -// UpdateCluster mocks the UpdateCluster method -func (m *MockClustersApi) UpdateCluster(ctx context.Context, groupId string, clusterName string, clusterRequest *admin.ClusterDescription20240805) admin.ApiUpdateClusterRequest { - args := m.Called(ctx, groupId, clusterName, clusterRequest) - return args.Get(0).(admin.ApiUpdateClusterRequest) -} - -// MockApiUpdateClusterRequest mocks the ApiUpdateClusterRequest -type MockApiUpdateClusterRequest struct { - mock.Mock -} - -// Execute mocks the Execute method -func (m *MockApiUpdateClusterRequest) Execute() (*admin.ClusterDescription20240805, *http.Response, error) { - args := m.Called() - if args.Get(0) == nil { - return nil, args.Get(1).(*http.Response), args.Error(2) - } - return args.Get(0).(*admin.ClusterDescription20240805), args.Get(1).(*http.Response), args.Error(2) -} - -func SuccessfulClusterSizeUpdate(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - mockRequest := new(MockApiUpdateClusterRequest) - - replicationSpecs := []admin.ReplicationSpec20240805{ - { - Id: admin.PtrString("rs1"), - RegionConfigs: &[]admin.CloudRegionConfig20240805{ - { - ProviderName: admin.PtrString("AWS"), - RegionName: admin.PtrString("US_EAST_1"), - ElectableSpecs: &admin.HardwareSpec20240805{ - InstanceSize: admin.PtrString("M10"), - }, - }, - }, - }, - } - - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: &replicationSpecs, - } - - mockRequest.On("Execute").Return(&admin.ClusterDescription20240805{}, nil, nil) - mockApi.On("UpdateCluster", mock.Anything, "groupId", "clusterName", mock.Anything).Return(mockRequest) - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.NoError(t, err) - mockApi.AssertExpectations(t) - mockRequest.AssertExpectations(t) -} - -func FailsWhenNoReplicationSpecs(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: nil, - } - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "no replication specs found") -} - -func FailsWhenEmptyReplicationSpecs(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - emptySpecs := []admin.ReplicationSpec20240805{} - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: &emptySpecs, - } - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "no replication specs found") -} - -func FailsWhenNoRegionConfigs(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - replicationSpecs := []admin.ReplicationSpec20240805{ - { - Id: admin.PtrString("rs1"), - RegionConfigs: nil, - }, - } - - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: &replicationSpecs, - } - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "no region configs found") -} - -func FailsWhenEmptyRegionConfigs(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - emptyRegions := []admin.CloudRegionConfig20240805{} - replicationSpecs := []admin.ReplicationSpec20240805{ - { - Id: admin.PtrString("rs1"), - RegionConfigs: &emptyRegions, - }, - } - - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: &replicationSpecs, - } - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "no region configs found") -} - -func FailsWhenApiCallErrors(t *testing.T) { - // Setup - mockApi := new(MockClustersApi) - mockRequest := new(MockApiUpdateClusterRequest) - - replicationSpecs := []admin.ReplicationSpec20240805{ - { - Id: admin.PtrString("rs1"), - RegionConfigs: &[]admin.CloudRegionConfig20240805{ - { - ProviderName: admin.PtrString("AWS"), - RegionName: admin.PtrString("US_EAST_1"), - }, - }, - }, - } - - cluster := &admin.ClusterDescription20240805{ - ReplicationSpecs: &replicationSpecs, - } - - mockRequest.On("Execute").Return(nil, nil, errors.New("API error")) - mockApi.On("UpdateCluster", mock.Anything, "groupId", "clusterName", mock.Anything).Return(mockRequest) - - // Act - err := UpdateClusterSize(context.Background(), mockApi, "groupId", "clusterName", cluster, "M20") - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to scale cluster") - mockApi.AssertExpectations(t) - mockRequest.AssertExpectations(t) -} From ff1424d8566a2b809cde39a031c3f82fd15c5254 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 14 Aug 2025 13:58:37 -0400 Subject: [PATCH 05/19] Update README --- usage-examples/go/atlas-sdk-go/CHANGELOG.md | 4 ++++ usage-examples/go/atlas-sdk-go/README.md | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) 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..a2b8658 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 ├── internal # Shared utilities and helpers +│ ├── archive/ │ ├── auth/ │ ├── billing/ +│ ├── clusters/ │ ├── config/ │ ├── data/ │ ├── errors/ @@ -56,10 +59,13 @@ 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, to 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 + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" + CONFIG_PATH="/configs" + APP_ENV="dev" ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. @@ -121,6 +127,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). From 76623238d3eafdc079eff0d664e1aa4f8e053c26 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 14 Aug 2025 15:44:56 -0400 Subject: [PATCH 06/19] Fix up comments --- .../atlas-sdk-go/main.snippet.line-items.go | 4 +- .../go/atlas-sdk-go/project-copy/CHANGELOG.md | 4 + .../go/atlas-sdk-go/project-copy/README.md | 21 +++- .../examples/billing/line_items/main.go | 12 +- .../internal/config/appcontext.go | 103 ++---------------- .../internal/config/loadconfig.go | 6 +- .../project-copy/internal/config/loadenv.go | 3 + .../project-copy/internal/config/utils.go | 1 - .../examples/billing/line_items/main.go | 12 +- .../internal/config/appcontext.go | 48 ++++---- .../internal/config/loadconfig.go | 6 +- .../atlas-sdk-go/internal/config/loadenv.go | 3 + .../go/atlas-sdk-go/internal/config/utils.go | 1 - .../internal/metrics/process_test.go | 21 +--- 14 files changed, 81 insertions(+), 164 deletions(-) delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/config/utils.go 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 4894754..6d459fe 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 @@ -4,10 +4,8 @@ package main import ( "context" "fmt" - "log" - "time" - "go.mongodb.org/atlas-sdk/v20250219001/admin" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" 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 08d81a3..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,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/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..a2b8658 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 ├── internal # Shared utilities and helpers +│ ├── archive/ │ ├── auth/ │ ├── billing/ +│ ├── clusters/ │ ├── config/ │ ├── data/ │ ├── errors/ @@ -56,10 +59,13 @@ 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, to 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 + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" + CONFIG_PATH="/configs" + APP_ENV="dev" ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. @@ -121,6 +127,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/examples/billing/line_items/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/examples/billing/line_items/main.go index db8005a..403cc09 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 @@ -3,10 +3,8 @@ package main import ( "context" "fmt" - "log" - "time" - "go.mongodb.org/atlas-sdk/v20250219001/admin" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -97,11 +95,3 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { fmt.Printf("Exported billing data to %s\n", csvPath) } -// With timeout -func loadConfigWithTimeout() (*config.AppContext, error) { - // Create a context with a 5-second timeout - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return config.LoadAppContextWithContext(ctx, "", false) -} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go index 6164d93..868dbce 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go @@ -13,6 +13,8 @@ import ( "atlas-sdk-go/internal/errors" ) +// Package config provides application context management, including environment-specific configurations +// and caching mechanisms to optimize performance and reduce redundant loading of configurations. var ( cachedAppContext *AppContext cachedAppContextTime time.Time @@ -41,12 +43,9 @@ type AppContext struct { // If strictValidation is true, invalid environments will return an error func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { // Environment resolution priority: - // 1. Explicitly passed environment parameter - // 2. APP_ENV environment variable - // 3. Default to "development" - // - // Special environments: - // - "test": Used for automated testing, loads from .env.test and configs/config.test.json + // 1. An explicitly passed environment parameter + // 2. An APP_ENV environment variable + // 3. Otherwise, defaults to "development" // Determine environment env := explicitEnv @@ -68,7 +67,6 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err } cacheMutex.RUnlock() - // Validate environment if !ValidateEnvironment(env) { if strictValidation { return nil, fmt.Errorf("invalid environment: %s", env) @@ -103,7 +101,6 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err log.Printf("Loading configuration for environment: %s", env) log.Printf("Using config file: %s", configPath) - // Load secrets and config secrets, err := LoadSecrets() if err != nil { return nil, errors.WithContext(err, "loading secrets") @@ -114,8 +111,7 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err return nil, errors.WithContext(err, "loading config") } - // Validate config with environment context - if err := config.Validate(env); err != nil { + if err = config.Validate(env); err != nil { return nil, errors.WithContext(err, "validating config") } @@ -138,7 +134,9 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err return appCtx, nil } -// LoadAppContextWithContext Add context support to handle timeouts and cancellation +// LoadAppContextWithContext initializes application context with environment-specific configuration using a provided context for cancellation support. +// If explicitEnv is provided, it overrides the APP_ENV environment variable +// If strictValidation is true, invalid environments will return an error func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { // Use context for potential operations that may need cancellation select { @@ -168,7 +166,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa } cacheMutex.RUnlock() - // Rest of implementation mirrors LoadAppContext but with context checks + // The implementation mirrors LoadAppContext but with context checks if !ValidateEnvironment(env) { if strictValidation { return nil, fmt.Errorf("invalid environment: %s", env) @@ -176,11 +174,6 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa log.Printf("Warning: Unexpected environment '%s' may cause issues", env) } - // Special handling for test environment - if env == "test" { - log.Printf("Using test environment - ensure test fixtures are available") - } - // Add context check before expensive operations select { case <-ctx.Done(): @@ -188,7 +181,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa default: } - // Load environment files with improved approach + // Load environment files envFiles := []string{ fmt.Sprintf(".env.%s", env), ".env", @@ -223,7 +216,6 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa log.Printf("Loading configuration for environment: %s", env) log.Printf("Using config file: %s", configPath) - // Load secrets and config secrets, err := LoadSecrets() if err != nil { return nil, errors.WithContext(err, "loading secrets") @@ -234,8 +226,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa return nil, errors.WithContext(err, "loading config") } - // Validate config with environment context - if err := config.Validate(env); err != nil { + if err = config.Validate(env); err != nil { return nil, errors.WithContext(err, "validating config") } @@ -258,73 +249,3 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa return appCtx, nil } -// getTestConfiguration provides specialized test configuration -func getTestConfiguration() (*Config, *Secrets, error) { - // Check if specific test config file exists - testConfigFile := "configs/config.test.json" - if _, err := os.Stat(testConfigFile); err == nil { - config, err := LoadConfig(testConfigFile) - if err != nil { - return nil, nil, err - } - - // Still use mock secrets to avoid requiring real credentials - mockSecrets := &Secrets{ - ServiceAccountID: "test-service-account-id", - ServiceAccountSecret: "test-service-account-secret", - } - - return config, mockSecrets, nil - } - - // Fall back to fully mocked configuration - return &Config{ - BaseURL: "https://cloud-mock.mongodb.com", - OrgID: "test-org-id", - ProjectID: "test-project-id", - ClusterName: "TestCluster", - ProcessID: "test-cluster-shard-00-00.test.mongodb.net:27017", - HostName: "test-cluster-shard-00-00.test.mongodb.net", - }, &Secrets{ - ServiceAccountID: "test-service-account-id", - ServiceAccountSecret: "test-service-account-secret", - }, nil -} - -// Add diff support for testing -func (a *AppContext) Diff(other *AppContext) []string { - var differences []string - - if a.Environment != other.Environment { - differences = append(differences, fmt.Sprintf("Environment: %s vs %s", - a.Environment, other.Environment)) - } - - // Compare important config fields - if a.Config.BaseURL != other.Config.BaseURL { - differences = append(differences, fmt.Sprintf("BaseURL: %s vs %s", - a.Config.BaseURL, other.Config.BaseURL)) - } - - if a.Config.OrgID != other.Config.OrgID { - differences = append(differences, fmt.Sprintf("OrgID: %s vs %s", - a.Config.OrgID, other.Config.OrgID)) - } - - if a.Config.ProjectID != other.Config.ProjectID { - differences = append(differences, fmt.Sprintf("ProjectID: %s vs %s", - a.Config.ProjectID, other.Config.ProjectID)) - } - - if a.Config.ClusterName != other.Config.ClusterName { - differences = append(differences, fmt.Sprintf("ClusterName: %s vs %s", - a.Config.ClusterName, other.Config.ClusterName)) - } - - if a.Config.ProcessID != other.Config.ProcessID { - differences = append(differences, fmt.Sprintf("ProcessID: %s vs %s", - a.Config.ProcessID, other.Config.ProcessID)) - } - - return differences -} 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 4e9c1dc..29d7c4f 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 @@ -8,6 +8,7 @@ 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"` @@ -18,6 +19,7 @@ type Config struct { } // LoadConfig reads a JSON configuration file and returns a Config struct +// It validates required fields and returns an error if any validation fails. func LoadConfig(path string) (*Config, error) { if path == "" { return nil, &errors.ValidationError{ @@ -34,7 +36,7 @@ func LoadConfig(path string) (*Config, error) { } var config Config - if err := json.Unmarshal(data, &config); err != nil { + if err = json.Unmarshal(data, &config); err != nil { return nil, errors.WithContext(err, "parsing configuration file") } @@ -62,6 +64,8 @@ func LoadConfig(path string) (*Config, error) { return &config, nil } +// Validate checks the configuration for required fields and environment-specific rules +// It returns an error if any validation fails. func (c *Config) Validate(env string) error { if c.BaseURL == "" { return &errors.ValidationError{Message: "BaseURL is required"} 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 index 6f5ba17..8444559 100644 --- 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 @@ -7,11 +7,14 @@ import ( "atlas-sdk-go/internal/errors" ) +// Environment variable names for service account credentials type Secrets struct { ServiceAccountID string ServiceAccountSecret string } +// LoadSecrets loads the required secrets from environment variables. +// It returns a Secrets struct or an error if any required variable is missing. func LoadSecrets() (*Secrets, error) { s := &Secrets{} var missing []string diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go deleted file mode 100644 index d912156..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/utils.go +++ /dev/null @@ -1 +0,0 @@ -package config 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 1f89cc0..654714c 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 @@ -7,10 +7,8 @@ package main import ( "context" "fmt" - "log" - "time" - "go.mongodb.org/atlas-sdk/v20250219001/admin" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -119,11 +117,3 @@ func exportInvoicesToCSV(details []billing.Detail, outDir, prefix string) { // Exported billing data to invoices/pending_5f7a9ec7d78fc03b42959328.json // Exported billing data to invoices/pending_5f7a9ec7d78fc03b42959328.csv // :state-remove-end: [copy] -// With timeout -func loadConfigWithTimeout() (*config.AppContext, error) { - // Create a context with a 5-second timeout - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return config.LoadAppContextWithContext(ctx, "", false) -} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go index d390a38..de5e3c9 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go @@ -13,6 +13,8 @@ import ( "atlas-sdk-go/internal/errors" ) +// Package config provides application context management, including environment-specific configurations +// and caching mechanisms to optimize performance and reduce redundant loading of configurations. var ( cachedAppContext *AppContext cachedAppContextTime time.Time @@ -41,12 +43,13 @@ type AppContext struct { // If strictValidation is true, invalid environments will return an error func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { // Environment resolution priority: - // 1. Explicitly passed environment parameter - // 2. APP_ENV environment variable - // 3. Default to "development" - // + // 1. An explicitly passed environment parameter + // 2. An APP_ENV environment variable + // 3. Otherwise, defaults to "development" + // :state-remove-start: copy // Special environments: // - "test": Used for automated testing, loads from .env.test and configs/config.test.json + // :state-remove-end: // Determine environment env := explicitEnv @@ -68,7 +71,6 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err } cacheMutex.RUnlock() - // Validate environment if !ValidateEnvironment(env) { if strictValidation { return nil, fmt.Errorf("invalid environment: %s", env) @@ -76,9 +78,9 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err log.Printf("Warning: Unexpected environment '%s' may cause issues", env) } // :state-remove-start: copy - // Special handling for test environment + // Special handling for internal-only test environment if env == "test" { - log.Printf("Using test environment - ensure test fixtures are available") + log.Printf("Using test environment") // If TEST_MOCK_CONFIG is set, use mock configuration if os.Getenv("TEST_MOCK_CONFIG") == "true" { @@ -131,7 +133,6 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err log.Printf("Loading configuration for environment: %s", env) log.Printf("Using config file: %s", configPath) - // Load secrets and config secrets, err := LoadSecrets() if err != nil { return nil, errors.WithContext(err, "loading secrets") @@ -142,8 +143,7 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err return nil, errors.WithContext(err, "loading config") } - // Validate config with environment context - if err := config.Validate(env); err != nil { + if err = config.Validate(env); err != nil { return nil, errors.WithContext(err, "validating config") } @@ -166,7 +166,9 @@ func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, err return appCtx, nil } -// LoadAppContextWithContext Add context support to handle timeouts and cancellation +// LoadAppContextWithContext initializes application context with environment-specific configuration using a provided context for cancellation support. +// If explicitEnv is provided, it overrides the APP_ENV environment variable +// If strictValidation is true, invalid environments will return an error func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { // Use context for potential operations that may need cancellation select { @@ -196,7 +198,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa } cacheMutex.RUnlock() - // Rest of implementation mirrors LoadAppContext but with context checks + // The implementation mirrors LoadAppContext but with context checks if !ValidateEnvironment(env) { if strictValidation { return nil, fmt.Errorf("invalid environment: %s", env) @@ -204,11 +206,12 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa log.Printf("Warning: Unexpected environment '%s' may cause issues", env) } + // :state-remove-start: copy // Special handling for test environment if env == "test" { log.Printf("Using test environment - ensure test fixtures are available") } - + // :state-remove-end: // Add context check before expensive operations select { case <-ctx.Done(): @@ -216,7 +219,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa default: } - // Load environment files with improved approach + // Load environment files envFiles := []string{ fmt.Sprintf(".env.%s", env), ".env", @@ -251,7 +254,6 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa log.Printf("Loading configuration for environment: %s", env) log.Printf("Using config file: %s", configPath) - // Load secrets and config secrets, err := LoadSecrets() if err != nil { return nil, errors.WithContext(err, "loading secrets") @@ -262,8 +264,7 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa return nil, errors.WithContext(err, "loading config") } - // Validate config with environment context - if err := config.Validate(env); err != nil { + if err = config.Validate(env); err != nil { return nil, errors.WithContext(err, "validating config") } @@ -286,14 +287,16 @@ func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictVa return appCtx, nil } -// getTestConfiguration provides specialized test configuration +// :state-remove-start: copy +// getTestConfiguration is an internal-only testing function that checks for a specific test configuration file +// and returns a mock configuration if it exists, otherwise returns a fully mocked configuration. func getTestConfiguration() (*Config, *Secrets, error) { // Check if specific test config file exists testConfigFile := "configs/config.test.json" if _, err := os.Stat(testConfigFile); err == nil { - config, err := LoadConfig(testConfigFile) - if err != nil { - return nil, nil, err + config, cfgErr := LoadConfig(testConfigFile) + if cfgErr != nil { + return nil, nil, cfgErr } // Still use mock secrets to avoid requiring real credentials @@ -319,7 +322,6 @@ func getTestConfiguration() (*Config, *Secrets, error) { }, nil } -// Add diff support for testing func (a *AppContext) Diff(other *AppContext) []string { var differences []string @@ -356,3 +358,5 @@ func (a *AppContext) Diff(other *AppContext) []string { return differences } + +// :state-remove-end: 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 4e9c1dc..29d7c4f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go @@ -8,6 +8,7 @@ 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"` @@ -18,6 +19,7 @@ type Config struct { } // LoadConfig reads a JSON configuration file and returns a Config struct +// It validates required fields and returns an error if any validation fails. func LoadConfig(path string) (*Config, error) { if path == "" { return nil, &errors.ValidationError{ @@ -34,7 +36,7 @@ func LoadConfig(path string) (*Config, error) { } var config Config - if err := json.Unmarshal(data, &config); err != nil { + if err = json.Unmarshal(data, &config); err != nil { return nil, errors.WithContext(err, "parsing configuration file") } @@ -62,6 +64,8 @@ func LoadConfig(path string) (*Config, error) { return &config, nil } +// Validate checks the configuration for required fields and environment-specific rules +// It returns an error if any validation fails. func (c *Config) Validate(env string) error { if c.BaseURL == "" { return &errors.ValidationError{Message: "BaseURL is required"} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go index 6f5ba17..8444559 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go @@ -7,11 +7,14 @@ import ( "atlas-sdk-go/internal/errors" ) +// Environment variable names for service account credentials type Secrets struct { ServiceAccountID string ServiceAccountSecret string } +// LoadSecrets loads the required secrets from environment variables. +// It returns a Secrets struct or an error if any required variable is missing. func LoadSecrets() (*Secrets, error) { s := &Secrets{} var missing []string diff --git a/usage-examples/go/atlas-sdk-go/internal/config/utils.go b/usage-examples/go/atlas-sdk-go/internal/config/utils.go deleted file mode 100644 index d912156..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/config/utils.go +++ /dev/null @@ -1 +0,0 @@ -package config 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 6e64c77..f82026e 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 @@ -1,29 +1,14 @@ -package metrics_test +package metrics import ( "context" - "testing" - "time" - - "atlas-sdk-go/internal/metrics" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.mongodb.org/atlas-sdk/v20250219001/admin" "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" + "testing" ) -// 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 // ----------------------------------------------------------------------------- @@ -80,7 +65,7 @@ func TestFetchProcessMetrics_Unit(t *testing.T) { GetHostMeasurementsExecute(mock.Anything). Return(tc.view, nil, nil).Once() - result, err := metrics.FetchProcessMetrics(ctx, mockSvc, &baseProcess) + result, err := FetchProcessMetrics(ctx, mockSvc, &baseProcess) if tc.wantErr { require.Error(t, err) From 901d194bb657fdb47b187ae175a00d7c23b23461 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Mon, 18 Aug 2025 18:12:26 -0400 Subject: [PATCH 07/19] Simplify config internals, and move out env load to main --- .../examples/billing/historical/main.go | 45 ++- .../examples/billing/line_items/main.go | 49 ++- .../examples/billing/linked_orgs/main.go | 21 +- .../examples/monitoring/logs/main.go | 29 +- .../examples/monitoring/metrics_disk/main.go | 24 +- .../monitoring/metrics_process/main.go | 25 +- .../examples/performance/archiving/main.go | 43 ++- .../atlas-sdk-go/internal/archive/analyze.go | 20 +- .../go/atlas-sdk-go/internal/auth/client.go | 9 +- .../internal/config/appcontext.go | 356 ++---------------- .../atlas-sdk-go/internal/config/loadall.go | 52 +-- .../internal/config/loadconfig.go | 40 +- .../atlas-sdk-go/internal/config/loadenv.go | 41 +- .../internal/data/export/formats_test.go | 6 +- .../go/atlas-sdk-go/internal/errors/utils.go | 7 - .../go/atlas-sdk-go/internal/fileutils/io.go | 2 +- .../internal/metrics/process_test.go | 3 +- 17 files changed, 252 insertions(+), 520 deletions(-) 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 69aa8f7..c37834f 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,30 +14,31 @@ 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" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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 +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 { @@ -60,8 +61,14 @@ 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 { @@ -71,21 +78,22 @@ func main() { // :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 @@ -99,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 654714c..55cabd1 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 @@ -7,31 +7,34 @@ package main import ( "context" "fmt" - "go.mongodb.org/atlas-sdk/v20250219001/admin" "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" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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, } @@ -40,7 +43,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 { @@ -53,33 +56,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 @@ -101,9 +111,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 4adf6df..f1a097e 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 @@ -7,29 +7,32 @@ package main import ( "context" "fmt" + "log" "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() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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 +41,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 e683200..b69f8cc 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,28 +11,29 @@ 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" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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, @@ -43,7 +44,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) @@ -53,22 +54,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 84725a9..f6f345d 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 @@ -8,30 +8,32 @@ import ( "context" "encoding/json" "fmt" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "development" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("development") // Cast string to config.Environment + configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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 +45,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 c8bfbc7..995ef99 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 @@ -8,32 +8,33 @@ import ( "context" "encoding/json" "fmt" - - "atlas-sdk-go/internal/errors" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "production" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers 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 +52,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 index b039319..d350946 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -7,76 +7,81 @@ package main import ( "context" "fmt" + "log" "time" "atlas-sdk-go/internal/archive" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" + + "github.com/joho/godotenv" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration and secrets for the environment - explicitEnv := "production" - appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) + _ = godotenv.Load() + + envName := config.Environment("") // Use empty string to load from environment variables + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers if err != nil { - errors.ExitWithError("Failed to initialize Atlas client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - projectID := appCtx.Config.ProjectID + projectID := cfg.ProjectID if projectID == "" { - errors.ExitWithError("Project ID not found in configuration", nil) + log.Fatal("Failed to find Project ID in configuration") } - fmt.Println("Starting archive analysis for project:", projectID) + fmt.Printf("Starting archive analysis for project: %s\n", projectID) // Step 1: List all clusters in the project clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() if err != nil { - errors.ExitWithError("Failed to list clusters", err) + log.Fatalf("Failed to list clusters: %v", err) } - fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + fmt.Printf("Found %d clusters to analyze\n", len(clusters.GetResults())) // Step 2: Process each cluster failedArchives := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s", clusterName) + fmt.Printf("Analyzing cluster: %s\n", clusterName) // Step 3: Find collections suitable for archiving - // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // NOTE: This example passes example database/collections. + // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("Found %d collections eligible for archiving in cluster %s", + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", len(candidates), clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("Configuring archive for %s.%s", + fmt.Printf("\nConfiguring archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("Failed to configure archive: %v", configureErr) + fmt.Printf("\nFailed to configure archive: %v", configureErr) failedArchives++ continue } - fmt.Printf("Successfully configured online archive for %s.%s", + fmt.Printf("\nSuccessfully configured online archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed", failedArchives) + fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) } fmt.Println("Archive analysis and configuration completed") diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go index e64b56f..c4299fd 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -41,14 +41,17 @@ func DefaultOptions() Options { } } -// CollectionsForArchiving demonstrates how to identify collections suitable for archiving -// In a real implementation, you would analyze collection data patterns -// and determine which collections are eligible based on criteria such as size, age, and access patterns. -// This function returns a list of candidates that meet the archiving criteria -// Note: This is a simplified example and should be customized for your specific use case +type ExpireAfterDays struct { + // NOTE: this placeholder struct can be extended to include more complex rules if needed + ExpireAfterDays int `json:"expireAfterDays,omitempty"` +} + +// CollectionsForArchiving identifies collections suitable for archiving as a simplified example for demonstration purposes. +// This function returns a list of Candidates that meet the archiving criteria +// NOTE: In a real implementation, you would determine which collections are eligible based on criteria analysis such as size, age, and access patterns. func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { - // For demonstration purposes, we specify example candidates + // For demonstration purposes, we specify example Candidates return []Candidate{ { DatabaseName: "sample_analytics", @@ -69,11 +72,6 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, } } -type ExpireAfterDays struct { - // NOTE: this placeholder struct can be extended to include more complex rules if needed - ExpireAfterDays int `json:"expireAfterDays,omitempty"` -} - // ValidateCandidate ensures the archiving candidate meets requirements func ValidateCandidate(candidate Candidate, opts Options) error { if candidate.DatabaseName == "" || candidate.CollectionName == "" { 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..0422826 100644 --- a/usage-examples/go/atlas-sdk-go/internal/auth/client.go +++ b/usage-examples/go/atlas-sdk-go/internal/auth/client.go @@ -11,7 +11,7 @@ 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) { +func NewClient(ctx context.Context, cfg *config.Config, secrets *config.Secrets) (*admin.APIClient, error) { if cfg == nil { return nil, &errors.ValidationError{Message: "config cannot be nil"} } @@ -22,9 +22,10 @@ func NewClient(cfg *config.Config, secrets *config.Secrets) (*admin.APIClient, e 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/config/appcontext.go b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go index de5e3c9..ca1414c 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go @@ -1,362 +1,70 @@ package config import ( - "context" "fmt" - "log" "os" - "sync" - "time" - - "github.com/joho/godotenv" "atlas-sdk-go/internal/errors" ) -// Package config provides application context management, including environment-specific configurations -// and caching mechanisms to optimize performance and reduce redundant loading of configurations. -var ( - cachedAppContext *AppContext - cachedAppContextTime time.Time - cacheTTL = 5 * time.Minute - cacheMutex sync.RWMutex +const ( + envAppEnv = "APP_ENV" // Environment variable for the application environment + envConfigPath = "APP_CONFIG_PATH" // Environment variable for the configuration file path + defaultConfigFormat = "configs/config.%s.json" ) -// Constants for environment variables and default paths const ( - EnvAppEnv = "APP_ENV" - EnvConfigPath = "ATLAS_CONFIG_PATH" - EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" - EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" - DefaultConfigFormat = "configs/config.%s.json" + envDevelopment Environment = "development" + envStaging Environment = "test" + envProduction Environment = "production" ) // AppContext contains all environment-specific configurations type AppContext struct { - Environment string - Config *Config - Secrets *Secrets + environment Environment + config Config + secrets Secrets } -// LoadAppContext initializes application context with environment-specific configuration -// If explicitEnv is provided, it overrides the APP_ENV environment variable -// If strictValidation is true, invalid environments will return an error -func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { - // Environment resolution priority: - // 1. An explicitly passed environment parameter - // 2. An APP_ENV environment variable - // 3. Otherwise, defaults to "development" - // :state-remove-start: copy - // Special environments: - // - "test": Used for automated testing, loads from .env.test and configs/config.test.json - // :state-remove-end: +type Environment string - // Determine environment +// LoadAppContext loads environment-specific configuration +// Returns error if no environment is specified +func LoadAppContext(explicitEnv Environment) (AppContext, error) { env := explicitEnv if env == "" { - env = os.Getenv(EnvAppEnv) - if env == "" { - env = "development" - } - } - - // Check cache first using the resolved environment - cacheMutex.RLock() - if cachedAppContext != nil && - cachedAppContext.Environment == env && - time.Since(cachedAppContextTime) < cacheTTL { - cached := cachedAppContext - cacheMutex.RUnlock() - return cached, nil - } - cacheMutex.RUnlock() - - if !ValidateEnvironment(env) { - if strictValidation { - return nil, fmt.Errorf("invalid environment: %s", env) - } - log.Printf("Warning: Unexpected environment '%s' may cause issues", env) - } - // :state-remove-start: copy - // Special handling for internal-only test environment - if env == "test" { - log.Printf("Using test environment") - - // If TEST_MOCK_CONFIG is set, use mock configuration - if os.Getenv("TEST_MOCK_CONFIG") == "true" { - mockConfig, mockSecrets, err := getTestConfiguration() - if err != nil { - return nil, errors.WithContext(err, "loading test configuration") - } - - appCtx := &AppContext{ - Environment: env, - Config: mockConfig, - Secrets: mockSecrets, - } - - // Cache the result - cacheMutex.Lock() - cachedAppContext = appCtx - cachedAppContextTime = time.Now() - cacheMutex.Unlock() - - return appCtx, nil - } + env = Environment(os.Getenv(envAppEnv)) } - // :state-remove-end: - // Load environment files - envFiles := []string{ - fmt.Sprintf(".env.%s", env), - ".env", + // Validate environment + if !ValidateEnvironment(string(env)) { + return AppContext{}, fmt.Errorf("invalid environment: %s", env) } - loaded := false - for _, file := range envFiles { - if err := godotenv.Load(file); err == nil { - log.Printf("Loaded environment from %s", file) - loaded = true - break - } - } - - if !loaded { - log.Printf("Warning: No environment files found, using system environment variables only") - } - - // Get config path from env var or use default - configPath := os.Getenv(EnvConfigPath) + // Determine config path + configPath := os.Getenv(envConfigPath) if configPath == "" { - configPath = fmt.Sprintf(DefaultConfigFormat, env) - } - - log.Printf("Loading configuration for environment: %s", env) - log.Printf("Using config file: %s", configPath) - - secrets, err := LoadSecrets() - if err != nil { - return nil, errors.WithContext(err, "loading secrets") - } - - config, err := LoadConfig(configPath) - if err != nil { - return nil, errors.WithContext(err, "loading config") + configPath = fmt.Sprintf(defaultConfigFormat, env) } - if err = config.Validate(env); err != nil { - return nil, errors.WithContext(err, "validating config") - } - - log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", - env, config.BaseURL, config.OrgID) - - // Create and initialize the AppContext - appCtx := &AppContext{ - Environment: env, - Config: config, - Secrets: secrets, - } - - // Cache the result - cacheMutex.Lock() - cachedAppContext = appCtx - cachedAppContextTime = time.Now() - cacheMutex.Unlock() - - return appCtx, nil -} - -// LoadAppContextWithContext initializes application context with environment-specific configuration using a provided context for cancellation support. -// If explicitEnv is provided, it overrides the APP_ENV environment variable -// If strictValidation is true, invalid environments will return an error -func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { - // Use context for potential operations that may need cancellation - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled while loading configuration: %w", ctx.Err()) - default: - // Continue with loading + // Check if config file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return AppContext{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} } - // Determine environment - env := explicitEnv - if env == "" { - env = os.Getenv(EnvAppEnv) - if env == "" { - env = "development" - } - } - - // Check cache first using the resolved environment - cacheMutex.RLock() - if cachedAppContext != nil && - cachedAppContext.Environment == env && - time.Since(cachedAppContextTime) < cacheTTL { - cached := cachedAppContext - cacheMutex.RUnlock() - return cached, nil - } - cacheMutex.RUnlock() - - // The implementation mirrors LoadAppContext but with context checks - if !ValidateEnvironment(env) { - if strictValidation { - return nil, fmt.Errorf("invalid environment: %s", env) - } - log.Printf("Warning: Unexpected environment '%s' may cause issues", env) - } - - // :state-remove-start: copy - // Special handling for test environment - if env == "test" { - log.Printf("Using test environment - ensure test fixtures are available") - } - // :state-remove-end: - // Add context check before expensive operations - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled while loading environment files: %w", ctx.Err()) - default: - } - - // Load environment files - envFiles := []string{ - fmt.Sprintf(".env.%s", env), - ".env", - } - - loaded := false - for _, file := range envFiles { - if err := godotenv.Load(file); err == nil { - log.Printf("Loaded environment from %s", file) - loaded = true - break - } - } - - if !loaded { - log.Printf("Warning: No environment files found, using system environment variables only") - } - - // Get config path from env var or use default - configPath := os.Getenv(EnvConfigPath) - if configPath == "" { - configPath = fmt.Sprintf(DefaultConfigFormat, env) - } - - // Add context check before loading secrets and config - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled before loading secrets/config: %w", ctx.Err()) - default: - } - - log.Printf("Loading configuration for environment: %s", env) - log.Printf("Using config file: %s", configPath) - + // Load secrets and config secrets, err := LoadSecrets() if err != nil { - return nil, errors.WithContext(err, "loading secrets") + return AppContext{}, errors.WithContext(err, "loading secrets") } config, err := LoadConfig(configPath) if err != nil { - return nil, errors.WithContext(err, "loading config") - } - - if err = config.Validate(env); err != nil { - return nil, errors.WithContext(err, "validating config") - } - - log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", - env, config.BaseURL, config.OrgID) - - // Create and initialize the AppContext - appCtx := &AppContext{ - Environment: env, - Config: config, - Secrets: secrets, + return AppContext{}, errors.WithContext(err, "loading config") } - // Cache the result - cacheMutex.Lock() - cachedAppContext = appCtx - cachedAppContextTime = time.Now() - cacheMutex.Unlock() - - return appCtx, nil + return AppContext{ + environment: env, + config: config, + secrets: secrets, + }, nil } - -// :state-remove-start: copy -// getTestConfiguration is an internal-only testing function that checks for a specific test configuration file -// and returns a mock configuration if it exists, otherwise returns a fully mocked configuration. -func getTestConfiguration() (*Config, *Secrets, error) { - // Check if specific test config file exists - testConfigFile := "configs/config.test.json" - if _, err := os.Stat(testConfigFile); err == nil { - config, cfgErr := LoadConfig(testConfigFile) - if cfgErr != nil { - return nil, nil, cfgErr - } - - // Still use mock secrets to avoid requiring real credentials - mockSecrets := &Secrets{ - ServiceAccountID: "test-service-account-id", - ServiceAccountSecret: "test-service-account-secret", - } - - return config, mockSecrets, nil - } - - // Fall back to fully mocked configuration - return &Config{ - BaseURL: "https://cloud-mock.mongodb.com", - OrgID: "test-org-id", - ProjectID: "test-project-id", - ClusterName: "TestCluster", - ProcessID: "test-cluster-shard-00-00.test.mongodb.net:27017", - HostName: "test-cluster-shard-00-00.test.mongodb.net", - }, &Secrets{ - ServiceAccountID: "test-service-account-id", - ServiceAccountSecret: "test-service-account-secret", - }, nil -} - -func (a *AppContext) Diff(other *AppContext) []string { - var differences []string - - if a.Environment != other.Environment { - differences = append(differences, fmt.Sprintf("Environment: %s vs %s", - a.Environment, other.Environment)) - } - - // Compare important config fields - if a.Config.BaseURL != other.Config.BaseURL { - differences = append(differences, fmt.Sprintf("BaseURL: %s vs %s", - a.Config.BaseURL, other.Config.BaseURL)) - } - - if a.Config.OrgID != other.Config.OrgID { - differences = append(differences, fmt.Sprintf("OrgID: %s vs %s", - a.Config.OrgID, other.Config.OrgID)) - } - - if a.Config.ProjectID != other.Config.ProjectID { - differences = append(differences, fmt.Sprintf("ProjectID: %s vs %s", - a.Config.ProjectID, other.Config.ProjectID)) - } - - if a.Config.ClusterName != other.Config.ClusterName { - differences = append(differences, fmt.Sprintf("ClusterName: %s vs %s", - a.Config.ClusterName, other.Config.ClusterName)) - } - - if a.Config.ProcessID != other.Config.ProcessID { - differences = append(differences, fmt.Sprintf("ProcessID: %s vs %s", - a.Config.ProcessID, other.Config.ProcessID)) - } - - return differences -} - -// :state-remove-end: 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 1bcc917..b1ebbd8 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -4,40 +4,40 @@ import ( "atlas-sdk-go/internal/errors" ) -// LoadAll loads secrets and config -// If configPath is empty, uses environment-specific loading -// If explicitEnv is provided, it overrides the APP_ENV environment variable -func LoadAll(configPath string, explicitEnv string) (*Secrets, *Config, error) { +// EnvironmentNames defines valid runtime environments +var allowedEnvironments = map[Environment]struct{}{ + envDevelopment: {}, + envStaging: {}, + envProduction: {}, +} + +func ValidateEnvironment(env string) bool { + _, ok := allowedEnvironments[Environment(env)] + return ok +} + +// LoadAll loads configuration for a specific environment +// +// Parameters: +// - envName: Environment name (dev/staging/prod/test); overrides APP_ENV if provided +// - configPath: Optional explicit config file path; if empty, uses environment-based path +// +// Returns secrets, config and any errors encountered during loading +func LoadAll(envName Environment, configPath string) (Secrets, Config, error) { if configPath == "" { - // Use environment-based loading - appCtx, err := LoadAppContext(explicitEnv, false) // Use non-strict validation by default + appCtx, err := LoadAppContext(envName) if err != nil { - return nil, nil, err + return Secrets{}, Config{}, err } - return appCtx.Secrets, appCtx.Config, nil + return appCtx.secrets, appCtx.config, nil // return values, not pointers } - - // Legacy path-specific loading s, err := LoadSecrets() if err != nil { - return nil, nil, errors.WithContext(err, "loading secrets") + return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") } - c, err := LoadConfig(configPath) if err != nil { - return nil, nil, errors.WithContext(err, "loading config") - } - - return s, c, nil -} - -// ValidateEnvironment checks if the provided environment is valid -func ValidateEnvironment(env string) bool { - validEnvs := map[string]bool{ - "development": true, - "staging": true, - "production": true, - "test": true, + return Secrets{}, Config{}, errors.WithContext(err, "loading config") } - return validEnvs[env] + return s, c, nil // return values, not pointers } 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 29d7c4f..efef784 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadconfig.go @@ -14,15 +14,16 @@ type Config struct { 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 // It validates required fields and returns an error if any validation fails. -func LoadConfig(path string) (*Config, error) { +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", } } @@ -30,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") + 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", } } @@ -55,29 +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 -} - -// Validate checks the configuration for required fields and environment-specific rules -// It returns an error if any validation fails. -func (c *Config) Validate(env string) error { - if c.BaseURL == "" { - return &errors.ValidationError{Message: "BaseURL is required"} - } - - // Add environment-specific validation - if env == "production" { - // Stricter validation for production - if strings.Contains(c.BaseURL, "dev") || strings.Contains(c.BaseURL, "test") { - return &errors.ValidationError{Message: "Production environment cannot use development URLs"} - } + if config.BaseURL == "" { + config.BaseURL = "https://cloud.mongodb.com" // Default base URL if not provided } - return nil + 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 index 8444559..e55acb6 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go @@ -1,22 +1,37 @@ package config import ( + "errors" + "fmt" "os" - "strings" +) - "atlas-sdk-go/internal/errors" +// Environment variable constants +const ( + envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" ) -// Environment variable names for service account credentials +var errMissingEnv = errors.New("missing environment variable") + +// Secrets contains sensitive configuration loaded from environment variables type Secrets struct { - ServiceAccountID string - ServiceAccountSecret string + serviceAccountID string + serviceAccountSecret string +} + +func (s Secrets) ServiceAccountID() string { + return s.serviceAccountID } -// LoadSecrets loads the required secrets from environment variables. -// It returns a Secrets struct or an error if any required variable is missing. -func LoadSecrets() (*Secrets, error) { - s := &Secrets{} +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) { @@ -27,13 +42,11 @@ func LoadSecrets() (*Secrets, error) { } } - look(EnvSAClientID, &s.ServiceAccountID) - look(EnvSAClientSecret, &s.ServiceAccountSecret) + look(envServiceAccountID, &s.serviceAccountID) + look(envServiceAccountSecret, &s.serviceAccountSecret) if len(missing) > 0 { - return nil, &errors.ValidationError{ - Message: "missing required environment variables: " + strings.Join(missing, ", "), - } + return Secrets{}, fmt.Errorf("load secrets: %w (missing: %v)", errMissingEnv, missing) } return s, nil } 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/io.go b/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go index 3e04d27..fac650b 100644 --- a/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go +++ b/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go @@ -52,7 +52,7 @@ 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") 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 f82026e..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 @@ -2,11 +2,12 @@ package metrics import ( "context" + "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.mongodb.org/atlas-sdk/v20250219001/admin" "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" - "testing" ) // ----------------------------------------------------------------------------- From 0110e97fb0aa7c0c2e2625dbf8123e0fa8a98732 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 19 Aug 2025 08:41:58 -0400 Subject: [PATCH 08/19] Re-generate snippets --- .../main.snippet.archive-collections.go | 43 +-- .../go/atlas-sdk-go/main.snippet.get-logs.go | 29 +- .../main.snippet.get-metrics-dev.go | 24 +- .../main.snippet.get-metrics-prod.go | 25 +- .../main.snippet.historical-billing.go | 45 ++-- .../atlas-sdk-go/main.snippet.line-items.go | 49 ++-- .../main.snippet.linked-billing.go | 21 +- .../go/atlas-sdk-go/project-copy/.env.example | 1 + .../configs/config.development.json | 12 - .../configs/config.production.json | 12 - .../examples/billing/historical/main.go | 45 ++-- .../examples/billing/line_items/main.go | 49 ++-- .../examples/billing/linked_orgs/main.go | 21 +- .../examples/monitoring/logs/main.go | 29 +- .../examples/monitoring/metrics_disk/main.go | 24 +- .../monitoring/metrics_process/main.go | 25 +- .../examples/performance/archiving/main.go | 43 +-- .../project-copy/internal/archive/analyze.go | 20 +- .../project-copy/internal/auth/client.go | 16 +- .../project-copy/internal/config/appconfig.go | 48 ++++ .../internal/config/appcontext.go | 251 ------------------ .../project-copy/internal/config/loadall.go | 67 ++--- .../internal/config/loadconfig.go | 40 +-- .../project-copy/internal/config/loadenv.go | 42 ++- .../project-copy/internal/errors/utils.go | 7 - usage-examples/go/atlas-sdk-go/.env.example | 1 + usage-examples/go/atlas-sdk-go/README.md | 10 +- .../configs/config.development.json | 4 - .../configs/config.production.json | 4 - .../examples/billing/historical/main.go | 8 +- .../examples/billing/line_items/main.go | 10 +- .../examples/billing/linked_orgs/main.go | 2 +- .../examples/monitoring/logs/main.go | 2 +- .../examples/monitoring/metrics_disk/main.go | 2 +- .../monitoring/metrics_process/main.go | 4 +- .../go/atlas-sdk-go/internal/auth/client.go | 9 +- .../atlas-sdk-go/internal/auth/client_test.go | 32 +-- .../internal/billing/collector_test.go | 4 +- .../atlas-sdk-go/internal/config/appconfig.go | 48 ++++ .../internal/config/appcontext.go | 70 ----- .../atlas-sdk-go/internal/config/loadall.go | 39 +-- .../atlas-sdk-go/internal/config/loadenv.go | 10 + 42 files changed, 527 insertions(+), 720 deletions(-) delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/config/appconfig.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/config/appcontext.go 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 index e9c85e1..bbd063a 100644 --- 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 @@ -4,76 +4,81 @@ package main import ( "context" "fmt" + "log" "time" "atlas-sdk-go/internal/archive" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" + + "github.com/joho/godotenv" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration and secrets for the environment - explicitEnv := "production" - appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) + _ = godotenv.Load() + + envName := config.Environment("") // Use empty string to load from environment variables + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers if err != nil { - errors.ExitWithError("Failed to initialize Atlas client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - projectID := appCtx.Config.ProjectID + projectID := cfg.ProjectID if projectID == "" { - errors.ExitWithError("Project ID not found in configuration", nil) + log.Fatal("Failed to find Project ID in configuration") } - fmt.Println("Starting archive analysis for project:", projectID) + fmt.Printf("Starting archive analysis for project: %s\n", projectID) // Step 1: List all clusters in the project clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() if err != nil { - errors.ExitWithError("Failed to list clusters", err) + log.Fatalf("Failed to list clusters: %v", err) } - fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + fmt.Printf("Found %d clusters to analyze\n", len(clusters.GetResults())) // Step 2: Process each cluster failedArchives := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s", clusterName) + fmt.Printf("Analyzing cluster: %s\n", clusterName) // Step 3: Find collections suitable for archiving - // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // NOTE: This example passes example database/collections. + // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("Found %d collections eligible for archiving in cluster %s", + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", len(candidates), clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("Configuring archive for %s.%s", + fmt.Printf("\nConfiguring archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("Failed to configure archive: %v", configureErr) + fmt.Printf("\nFailed to configure archive: %v", configureErr) failedArchives++ continue } - fmt.Printf("Successfully configured online archive for %s.%s", + fmt.Printf("\nSuccessfully configured online archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed", failedArchives) + fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) } 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 012696b..44c8197 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,28 +8,29 @@ 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" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -40,7 +41,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) @@ -50,22 +51,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) } 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 fedc664..8988c7f 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 @@ -5,30 +5,32 @@ import ( "context" "encoding/json" "fmt" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "development" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("development") // Cast string to config.Environment + configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -40,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/main.snippet.get-metrics-prod.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-metrics-prod.go index 49fd66b..041f0a4 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 @@ -5,32 +5,33 @@ import ( "context" "encoding/json" "fmt" - - "atlas-sdk-go/internal/errors" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "production" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -48,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/main.snippet.historical-billing.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.historical-billing.go index 6db1e56..fe71a8a 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,30 +11,31 @@ 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" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() + + ctx := context.Background() + envName := config.Environment("") + configPath := "" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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 @@ -43,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 { @@ -57,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 @@ -89,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 6d459fe..68d5748 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 @@ -4,31 +4,36 @@ package main import ( "context" "fmt" - "go.mongodb.org/atlas-sdk/v20250219001/admin" "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" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } + + ctx := context.Background() + envName := config.Environment("production") + configPath := "configs/config.production.json" + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + 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, } @@ -37,7 +42,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 { @@ -50,26 +55,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 @@ -91,8 +103,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 0820414..abe80d6 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 @@ -4,29 +4,32 @@ package main import ( "context" "fmt" + "log" "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() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, } @@ -35,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/.env.example b/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example index a1f8da0..4edac11 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,3 @@ MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret +ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional directory for downloads diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json deleted file mode 100644 index dc9dd77..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.development.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ENVIRONMENT": "dev", - "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", - "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", - "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", - "ATLAS_PROJECT_NAME": "Customer Portal - Dev", - "ATLAS_PROCESS_ID": "CustomerPortalDev-shard-00-00.ajlj3.mongodb.net:27017", - "CLOUD_PROVIDER": "AWS", - "AUTO_SCALING_DISK_GB": true, - "AUTO_SCALING_COMPUTE": true, - "DISK_SIZE_GB": 10000 -} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json deleted file mode 100644 index 3ffad1e..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.production.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ENVIRONMENT": "prod", - "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", - "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", - "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", - "ATLAS_PROJECT_NAME": "Customer Portal - Prod", - "ATLAS_PROCESS_ID": "CustomerPortalProd-shard-00-00.ajlj3.mongodb.net:27017", - "CLOUD_PROVIDER": "AWS", - "AUTO_SCALING_DISK_GB": true, - "AUTO_SCALING_COMPUTE": true, - "DISK_SIZE_GB": 40000 -} 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 17253a5..99ff01d 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,30 +10,31 @@ 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" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() + + ctx := context.Background() + envName := config.Environment("") + configPath := "" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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 @@ -42,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 { @@ -56,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 @@ -88,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 403cc09..36e2093 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 @@ -3,31 +3,36 @@ package main import ( "context" "fmt" - "go.mongodb.org/atlas-sdk/v20250219001/admin" "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" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } + + ctx := context.Background() + envName := config.Environment("production") + configPath := "configs/config.production.json" + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + 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, } @@ -36,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 { @@ -49,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 @@ -90,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/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 f58a601..892aa05 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 @@ -3,29 +3,32 @@ package main import ( "context" "fmt" + "log" "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() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, } @@ -34,7 +37,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 5441b1a..692bbb1 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,28 +7,29 @@ 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" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "" // Use default environment - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -39,7 +40,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) @@ -49,22 +50,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) } 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 fff535e..dbc2a77 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 @@ -4,30 +4,32 @@ import ( "context" "encoding/json" "fmt" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" "atlas-sdk-go/internal/metrics" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "development" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() // or godotenv.Load(".env.development") + + ctx := context.Background() + envName := config.Environment("development") // Cast string to config.Environment + configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -39,13 +41,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 db83955..e538977 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 @@ -4,32 +4,33 @@ import ( "context" "encoding/json" "fmt" - - "atlas-sdk-go/internal/errors" + "log" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" + "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" "atlas-sdk-go/internal/metrics" ) func main() { - configPath := "" // Use default config path for environment - explicitEnv := "production" - secrets, cfg, err := config.LoadAll(configPath, explicitEnv) + _ = godotenv.Load() + + ctx := context.Background() + envName := config.Environment("test") // Cast string to config.Environment + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(cfg, secrets) + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers 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, @@ -47,13 +48,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 index 1d7e8b4..44c9255 100644 --- 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 @@ -3,76 +3,81 @@ package main import ( "context" "fmt" + "log" "time" "atlas-sdk-go/internal/archive" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" - "atlas-sdk-go/internal/errors" + + "github.com/joho/godotenv" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - // Load application context with configuration and secrets for the environment - explicitEnv := "production" - appCtx, err := config.LoadAppContextWithContext(ctx, explicitEnv, false) + _ = godotenv.Load() + + envName := config.Environment("") // Use empty string to load from environment variables + configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { - errors.ExitWithError("Failed to load configuration", err) + log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(appCtx.Config, appCtx.Secrets) + client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers if err != nil { - errors.ExitWithError("Failed to initialize Atlas client", err) + log.Fatalf("Failed to initialize authentication client: %v", err) } - projectID := appCtx.Config.ProjectID + projectID := cfg.ProjectID if projectID == "" { - errors.ExitWithError("Project ID not found in configuration", nil) + log.Fatal("Failed to find Project ID in configuration") } - fmt.Println("Starting archive analysis for project:", projectID) + fmt.Printf("Starting archive analysis for project: %s\n", projectID) // Step 1: List all clusters in the project clusters, _, err := client.ClustersApi.ListClusters(ctx, projectID).Execute() if err != nil { - errors.ExitWithError("Failed to list clusters", err) + log.Fatalf("Failed to list clusters: %v", err) } - fmt.Printf("Found %d clusters to analyze", len(clusters.GetResults())) + fmt.Printf("Found %d clusters to analyze\n", len(clusters.GetResults())) // Step 2: Process each cluster failedArchives := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s", clusterName) + fmt.Printf("Analyzing cluster: %s\n", clusterName) // Step 3: Find collections suitable for archiving - // NOTE: In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // NOTE: This example passes example database/collections. + // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("Found %d collections eligible for archiving in cluster %s", + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", len(candidates), clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("Configuring archive for %s.%s", + fmt.Printf("\nConfiguring archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("Failed to configure archive: %v", configureErr) + fmt.Printf("\nFailed to configure archive: %v", configureErr) failedArchives++ continue } - fmt.Printf("Successfully configured online archive for %s.%s", + fmt.Printf("\nSuccessfully configured online archive for %s.%s ", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed", failedArchives) + fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) } fmt.Println("Archive analysis and configuration completed") 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 index e64b56f..c4299fd 100644 --- 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 @@ -41,14 +41,17 @@ func DefaultOptions() Options { } } -// CollectionsForArchiving demonstrates how to identify collections suitable for archiving -// In a real implementation, you would analyze collection data patterns -// and determine which collections are eligible based on criteria such as size, age, and access patterns. -// This function returns a list of candidates that meet the archiving criteria -// Note: This is a simplified example and should be customized for your specific use case +type ExpireAfterDays struct { + // NOTE: this placeholder struct can be extended to include more complex rules if needed + ExpireAfterDays int `json:"expireAfterDays,omitempty"` +} + +// CollectionsForArchiving identifies collections suitable for archiving as a simplified example for demonstration purposes. +// This function returns a list of Candidates that meet the archiving criteria +// NOTE: In a real implementation, you would determine which collections are eligible based on criteria analysis such as size, age, and access patterns. func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { - // For demonstration purposes, we specify example candidates + // For demonstration purposes, we specify example Candidates return []Candidate{ { DatabaseName: "sample_analytics", @@ -69,11 +72,6 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, } } -type ExpireAfterDays struct { - // NOTE: this placeholder struct can be extended to include more complex rules if needed - ExpireAfterDays int `json:"expireAfterDays,omitempty"` -} - // ValidateCandidate ensures the archiving candidate meets requirements func ValidateCandidate(candidate Candidate, opts Options) error { if candidate.DatabaseName == "" || candidate.CollectionName == "" { 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..9114ae0 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,16 @@ 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"} - } - - if secrets == nil { +func NewClient(ctx context.Context, cfg config.Config, secrets config.Secrets) (*admin.APIClient, error) { + 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/config/appconfig.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go new file mode 100644 index 0000000..5243154 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go @@ -0,0 +1,48 @@ +package config + +import ( + goErr "errors" + "os" + + "atlas-sdk-go/internal/errors" +) + +// Environment defines the runtime environment for the application +type Environment string + +const ( + envDevelopment Environment = "development" + envStaging Environment = "test" + envProduction Environment = "production" +) + +// AppConfig contains all environment-specific configurations +type AppConfig struct { + environment Environment + config Config + secrets Secrets +} + +// LoadAppConfig loads the application configuration based on the provided config file and environment. +// Returns the app's configuration or any error encountered during loading +func LoadAppConfig(configFile string, env Environment) (AppConfig, error) { + if configFile == "" { + return AppConfig{}, goErr.New("config file path must be provided") + } + if _, err := os.Stat(configFile); os.IsNotExist(err) { + return AppConfig{}, &errors.NotFoundError{Resource: "configuration file", ID: configFile} + } + secrets, err := LoadSecrets() + if err != nil { + return AppConfig{}, errors.WithContext(err, "loading secrets") + } + config, err := LoadConfig(configFile) + if err != nil { + return AppConfig{}, errors.WithContext(err, "loading config") + } + return AppConfig{ + environment: env, + config: config, + secrets: secrets, + }, nil +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go deleted file mode 100644 index 868dbce..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appcontext.go +++ /dev/null @@ -1,251 +0,0 @@ -package config - -import ( - "context" - "fmt" - "log" - "os" - "sync" - "time" - - "github.com/joho/godotenv" - - "atlas-sdk-go/internal/errors" -) - -// Package config provides application context management, including environment-specific configurations -// and caching mechanisms to optimize performance and reduce redundant loading of configurations. -var ( - cachedAppContext *AppContext - cachedAppContextTime time.Time - cacheTTL = 5 * time.Minute - cacheMutex sync.RWMutex -) - -// Constants for environment variables and default paths -const ( - EnvAppEnv = "APP_ENV" - EnvConfigPath = "ATLAS_CONFIG_PATH" - EnvSAClientID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" - EnvSAClientSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" - DefaultConfigFormat = "configs/config.%s.json" -) - -// AppContext contains all environment-specific configurations -type AppContext struct { - Environment string - Config *Config - Secrets *Secrets -} - -// LoadAppContext initializes application context with environment-specific configuration -// If explicitEnv is provided, it overrides the APP_ENV environment variable -// If strictValidation is true, invalid environments will return an error -func LoadAppContext(explicitEnv string, strictValidation bool) (*AppContext, error) { - // Environment resolution priority: - // 1. An explicitly passed environment parameter - // 2. An APP_ENV environment variable - // 3. Otherwise, defaults to "development" - - // Determine environment - env := explicitEnv - if env == "" { - env = os.Getenv(EnvAppEnv) - if env == "" { - env = "development" - } - } - - // Check cache first using the resolved environment - cacheMutex.RLock() - if cachedAppContext != nil && - cachedAppContext.Environment == env && - time.Since(cachedAppContextTime) < cacheTTL { - cached := cachedAppContext - cacheMutex.RUnlock() - return cached, nil - } - cacheMutex.RUnlock() - - if !ValidateEnvironment(env) { - if strictValidation { - return nil, fmt.Errorf("invalid environment: %s", env) - } - log.Printf("Warning: Unexpected environment '%s' may cause issues", env) - } - // Load environment files - envFiles := []string{ - fmt.Sprintf(".env.%s", env), - ".env", - } - - loaded := false - for _, file := range envFiles { - if err := godotenv.Load(file); err == nil { - log.Printf("Loaded environment from %s", file) - loaded = true - break - } - } - - if !loaded { - log.Printf("Warning: No environment files found, using system environment variables only") - } - - // Get config path from env var or use default - configPath := os.Getenv(EnvConfigPath) - if configPath == "" { - configPath = fmt.Sprintf(DefaultConfigFormat, env) - } - - log.Printf("Loading configuration for environment: %s", env) - log.Printf("Using config file: %s", configPath) - - secrets, err := LoadSecrets() - if err != nil { - return nil, errors.WithContext(err, "loading secrets") - } - - config, err := LoadConfig(configPath) - if err != nil { - return nil, errors.WithContext(err, "loading config") - } - - if err = config.Validate(env); err != nil { - return nil, errors.WithContext(err, "validating config") - } - - log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", - env, config.BaseURL, config.OrgID) - - // Create and initialize the AppContext - appCtx := &AppContext{ - Environment: env, - Config: config, - Secrets: secrets, - } - - // Cache the result - cacheMutex.Lock() - cachedAppContext = appCtx - cachedAppContextTime = time.Now() - cacheMutex.Unlock() - - return appCtx, nil -} - -// LoadAppContextWithContext initializes application context with environment-specific configuration using a provided context for cancellation support. -// If explicitEnv is provided, it overrides the APP_ENV environment variable -// If strictValidation is true, invalid environments will return an error -func LoadAppContextWithContext(ctx context.Context, explicitEnv string, strictValidation bool) (*AppContext, error) { - // Use context for potential operations that may need cancellation - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled while loading configuration: %w", ctx.Err()) - default: - // Continue with loading - } - - // Determine environment - env := explicitEnv - if env == "" { - env = os.Getenv(EnvAppEnv) - if env == "" { - env = "development" - } - } - - // Check cache first using the resolved environment - cacheMutex.RLock() - if cachedAppContext != nil && - cachedAppContext.Environment == env && - time.Since(cachedAppContextTime) < cacheTTL { - cached := cachedAppContext - cacheMutex.RUnlock() - return cached, nil - } - cacheMutex.RUnlock() - - // The implementation mirrors LoadAppContext but with context checks - if !ValidateEnvironment(env) { - if strictValidation { - return nil, fmt.Errorf("invalid environment: %s", env) - } - log.Printf("Warning: Unexpected environment '%s' may cause issues", env) - } - - // Add context check before expensive operations - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled while loading environment files: %w", ctx.Err()) - default: - } - - // Load environment files - envFiles := []string{ - fmt.Sprintf(".env.%s", env), - ".env", - } - - loaded := false - for _, file := range envFiles { - if err := godotenv.Load(file); err == nil { - log.Printf("Loaded environment from %s", file) - loaded = true - break - } - } - - if !loaded { - log.Printf("Warning: No environment files found, using system environment variables only") - } - - // Get config path from env var or use default - configPath := os.Getenv(EnvConfigPath) - if configPath == "" { - configPath = fmt.Sprintf(DefaultConfigFormat, env) - } - - // Add context check before loading secrets and config - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled before loading secrets/config: %w", ctx.Err()) - default: - } - - log.Printf("Loading configuration for environment: %s", env) - log.Printf("Using config file: %s", configPath) - - secrets, err := LoadSecrets() - if err != nil { - return nil, errors.WithContext(err, "loading secrets") - } - - config, err := LoadConfig(configPath) - if err != nil { - return nil, errors.WithContext(err, "loading config") - } - - if err = config.Validate(env); err != nil { - return nil, errors.WithContext(err, "validating config") - } - - log.Printf("Configuration loaded successfully: env=%s, baseURL=%s, orgID=%s", - env, config.BaseURL, config.OrgID) - - // Create and initialize the AppContext - appCtx := &AppContext{ - Environment: env, - Config: config, - Secrets: secrets, - } - - // Cache the result - cacheMutex.Lock() - cachedAppContext = appCtx - cachedAppContextTime = time.Now() - cacheMutex.Unlock() - - return appCtx, 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 1bcc917..8c819df 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,43 +1,44 @@ package config import ( - "atlas-sdk-go/internal/errors" + "fmt" ) -// LoadAll loads secrets and config -// If configPath is empty, uses environment-specific loading -// If explicitEnv is provided, it overrides the APP_ENV environment variable -func LoadAll(configPath string, explicitEnv string) (*Secrets, *Config, error) { - if configPath == "" { - // Use environment-based loading - appCtx, err := LoadAppContext(explicitEnv, false) // Use non-strict validation by default - if err != nil { - return nil, nil, err - } - return appCtx.Secrets, appCtx.Config, nil - } - - // Legacy path-specific loading - s, err := LoadSecrets() - if err != nil { - return nil, nil, errors.WithContext(err, "loading secrets") - } - - c, err := LoadConfig(configPath) - if err != nil { - return nil, nil, errors.WithContext(err, "loading config") - } - - return s, c, nil +// EnvironmentNames defines valid runtime environments +var allowedEnvironments = map[Environment]struct{}{ + envDevelopment: {}, + envStaging: {}, + envProduction: {}, } -// ValidateEnvironment checks if the provided environment is valid func ValidateEnvironment(env string) bool { - validEnvs := map[string]bool{ - "development": true, - "staging": true, - "production": true, - "test": true, + _, ok := allowedEnvironments[Environment(env)] + return ok +} + +// LoadAll loads the application configuration and secrets based on the provided environment name and optional config file path. +// If configPath is empty, it defaults to "configs/config.{env}.json" based on the environment name. +// If envName is empty, it defaults to "configs/config.json". +// Parameters: +// - envName: Environment to load configuration for (development, staging, production) +// - configPath: Optional explicit path to the configuration file +// +// Returns: +// - Secrets: Loaded secrets +// - Config: Loaded application configuration +// - error: Any error encountered during loading +func LoadAll(envName Environment, configPath string) (Secrets, Config, error) { + var configFile string + if configPath != "" { + configFile = configPath + } else if envName != "" { + configFile = fmt.Sprintf("configs/config.%s.json", envName) + } else { + configFile = "configs/config.json" + } + appConfig, err := LoadAppConfig(configFile, envName) + if err != nil { + return Secrets{}, Config{}, err } - return validEnvs[env] + return appConfig.secrets, appConfig.config, nil } 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 29d7c4f..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 @@ -14,15 +14,16 @@ type Config struct { 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 // It validates required fields and returns an error if any validation fails. -func LoadConfig(path string) (*Config, error) { +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", } } @@ -30,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") + 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", } } @@ -55,29 +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 -} - -// Validate checks the configuration for required fields and environment-specific rules -// It returns an error if any validation fails. -func (c *Config) Validate(env string) error { - if c.BaseURL == "" { - return &errors.ValidationError{Message: "BaseURL is required"} - } - - // Add environment-specific validation - if env == "production" { - // Stricter validation for production - if strings.Contains(c.BaseURL, "dev") || strings.Contains(c.BaseURL, "test") { - return &errors.ValidationError{Message: "Production environment cannot use development URLs"} - } + if config.BaseURL == "" { + config.BaseURL = "https://cloud.mongodb.com" // Default base URL if not provided } - return nil + 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 index 8444559..ed484ed 100644 --- 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 @@ -1,22 +1,37 @@ package config import ( + "errors" + "fmt" "os" - "strings" +) - "atlas-sdk-go/internal/errors" +// Environment variable constants +const ( + envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" + envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" ) -// Environment variable names for service account credentials +var errMissingEnv = errors.New("missing environment variable") + +// Secrets contains sensitive configuration loaded from environment variables type Secrets struct { - ServiceAccountID string - ServiceAccountSecret string + serviceAccountID string + serviceAccountSecret string +} + +func (s Secrets) ServiceAccountID() string { + return s.serviceAccountID } -// LoadSecrets loads the required secrets from environment variables. -// It returns a Secrets struct or an error if any required variable is missing. -func LoadSecrets() (*Secrets, error) { - s := &Secrets{} +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) { @@ -27,13 +42,12 @@ func LoadSecrets() (*Secrets, error) { } } - look(EnvSAClientID, &s.ServiceAccountID) - look(EnvSAClientSecret, &s.ServiceAccountSecret) + look(envServiceAccountID, &s.serviceAccountID) + look(envServiceAccountSecret, &s.serviceAccountSecret) if len(missing) > 0 { - return nil, &errors.ValidationError{ - Message: "missing required environment variables: " + strings.Join(missing, ", "), - } + 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/usage-examples/go/atlas-sdk-go/.env.example b/usage-examples/go/atlas-sdk-go/.env.example index a1f8da0..4edac11 100644 --- a/usage-examples/go/atlas-sdk-go/.env.example +++ b/usage-examples/go/atlas-sdk-go/.env.example @@ -1,2 +1,3 @@ MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret +ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional directory for downloads diff --git a/usage-examples/go/atlas-sdk-go/README.md b/usage-examples/go/atlas-sdk-go/README.md index a2b8658..ddb3e6b 100644 --- a/usage-examples/go/atlas-sdk-go/README.md +++ b/usage-examples/go/atlas-sdk-go/README.md @@ -31,7 +31,7 @@ and improvements to existing code. │ ├── monitoring/ │ └── performance/ ├── configs # Atlas configuration template -│ └── config.json +│ └── config.example.json ├── internal # Shared utilities and helpers │ ├── archive/ │ ├── auth/ @@ -59,19 +59,17 @@ and improvements to existing code. ## Setting Environment Variables -1. Create a `.env.` file in the root directory with your MongoDB Atlas service account credentials. For example, to create a `.env.development` file for your dev environment: +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 - ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" - CONFIG_PATH="/configs" - APP_ENV="dev" + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional download directory ``` > **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": "", diff --git a/usage-examples/go/atlas-sdk-go/configs/config.development.json b/usage-examples/go/atlas-sdk-go/configs/config.development.json index dc9dd77..feed6c9 100644 --- a/usage-examples/go/atlas-sdk-go/configs/config.development.json +++ b/usage-examples/go/atlas-sdk-go/configs/config.development.json @@ -5,8 +5,4 @@ "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", "ATLAS_PROJECT_NAME": "Customer Portal - Dev", "ATLAS_PROCESS_ID": "CustomerPortalDev-shard-00-00.ajlj3.mongodb.net:27017", - "CLOUD_PROVIDER": "AWS", - "AUTO_SCALING_DISK_GB": true, - "AUTO_SCALING_COMPUTE": true, - "DISK_SIZE_GB": 10000 } diff --git a/usage-examples/go/atlas-sdk-go/configs/config.production.json b/usage-examples/go/atlas-sdk-go/configs/config.production.json index 3ffad1e..7e78a8c 100644 --- a/usage-examples/go/atlas-sdk-go/configs/config.production.json +++ b/usage-examples/go/atlas-sdk-go/configs/config.production.json @@ -5,8 +5,4 @@ "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", "ATLAS_PROJECT_NAME": "Customer Portal - Prod", "ATLAS_PROCESS_ID": "CustomerPortalProd-shard-00-00.ajlj3.mongodb.net:27017", - "CLOUD_PROVIDER": "AWS", - "AUTO_SCALING_DISK_GB": true, - "AUTO_SCALING_COMPUTE": true, - "DISK_SIZE_GB": 40000 } 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 c37834f..dc7e31e 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 @@ -21,17 +21,17 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + _ = godotenv.Load() ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("") + configPath := "" // Optional explicit config file path; if empty, uses environment-based path secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 55cabd1..3292ee2 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 @@ -20,17 +20,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 f1a097e..e095aa1 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 @@ -28,7 +28,7 @@ func main() { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 b69f8cc..bcddadc 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 @@ -29,7 +29,7 @@ func main() { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 f6f345d..94c6998 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 @@ -29,7 +29,7 @@ func main() { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 995ef99..6d20eee 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 @@ -20,7 +20,7 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + _ = godotenv.Load() ctx := context.Background() envName := config.Environment("test") // Cast string to config.Environment @@ -30,7 +30,7 @@ func main() { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 0422826..9114ae0 100644 --- a/usage-examples/go/atlas-sdk-go/internal/auth/client.go +++ b/usage-examples/go/atlas-sdk-go/internal/auth/client.go @@ -11,15 +11,10 @@ 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(ctx context.Context, cfg *config.Config, secrets *config.Secrets) (*admin.APIClient, error) { - if cfg == nil { - return nil, &errors.ValidationError{Message: "config cannot be nil"} - } - - if secrets == nil { +func NewClient(ctx context.Context, cfg config.Config, secrets config.Secrets) (*admin.APIClient, error) { + if secrets.ServiceAccountID() == "" || secrets.ServiceAccountSecret() == "" { return nil, &errors.ValidationError{Message: "secrets cannot be nil"} } - sdk, err := admin.NewClient( admin.UseBaseURL(cfg.BaseURL), admin.UseOAuthAuth( 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..faf4888 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") + require.True(t, assert.ErrorAs(t, err, &validationErr), "expected error to be *errors.ValidationError") assert.Equal(t, "config cannot be nil", 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/config/appconfig.go b/usage-examples/go/atlas-sdk-go/internal/config/appconfig.go new file mode 100644 index 0000000..5243154 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/config/appconfig.go @@ -0,0 +1,48 @@ +package config + +import ( + goErr "errors" + "os" + + "atlas-sdk-go/internal/errors" +) + +// Environment defines the runtime environment for the application +type Environment string + +const ( + envDevelopment Environment = "development" + envStaging Environment = "test" + envProduction Environment = "production" +) + +// AppConfig contains all environment-specific configurations +type AppConfig struct { + environment Environment + config Config + secrets Secrets +} + +// LoadAppConfig loads the application configuration based on the provided config file and environment. +// Returns the app's configuration or any error encountered during loading +func LoadAppConfig(configFile string, env Environment) (AppConfig, error) { + if configFile == "" { + return AppConfig{}, goErr.New("config file path must be provided") + } + if _, err := os.Stat(configFile); os.IsNotExist(err) { + return AppConfig{}, &errors.NotFoundError{Resource: "configuration file", ID: configFile} + } + secrets, err := LoadSecrets() + if err != nil { + return AppConfig{}, errors.WithContext(err, "loading secrets") + } + config, err := LoadConfig(configFile) + if err != nil { + return AppConfig{}, errors.WithContext(err, "loading config") + } + return AppConfig{ + environment: env, + config: config, + secrets: secrets, + }, nil +} diff --git a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go b/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go deleted file mode 100644 index ca1414c..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/config/appcontext.go +++ /dev/null @@ -1,70 +0,0 @@ -package config - -import ( - "fmt" - "os" - - "atlas-sdk-go/internal/errors" -) - -const ( - envAppEnv = "APP_ENV" // Environment variable for the application environment - envConfigPath = "APP_CONFIG_PATH" // Environment variable for the configuration file path - defaultConfigFormat = "configs/config.%s.json" -) - -const ( - envDevelopment Environment = "development" - envStaging Environment = "test" - envProduction Environment = "production" -) - -// AppContext contains all environment-specific configurations -type AppContext struct { - environment Environment - config Config - secrets Secrets -} - -type Environment string - -// LoadAppContext loads environment-specific configuration -// Returns error if no environment is specified -func LoadAppContext(explicitEnv Environment) (AppContext, error) { - env := explicitEnv - if env == "" { - env = Environment(os.Getenv(envAppEnv)) - } - // Validate environment - if !ValidateEnvironment(string(env)) { - return AppContext{}, fmt.Errorf("invalid environment: %s", env) - } - - // Determine config path - configPath := os.Getenv(envConfigPath) - if configPath == "" { - configPath = fmt.Sprintf(defaultConfigFormat, env) - } - - // Check if config file exists - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return AppContext{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} - } - - // Load secrets and config - secrets, err := LoadSecrets() - if err != nil { - return AppContext{}, errors.WithContext(err, "loading secrets") - } - - config, err := LoadConfig(configPath) - if err != nil { - return AppContext{}, errors.WithContext(err, "loading config") - } - - return AppContext{ - environment: env, - config: config, - secrets: secrets, - }, nil -} 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 b1ebbd8..8c819df 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -1,7 +1,7 @@ package config import ( - "atlas-sdk-go/internal/errors" + "fmt" ) // EnvironmentNames defines valid runtime environments @@ -16,28 +16,29 @@ func ValidateEnvironment(env string) bool { return ok } -// LoadAll loads configuration for a specific environment -// +// LoadAll loads the application configuration and secrets based on the provided environment name and optional config file path. +// If configPath is empty, it defaults to "configs/config.{env}.json" based on the environment name. +// If envName is empty, it defaults to "configs/config.json". // Parameters: -// - envName: Environment name (dev/staging/prod/test); overrides APP_ENV if provided -// - configPath: Optional explicit config file path; if empty, uses environment-based path +// - envName: Environment to load configuration for (development, staging, production) +// - configPath: Optional explicit path to the configuration file // -// Returns secrets, config and any errors encountered during loading +// Returns: +// - Secrets: Loaded secrets +// - Config: Loaded application configuration +// - error: Any error encountered during loading func LoadAll(envName Environment, configPath string) (Secrets, Config, error) { - if configPath == "" { - appCtx, err := LoadAppContext(envName) - if err != nil { - return Secrets{}, Config{}, err - } - return appCtx.secrets, appCtx.config, nil // return values, not pointers - } - s, err := LoadSecrets() - if err != nil { - return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") + var configFile string + if configPath != "" { + configFile = configPath + } else if envName != "" { + configFile = fmt.Sprintf("configs/config.%s.json", envName) + } else { + configFile = "configs/config.json" } - c, err := LoadConfig(configPath) + appConfig, err := LoadAppConfig(configFile, envName) if err != nil { - return Secrets{}, Config{}, errors.WithContext(err, "loading config") + return Secrets{}, Config{}, err } - return s, c, nil // return values, not pointers + return appConfig.secrets, appConfig.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 index e55acb6..2df3778 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go @@ -50,3 +50,13 @@ func LoadSecrets() (Secrets, error) { } 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: From b385fc75c9489d6d9a41f761beb8906a385d38a9 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 19 Aug 2025 08:43:59 -0400 Subject: [PATCH 09/19] remove extra keys from configs --- usage-examples/go/atlas-sdk-go/configs/config.development.json | 1 - usage-examples/go/atlas-sdk-go/configs/config.production.json | 1 - 2 files changed, 2 deletions(-) diff --git a/usage-examples/go/atlas-sdk-go/configs/config.development.json b/usage-examples/go/atlas-sdk-go/configs/config.development.json index feed6c9..ab61f2f 100644 --- a/usage-examples/go/atlas-sdk-go/configs/config.development.json +++ b/usage-examples/go/atlas-sdk-go/configs/config.development.json @@ -1,5 +1,4 @@ { - "ENVIRONMENT": "dev", "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", diff --git a/usage-examples/go/atlas-sdk-go/configs/config.production.json b/usage-examples/go/atlas-sdk-go/configs/config.production.json index 7e78a8c..59f9033 100644 --- a/usage-examples/go/atlas-sdk-go/configs/config.production.json +++ b/usage-examples/go/atlas-sdk-go/configs/config.production.json @@ -1,5 +1,4 @@ { - "ENVIRONMENT": "prod", "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", From 9eeebaddcdad8dc0abeab530105190eabe76e781 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 19 Aug 2025 09:57:43 -0400 Subject: [PATCH 10/19] clean up comments and debugging; re-generate snippets --- .../main.snippet.archive-collections.go | 40 +++++----- .../go/atlas-sdk-go/main.snippet.get-logs.go | 10 ++- .../main.snippet.get-metrics-dev.go | 10 ++- .../main.snippet.get-metrics-prod.go | 10 ++- .../main.snippet.historical-billing.go | 10 ++- .../main.snippet.linked-billing.go | 10 ++- .../go/atlas-sdk-go/project-copy/.gitignore | 15 ++-- .../go/atlas-sdk-go/project-copy/README.md | 10 +-- .../examples/billing/historical/main.go | 10 ++- .../examples/billing/linked_orgs/main.go | 10 ++- .../examples/monitoring/logs/main.go | 10 ++- .../examples/monitoring/metrics_disk/main.go | 10 ++- .../monitoring/metrics_process/main.go | 10 ++- .../examples/performance/archiving/main.go | 40 +++++----- usage-examples/go/atlas-sdk-go/.gitignore | 15 ++-- .../examples/billing/historical/main.go | 10 ++- .../examples/billing/linked_orgs/main.go | 10 ++- .../examples/monitoring/logs/main.go | 10 ++- .../examples/monitoring/metrics_disk/main.go | 10 ++- .../monitoring/metrics_process/main.go | 10 ++- .../examples/performance/archiving/main.go | 75 ++++++++++--------- 21 files changed, 197 insertions(+), 148 deletions(-) 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 index bbd063a..22e178f 100644 --- 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 @@ -18,16 +18,18 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } - envName := config.Environment("") // Use empty string to load from environment variables - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } @@ -39,48 +41,50 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // Step 1: List all clusters in the project + // List 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("Found %d clusters to analyze\n", len(clusters.GetResults())) + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Step 2: Process each cluster + // Process each cluster failedArchives := 0 + totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s\n", clusterName) + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Step 3: Find collections suitable for archiving - // NOTE: This example passes example database/collections. - // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // Find collections suitable for archiving + // NOTE: This function passes example database/collection names. + // In a real production scenario, you would analyze data patterns and customize the selection logic. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", - len(candidates), clusterName) + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + totalCandidates, clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("\nConfiguring archive for %s.%s ", + fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("\nFailed to configure archive: %v", configureErr) + fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ continue } - fmt.Printf("\nSuccessfully configured online archive for %s.%s ", + fmt.Printf(" Successfully configured online archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) + fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) } - fmt.Println("Archive analysis and configuration completed") + 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 44c8197..9f391ff 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 @@ -16,17 +16,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 8988c7f..4360f21 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 @@ -16,17 +16,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("development") // Cast string to config.Environment - configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("development") + configPath := "configs/config.development.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 041f0a4..944de05 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 @@ -17,17 +17,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 fe71a8a..82f758a 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 @@ -18,17 +18,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("") - configPath := "" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 abe80d6..2fec72c 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 @@ -15,17 +15,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 7ade94e..8e9c5d6 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore @@ -1,15 +1,18 @@ -# Secrets +# Secrets (keep example) .env !.env.example .env.development .env.production -configs/config.example.json -!configs/config.development.json -!configs/config.production.json +.env.test -tmp +# config files (keep example) +configs +!configs/config.example.json +# temporary files +tmp +temp -# Logs +# downloaded logs *.log *.gz 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 a2b8658..ddb3e6b 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 @@ -31,7 +31,7 @@ and improvements to existing code. │ ├── monitoring/ │ └── performance/ ├── configs # Atlas configuration template -│ └── config.json +│ └── config.example.json ├── internal # Shared utilities and helpers │ ├── archive/ │ ├── auth/ @@ -59,19 +59,17 @@ and improvements to existing code. ## Setting Environment Variables -1. Create a `.env.` file in the root directory with your MongoDB Atlas service account credentials. For example, to create a `.env.development` file for your dev environment: +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 - ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" - CONFIG_PATH="/configs" - APP_ENV="dev" + ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional download directory ``` > **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": "", 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 99ff01d..d1f1296 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 @@ -17,17 +17,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("") - configPath := "" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 892aa05..a140ad2 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 @@ -14,17 +14,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 692bbb1..f28a6a5 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 @@ -15,17 +15,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 dbc2a77..f63f609 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 @@ -15,17 +15,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("development") // Cast string to config.Environment - configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("development") + configPath := "configs/config.development.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 e538977..27b6e08 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 @@ -16,17 +16,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 index 44c9255..e672077 100644 --- 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 @@ -17,16 +17,18 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } - envName := config.Environment("") // Use empty string to load from environment variables - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } @@ -38,48 +40,50 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // Step 1: List all clusters in the project + // List 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("Found %d clusters to analyze\n", len(clusters.GetResults())) + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Step 2: Process each cluster + // Process each cluster failedArchives := 0 + totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s\n", clusterName) + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Step 3: Find collections suitable for archiving - // NOTE: This example passes example database/collections. - // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // Find collections suitable for archiving + // NOTE: This function passes example database/collection names. + // In a real production scenario, you would analyze data patterns and customize the selection logic. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", - len(candidates), clusterName) + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + totalCandidates, clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("\nConfiguring archive for %s.%s ", + fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("\nFailed to configure archive: %v", configureErr) + fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ continue } - fmt.Printf("\nSuccessfully configured online archive for %s.%s ", + fmt.Printf(" Successfully configured online archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) + fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) } - fmt.Println("Archive analysis and configuration completed") + fmt.Println("Archive analysis and configuration completed.") } diff --git a/usage-examples/go/atlas-sdk-go/.gitignore b/usage-examples/go/atlas-sdk-go/.gitignore index 7ade94e..8e9c5d6 100644 --- a/usage-examples/go/atlas-sdk-go/.gitignore +++ b/usage-examples/go/atlas-sdk-go/.gitignore @@ -1,15 +1,18 @@ -# Secrets +# Secrets (keep example) .env !.env.example .env.development .env.production -configs/config.example.json -!configs/config.development.json -!configs/config.production.json +.env.test -tmp +# config files (keep example) +configs +!configs/config.example.json +# temporary files +tmp +temp -# Logs +# downloaded logs *.log *.gz 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 dc7e31e..303ceb3 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 @@ -21,17 +21,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("") - configPath := "" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 e095aa1..94c70fc 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 @@ -18,17 +18,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 bcddadc..8ae955b 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 @@ -19,17 +19,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 94c6998..fefdddd 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 @@ -19,17 +19,19 @@ import ( ) func main() { - _ = godotenv.Load() // or godotenv.Load(".env.development") + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("development") // Cast string to config.Environment - configPath := "configs/config.development.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("development") + configPath := "configs/config.development.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 6d20eee..ee92e3b 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 @@ -20,17 +20,19 @@ import ( ) func main() { - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } ctx := context.Background() - envName := config.Environment("test") // Cast string to config.Environment - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, cfg, secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } 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 index d350946..a01bab7 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -21,16 +21,18 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - _ = godotenv.Load() + if err := godotenv.Load(); err != nil { + log.Printf("Warning: could not load .env file: %v", err) + } - envName := config.Environment("") // Use empty string to load from environment variables - configPath := "configs/config.test.json" // Optional explicit config file path; if empty, uses environment-based path + envName := config.Environment("production") + configPath := "configs/config.production.json" secrets, cfg, err := config.LoadAll(envName, configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } - client, err := auth.NewClient(ctx, &cfg, &secrets) // Pass pointers + client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) } @@ -42,49 +44,51 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // Step 1: List all clusters in the project + // List 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("Found %d clusters to analyze\n", len(clusters.GetResults())) + fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Step 2: Process each cluster + // Process each cluster failedArchives := 0 + totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() - fmt.Printf("Analyzing cluster: %s\n", clusterName) + fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Step 3: Find collections suitable for archiving - // NOTE: This example passes example database/collections. - // In a real production scenario, you would customize the collection analysis logic to match your specific data patterns. + // Find collections suitable for archiving + // NOTE: This function passes example database/collection names. + // In a real production scenario, you would analyze data patterns and customize the selection logic. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) - fmt.Printf("\nFound %d collections eligible for archiving in cluster %s", - len(candidates), clusterName) + totalCandidates += len(candidates) + fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", + totalCandidates, clusterName) // Step 4: Configure online archive for each candidate collection for _, candidate := range candidates { - fmt.Printf("\nConfiguring archive for %s.%s ", + fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate) if configureErr != nil { - fmt.Printf("\nFailed to configure archive: %v", configureErr) + fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ continue } - fmt.Printf("\nSuccessfully configured online archive for %s.%s ", + fmt.Printf(" Successfully configured online archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) } } if failedArchives > 0 { - fmt.Printf("Warning: %d archive configurations failed\n", failedArchives) + fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) } - fmt.Println("Archive analysis and configuration completed") + fmt.Println("Archive analysis and configuration completed.") } // :snippet-end: [archive-collections] @@ -92,21 +96,24 @@ func main() { // NOTE: INTERNAL // ** OUTPUT EXAMPLE ** // -// Configuration loaded successfully: env=production, baseURL=https://cloud.mongodb.com, orgID=5bfda007553855125605a5cf // Starting archive analysis for project: 5f60207f14dfb25d24511201 -// Found 2 clusters to analyze -// Analyzing cluster: Cluster0 -// Found 2 collections eligible for archiving in cluster Cluster0 -// Configuring archive for sample_analytics.transactions -// Successfully configured online archive for sample_analytics.transactions -// Configuring archive for sample_analytics.users -// Successfully configured online archive for sample_analytics.users -// Analyzing cluster: Cluster1 -// Found 1 collections eligible for archiving in cluster Cluster1 -// Configuring archive for sample_analytics.orders -// Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields -// Configuring archive for sample_logs.application_logs -// Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields -// Warning: 2 archive configurations failed -// Archive analysis and configuration completed +// +//Found 2 clusters to analyze +// +//=== Analyzing cluster: Cluster0 === +//Found 2 collections eligible for archiving in cluster Cluster0 +//- Configuring archive for sample_analytics.transactions +//Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields +//- Configuring archive for sample_logs.application_logs +//Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields +// +//=== Analyzing cluster: AtlasCluster === +//Found 4 collections eligible for archiving in cluster AtlasCluster +//- Configuring archive for sample_analytics.transactions +//Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields +//- Configuring archive for sample_logs.application_logs +//Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields +// +//WARNING: 4 of 4 archive configurations failed +//Archive analysis and configuration completed. // :state-remove-end: [copy] From 81d88affd4395820decac5f0eee89b26f21e2e9f Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 20 Aug 2025 06:59:59 -0400 Subject: [PATCH 11/19] Update main callers with simplified env loading --- usage-examples/go/atlas-sdk-go/.env.example | 8 +- .../examples/billing/historical/main.go | 14 +- .../examples/billing/line_items/main.go | 13 +- .../examples/billing/linked_orgs/main.go | 13 +- .../examples/monitoring/logs/main.go | 13 +- .../examples/monitoring/metrics_disk/main.go | 13 +- .../monitoring/metrics_process/main.go | 13 +- .../examples/performance/archiving/main.go | 51 +- usage-examples/go/atlas-sdk-go/go.mod | 14 + usage-examples/go/atlas-sdk-go/go.sum | 48 ++ .../atlas-sdk-go/internal/archive/analyze.go | 80 ++- .../internal/archive/analyze_test.go | 492 +++++------------- .../atlas-sdk-go/internal/clusters/utils.go | 16 +- .../internal/clusters/utils_srv_test.go | 81 +++ .../atlas-sdk-go/internal/config/appconfig.go | 48 -- .../atlas-sdk-go/internal/config/loadall.go | 51 +- .../atlas-sdk-go/internal/config/loadenv.go | 1 - 17 files changed, 459 insertions(+), 510 deletions(-) create mode 100644 usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/config/appconfig.go diff --git a/usage-examples/go/atlas-sdk-go/.env.example b/usage-examples/go/atlas-sdk-go/.env.example index 4edac11..02b6033 100644 --- a/usage-examples/go/atlas-sdk-go/.env.example +++ b/usage-examples/go/atlas-sdk-go/.env.example @@ -1,3 +1,5 @@ -MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id -MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret -ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional directory for downloads +MONGODB_ATLAS_SERVICE_ACCOUNT_ID= +MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= +ATLAS_DOWNLOADS_DIR=tmp/atlas_downloads # optional directory for downloads +APP_ENV= # optional env (development or production) +CONFIG_FILE=./configs/config..json # path to corresponding config file \ No newline at end of file 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 303ceb3..b0a31d3 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 @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/auth" @@ -21,18 +22,17 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 3292ee2..bd944f1 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 @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -20,18 +21,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 94c70fc..ee122b6 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 @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -18,18 +19,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 8ae955b..71c2e52 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 @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -19,18 +20,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 fefdddd..1f9182d 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 @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -19,18 +20,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("development") - configPath := "configs/config.development.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 ee92e3b..4700377 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 @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -20,18 +21,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 index a01bab7..40c1e91 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/archive" @@ -18,20 +19,19 @@ import ( ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) 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) @@ -44,7 +44,7 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // List all clusters in the project + // 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) @@ -52,22 +52,22 @@ func main() { fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Process each cluster + // Connect to each cluster and analyze collections for archiving failedArchives := 0 totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Find collections suitable for archiving - // NOTE: This function passes example database/collection names. - // In a real production scenario, you would analyze data patterns and customize the selection logic. + // Find collections suitable for archiving based on specific criteria. + // NOTE: The actual implementation of this function would involve more complex logic + // to determine which collections are eligible for archiving. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) totalCandidates += len(candidates) fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", - totalCandidates, clusterName) + len(candidates), clusterName) - // Step 4: Configure online archive for each candidate collection + // Configure online archive for each candidate collection for _, candidate := range candidates { fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) @@ -96,24 +96,11 @@ func main() { // NOTE: INTERNAL // ** OUTPUT EXAMPLE ** // -// Starting archive analysis for project: 5f60207f14dfb25d24511201 -// -//Found 2 clusters to analyze -// -//=== Analyzing cluster: Cluster0 === -//Found 2 collections eligible for archiving in cluster Cluster0 -//- Configuring archive for sample_analytics.transactions -//Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields -//- Configuring archive for sample_logs.application_logs -//Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields +//Starting archive analysis for project: 634f249136136a2dd3f8d8f9 // -//=== Analyzing cluster: AtlasCluster === -//Found 4 collections eligible for archiving in cluster AtlasCluster -//- Configuring archive for sample_analytics.transactions -//Failed to configure archive: validate archive candidate for sample_analytics.transactions: date field transaction_date must be included in partition fields -//- Configuring archive for sample_logs.application_logs -//Failed to configure archive: validate archive candidate for sample_logs.application_logs: date field timestamp must be included in partition fields +//Found 1 clusters to analyze // -//WARNING: 4 of 4 archive configurations failed +//=== 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 0c084dd..aac1478 100644 --- a/usage-examples/go/atlas-sdk-go/go.mod +++ b/usage-examples/go/atlas-sdk-go/go.mod @@ -10,6 +10,7 @@ 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 ( @@ -20,3 +21,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/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 index c4299fd..79e5acd 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -1,12 +1,17 @@ package archive import ( + "atlas-sdk-go/internal/errors" "context" "fmt" + "time" - "go.mongodb.org/atlas-sdk/v20250219001/admin" + "atlas-sdk-go/internal/clusters" - "atlas-sdk-go/internal/errors" + "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 @@ -46,10 +51,10 @@ type ExpireAfterDays struct { ExpireAfterDays int `json:"expireAfterDays,omitempty"` } -// CollectionsForArchiving identifies collections suitable for archiving as a simplified example for demonstration purposes. +// CollectionsForArchivingExample identifies collections suitable for archiving as a simplified example for demonstration purposes. // This function returns a list of Candidates that meet the archiving criteria // NOTE: In a real implementation, you would determine which collections are eligible based on criteria analysis such as size, age, and access patterns. -func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, +func CollectionsForArchivingExample(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { // For demonstration purposes, we specify example Candidates return []Candidate{ @@ -179,3 +184,70 @@ func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, return nil } + +// CollectionsForArchiving retrieves collections from a MongoDB Atlas cluster that are candidates for archiving. +// It connects to the cluster using the official MongoDB Go Driver and lists collections based on specified criteria. +// NOTE: This is a simplified example; in a real implementation, you would analyze collections based on size, age, +// access patterns, and other factors to determine candidates for archiving. +func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { + candidates := make([]Candidate, 0) + + // Get the SRV connection string for the cluster + srv, err := clusters.GetClusterSRVConnectionString(ctx, sdk, projectID, clusterName) + if err != nil || srv == "" { + return candidates + } + + 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 candidates + } + defer func() { _ = client.Disconnect(context.Background()) }() + + _ = client.Ping(ctxConn, nil) + + dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) + if err != nil { + return candidates + } + + // Set the simple demo criteria for archiving collections, skipping internal databases. + // NOTE: For this example, we assume collections with more than 100,000 documents are candidates + // for archiving, but this threshold can be adjusted based on your requirements. + const docThreshold = 100000 + for _, dbName := range dbNames { + if dbName == "admin" || dbName == "local" || dbName == "config" { + continue + } + 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 + } + if count >= docThreshold { + candidates = append(candidates, Candidate{ + DatabaseName: dbName, + CollectionName: collName, + DateField: "createdAt", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + }) + } + } + } + return candidates +} 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 index 4c41e96..ba70397 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go @@ -2,6 +2,9 @@ package archive import ( "context" + "net/http" + "net/http/httptest" + "sort" "testing" "github.com/stretchr/testify/assert" @@ -9,401 +12,186 @@ import ( "go.mongodb.org/atlas-sdk/v20250219001/admin" ) -func TestDefaultOptions_ReturnsExpectedDefaults(t *testing.T) { - t.Parallel() - - opts := DefaultOptions() - - assert.Equal(t, 2, opts.DefaultRetentionMultiplier) - assert.Equal(t, 30, opts.MinimumRetentionDays) - assert.True(t, opts.EnableDataExpiration) - assert.Equal(t, "DAILY", opts.ArchiveSchedule) -} - -func TestCollectionsForArchiving_ReturnsExpectedCandidates(t *testing.T) { - t.Parallel() - ctx := context.Background() - var client *admin.APIClient - - candidates := CollectionsForArchiving(ctx, client, "project123", "cluster456") - - require.Len(t, candidates, 2) - - analyticsCandidate := candidates[0] - assert.Equal(t, "sample_analytics", analyticsCandidate.DatabaseName) - assert.Equal(t, "transactions", analyticsCandidate.CollectionName) - assert.Equal(t, "transaction_date", analyticsCandidate.DateField) - assert.Equal(t, "DATE", analyticsCandidate.DateFormat) - assert.Equal(t, 90, analyticsCandidate.RetentionDays) - assert.Equal(t, []string{"customer_id", "merchant"}, analyticsCandidate.PartitionFields) - - logsCandidate := candidates[1] - assert.Equal(t, "sample_logs", logsCandidate.DatabaseName) - assert.Equal(t, "application_logs", logsCandidate.CollectionName) - assert.Equal(t, "timestamp", logsCandidate.DateField) - assert.Equal(t, "EPOCH_MILLIS", logsCandidate.DateFormat) - assert.Equal(t, 30, logsCandidate.RetentionDays) - assert.Equal(t, []string{"service_name", "log_level"}, logsCandidate.PartitionFields) +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 TestCollectionsForArchiving_HandlesNilClientGracefully(t *testing.T) { - t.Parallel() +func TestCollectionsForArchiving_ReturnsEmpty_WhenSRVLookupFails(t *testing.T) { ctx := context.Background() - candidates := CollectionsForArchiving(ctx, nil, "project123", "cluster456") - - assert.Len(t, candidates, 2) -} - -func TestCollectionsForArchiving_HandlesEmptyProjectIDAndClusterName(t *testing.T) { - t.Parallel() - ctx := context.Background() - var client *admin.APIClient - - candidates := CollectionsForArchiving(ctx, client, "", "") - - assert.Len(t, candidates, 2) -} - -func TestValidateCandidate_SucceedsWithValidCandidate(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"created_at", "user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) -} - -func TestValidateCandidate_FailsWhenDatabaseNameIsEmpty(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "database name and collection name are required") -} - -func TestValidateCandidate_FailsWhenCollectionNameIsEmpty(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"user_id"}, + // Simulate Atlas returning an error for GetCluster + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"true"}`)) } - opts := DefaultOptions() + client := newTestAtlasClient(t, handler) - err := ValidateCandidate(candidate, opts) + candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - assert.Error(t, err) - assert.Contains(t, err.Error(), "database name and collection name are required") + require.NotNil(t, candidates) + assert.Len(t, candidates, 0) } -func TestValidateCandidate_FailsWhenBothDatabaseAndCollectionNamesAreEmpty(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "", - CollectionName: "", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "database name and collection name are required") -} - -func TestValidateCandidate_FailsWhenRetentionDaysBelowMinimum(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 15, - PartitionFields: []string{"user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "retention days must be at least 30") -} +func TestCollectionsForArchiving_ReturnsEmpty_WhenMongoConnectFails(t *testing.T) { + ctx := context.Background() -func TestValidateCandidate_FailsWhenRetentionDaysEqualsZero(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 0, - PartitionFields: []string{"user_id"}, + // Use closed port to fail quickly + 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://127.0.0.1:1"} + }`)) } - opts := DefaultOptions() + client := newTestAtlasClient(t, handler) - err := ValidateCandidate(candidate, opts) + candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - assert.Error(t, err) - assert.Contains(t, err.Error(), "retention days must be at least 30") + require.NotNil(t, candidates) + assert.Len(t, candidates, 0) } -func TestValidateCandidate_FailsWhenPartitionFieldsAreEmpty(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) +// ---- Fakes for mongo client interfaces ---- - assert.Error(t, err) - assert.Contains(t, err.Error(), "at least one partition field is required") +type fakeCollection struct { + count int64 + countErr error } -func TestValidateCandidate_FailsWhenPartitionFieldsAreNil(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: nil, +func (f *fakeCollection) EstimatedDocumentCount(ctx context.Context) (int64, error) { + if f.countErr != nil { + return 0, f.countErr } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "at least one partition field is required") + return f.count, nil } -func TestValidateCandidate_SucceedsWithCustomMinimumRetentionDays(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 45, - PartitionFields: []string{"created_at", "user_id"}, - } - opts := Options{ - MinimumRetentionDays: 40, - } - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) +type fakeDatabase struct { + collections map[string]*fakeCollection + listErr error } -func TestValidateCandidate_FailsWhenRetentionDaysBelowCustomMinimum(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 35, - PartitionFields: []string{"user_id"}, +func (f *fakeDatabase) ListCollectionNames(ctx context.Context, filter interface{}) ([]string, error) { + if f.listErr != nil { + return nil, f.listErr } - opts := Options{ - MinimumRetentionDays: 40, + names := make([]string, 0, len(f.collections)) + for k := range f.collections { + names = append(names, k) } - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "retention days must be at least 40") + // keep deterministic order + sort.Strings(names) + return names, nil } - -func TestValidateCandidate_SucceedsWithMultiplePartitionFields(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"created_at", "user_id", "tenant_id", "category"}, +func (f *fakeDatabase) Collection(name string) mongoCollection { + c, ok := f.collections[name] + if !ok { + return &fakeCollection{} } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) + return c } -func TestValidateCandidate_SucceedsWithEpochMillisDateFormat(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "timestamp", - DateFormat: "EPOCH_MILLIS", - RetentionDays: 90, - PartitionFields: []string{"timestamp", "service_name"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) -} - -func TestValidateCandidate_SucceedsWhenRetentionDaysEqualsMinimum(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 30, - PartitionFields: []string{"created_at", "user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) +type fakeClient struct { + dbs map[string]*fakeDatabase } -func TestValidateCandidate_FailsWhenDateFieldNotInPartitionFields(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"user_id", "tenant_id"}, +func (f *fakeClient) Ping(ctx context.Context) error { return nil } +func (f *fakeClient) ListDatabaseNames(ctx context.Context, filter interface{}) ([]string, error) { + names := make([]string, 0, len(f.dbs)) + for k := range f.dbs { + names = append(names, k) } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "date field created_at must be included in partition fields") + return names, nil } - -func TestValidateCandidate_FailsWithInvalidDateFormat(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "INVALID_FORMAT", - RetentionDays: 60, - PartitionFields: []string{"created_at", "user_id"}, +func (f *fakeClient) Database(name string) mongoDatabase { + db, ok := f.dbs[name] + if !ok { + return &fakeDatabase{collections: map[string]*fakeCollection{}} } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid date format: INVALID_FORMAT") + return db } +func (f *fakeClient) Disconnect(ctx context.Context) error { return nil } -func TestValidateCandidate_SucceedsWithValidDateFormats(t *testing.T) { - t.Parallel() - validFormats := []string{"DATE", "EPOCH_SECONDS", "EPOCH_MILLIS", "EPOCH_NANOSECONDS", "OBJECTID"} - - for _, format := range validFormats { - t.Run("format_"+format, func(t *testing.T) { - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "timestamp", - DateFormat: format, - RetentionDays: 60, - PartitionFields: []string{"timestamp", "user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) - }) +// helper to stub Atlas SRV lookup with any valid URI string +func okSRVHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"connectionStrings": {"standardSrv": "mongodb://fake"}}`)) } } -func TestValidateCandidate_SucceedsWhenDateFieldIsEmpty(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "", - DateFormat: "", - RetentionDays: 60, - PartitionFields: []string{"user_id"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) -} +func TestCollectionsForArchiving_SuccessThresholdAndSkipInternal(t *testing.T) { + ctx := context.Background() -func TestValidateCandidate_SucceedsWhenDateFieldIsFirstInPartitionFields(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"created_at", "user_id", "tenant_id"}, + // Arrange fake mongo client + old := newMongoClient + t.Cleanup(func() { newMongoClient = old }) + newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { + return &fakeClient{dbs: map[string]*fakeDatabase{ + "admin": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, + "local": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, + "config": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, + "appdb": {collections: map[string]*fakeCollection{ + "small": {count: 10}, + "big": {count: 100000}, // threshold match + }}, + }}, nil + } + client := newTestAtlasClient(t, okSRVHandler()) + + // Act + candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") + + // Assert + require.NotNil(t, candidates) + assert.Equal(t, []Candidate{ // order is deterministic in our fake + {DatabaseName: "appdb", CollectionName: "big", DateField: "createdAt", DateFormat: "DATE", RetentionDays: 90, PartitionFields: []string{"createdAt"}}, + }, candidates) +} + +func TestCollectionsForArchiving_SkipsOnListCollectionError(t *testing.T) { + ctx := context.Background() + old := newMongoClient + t.Cleanup(func() { newMongoClient = old }) + newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { + return &fakeClient{dbs: map[string]*fakeDatabase{ + "appdb": {collections: map[string]*fakeCollection{"big": {count: 200000}}}, + "bad_db": {collections: map[string]*fakeCollection{"ignored": {count: 999999}}, listErr: assert.AnError}, + }}, nil } - opts := DefaultOptions() + client := newTestAtlasClient(t, okSRVHandler()) - err := ValidateCandidate(candidate, opts) + candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - assert.NoError(t, err) + require.NotNil(t, candidates) + // Should include appdb.big only; bad_db is skipped due to error + assert.Equal(t, 1, len(candidates)) + assert.Equal(t, "appdb", candidates[0].DatabaseName) + assert.Equal(t, "big", candidates[0].CollectionName) } -func TestValidateCandidate_SucceedsWhenDateFieldIsLastInPartitionFields(t *testing.T) { - t.Parallel() - candidate := Candidate{ - DatabaseName: "testdb", - CollectionName: "testcoll", - DateField: "created_at", - DateFormat: "DATE", - RetentionDays: 60, - PartitionFields: []string{"user_id", "tenant_id", "created_at"}, - } - opts := DefaultOptions() - - err := ValidateCandidate(candidate, opts) - - assert.NoError(t, err) +func TestCollectionsForArchiving_SkipsOnCountError(t *testing.T) { + ctx := context.Background() + old := newMongoClient + t.Cleanup(func() { newMongoClient = old }) + newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { + return &fakeClient{dbs: map[string]*fakeDatabase{ + "appdb": {collections: map[string]*fakeCollection{ + "bad": {count: 0, countErr: assert.AnError}, + "ok": {count: 150000}, + }}, + }}, nil + } + client := newTestAtlasClient(t, okSRVHandler()) + + candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") + + require.NotNil(t, candidates) + assert.Equal(t, 1, len(candidates)) + assert.Equal(t, "ok", candidates[0].CollectionName) } diff --git a/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go index 693d032..6f1f51f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils.go @@ -33,7 +33,6 @@ func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *admin.ListAtlasProcessesApiParams, clusterName string) (string, error) { req := sdk.ListAtlasProcesses(ctx, p.GroupId) - // List all processes in the project r, _, err := req.Execute() if err != nil { return "", errors.FormatError("list atlas processes", p.GroupId, err) @@ -55,3 +54,18 @@ func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, 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_srv_test.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go new file mode 100644 index 0000000..241c5df --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go @@ -0,0 +1,81 @@ +package clusters + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +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/appconfig.go b/usage-examples/go/atlas-sdk-go/internal/config/appconfig.go deleted file mode 100644 index 5243154..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/config/appconfig.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - goErr "errors" - "os" - - "atlas-sdk-go/internal/errors" -) - -// Environment defines the runtime environment for the application -type Environment string - -const ( - envDevelopment Environment = "development" - envStaging Environment = "test" - envProduction Environment = "production" -) - -// AppConfig contains all environment-specific configurations -type AppConfig struct { - environment Environment - config Config - secrets Secrets -} - -// LoadAppConfig loads the application configuration based on the provided config file and environment. -// Returns the app's configuration or any error encountered during loading -func LoadAppConfig(configFile string, env Environment) (AppConfig, error) { - if configFile == "" { - return AppConfig{}, goErr.New("config file path must be provided") - } - if _, err := os.Stat(configFile); os.IsNotExist(err) { - return AppConfig{}, &errors.NotFoundError{Resource: "configuration file", ID: configFile} - } - secrets, err := LoadSecrets() - if err != nil { - return AppConfig{}, errors.WithContext(err, "loading secrets") - } - config, err := LoadConfig(configFile) - if err != nil { - return AppConfig{}, errors.WithContext(err, "loading config") - } - return AppConfig{ - environment: env, - config: config, - secrets: secrets, - }, nil -} 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 8c819df..bb7bee4 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -1,44 +1,31 @@ package config import ( + "atlas-sdk-go/internal/errors" "fmt" + "os" + "strings" ) -// EnvironmentNames defines valid runtime environments -var allowedEnvironments = map[Environment]struct{}{ - envDevelopment: {}, - envStaging: {}, - envProduction: {}, -} +const defaultConfigDir = "configs" -func ValidateEnvironment(env string) bool { - _, ok := allowedEnvironments[Environment(env)] - return ok -} +// LoadAll loads both secrets and configuration from the specified paths. +func LoadAll(configPath string) (Secrets, Config, error) { + if strings.TrimSpace(configPath) == "" { + configPath = fmt.Sprintf("%s/config.json", defaultConfigDir) // Default path if not specified in environment + } -// LoadAll loads the application configuration and secrets based on the provided environment name and optional config file path. -// If configPath is empty, it defaults to "configs/config.{env}.json" based on the environment name. -// If envName is empty, it defaults to "configs/config.json". -// Parameters: -// - envName: Environment to load configuration for (development, staging, production) -// - configPath: Optional explicit path to the configuration file -// -// Returns: -// - Secrets: Loaded secrets -// - Config: Loaded application configuration -// - error: Any error encountered during loading -func LoadAll(envName Environment, configPath string) (Secrets, Config, error) { - var configFile string - if configPath != "" { - configFile = configPath - } else if envName != "" { - configFile = fmt.Sprintf("configs/config.%s.json", envName) - } else { - configFile = "configs/config.json" + if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { + return Secrets{}, Config{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} + } + + secrets, err := LoadSecrets() + if err != nil { + return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") } - appConfig, err := LoadAppConfig(configFile, envName) + cfg, err := LoadConfig(configPath) if err != nil { - return Secrets{}, Config{}, err + return Secrets{}, Config{}, errors.WithContext(err, "loading config") } - return appConfig.secrets, appConfig.config, nil + return secrets, cfg, 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 index 2df3778..0778633 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadenv.go @@ -6,7 +6,6 @@ import ( "os" ) -// Environment variable constants const ( envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" From 117ee364bcecad000a7ac3b059e4da4a20616380 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 20 Aug 2025 07:14:23 -0400 Subject: [PATCH 12/19] Re-generate snippets...again... --- .../main.snippet.archive-collections.go | 30 +- .../go/atlas-sdk-go/main.snippet.get-logs.go | 13 +- .../main.snippet.get-metrics-dev.go | 13 +- .../main.snippet.get-metrics-prod.go | 13 +- .../main.snippet.historical-billing.go | 14 +- .../atlas-sdk-go/main.snippet.line-items.go | 13 +- .../main.snippet.linked-billing.go | 13 +- .../go/atlas-sdk-go/project-copy/.env.example | 7 +- .../go/atlas-sdk-go/project-copy/.gitignore | 6 +- .../go/atlas-sdk-go/project-copy/README.md | 5 +- .../examples/billing/historical/main.go | 14 +- .../examples/billing/line_items/main.go | 13 +- .../examples/billing/linked_orgs/main.go | 13 +- .../examples/monitoring/logs/main.go | 13 +- .../examples/monitoring/metrics_disk/main.go | 13 +- .../monitoring/metrics_process/main.go | 13 +- .../examples/performance/archiving/main.go | 30 +- .../go/atlas-sdk-go/project-copy/go.mod | 14 + .../go/atlas-sdk-go/project-copy/go.sum | 48 ++++ .../project-copy/internal/archive/analyze.go | 114 +++++--- .../project-copy/internal/clusters/utils.go | 16 +- .../project-copy/internal/config/appconfig.go | 48 ---- .../project-copy/internal/config/loadall.go | 52 ++-- .../project-copy/internal/config/loadenv.go | 1 - usage-examples/go/atlas-sdk-go/.env.example | 3 +- usage-examples/go/atlas-sdk-go/.gitignore | 6 +- usage-examples/go/atlas-sdk-go/README.md | 5 +- .../examples/billing/historical/main.go | 2 +- .../atlas-sdk-go/internal/archive/analyze.go | 42 +-- .../internal/archive/analyze_test.go | 257 ++++++------------ .../internal/clusters/utils_srv_test.go | 81 ------ .../internal/clusters/utils_test.go | 71 +++++ .../atlas-sdk-go/internal/config/loadall.go | 3 +- 33 files changed, 469 insertions(+), 530 deletions(-) delete mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go delete mode 100644 usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go 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 index 22e178f..6e79876 100644 --- 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/archive" @@ -15,20 +16,19 @@ import ( ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) 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) @@ -41,7 +41,7 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // List all clusters in the project + // 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) @@ -49,22 +49,22 @@ func main() { fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Process each cluster + // Connect to each cluster and analyze collections for archiving failedArchives := 0 totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Find collections suitable for archiving - // NOTE: This function passes example database/collection names. - // In a real production scenario, you would analyze data patterns and customize the selection logic. + // Find collections suitable for archiving based on specific criteria. + // NOTE: The actual implementation of this function would involve more complex logic + // to determine which collections are eligible for archiving. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) totalCandidates += len(candidates) fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", - totalCandidates, clusterName) + len(candidates), clusterName) - // Step 4: Configure online archive for each candidate collection + // Configure online archive for each candidate collection for _, candidate := range candidates { fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) 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 9f391ff..2774427 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -16,18 +17,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 4360f21..16f2873 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 @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -16,18 +17,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("development") - configPath := "configs/config.development.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 944de05..f492013 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 @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -17,18 +18,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 82f758a..646722f 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/auth" @@ -18,18 +19,17 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_PATH") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 68d5748..a104f2b 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -17,18 +18,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 2fec72c..f4236f4 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 @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -15,18 +16,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 4edac11..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,3 +1,4 @@ -MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id -MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret -ATLAS_DOWNLOADS_DIR="tmp/atlas_downloads" # optional directory for downloads +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 8e9c5d6..f65e948 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.gitignore @@ -1,9 +1,9 @@ # Secrets (keep example) -.env +tmp/.env !.env.example -.env.development +tmp/.env.development .env.production -.env.test +tmp/.env.test # config files (keep example) configs 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 ddb3e6b..6703e06 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 @@ -61,9 +61,10 @@ and improvements to existing code. 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" ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. 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 d1f1296..20f3afd 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/auth" @@ -17,18 +18,17 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_PATH") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 36e2093..313bcdd 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -16,18 +17,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 a140ad2..6b11bd4 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -14,18 +15,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 f28a6a5..e55ce14 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -15,18 +16,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 f63f609..296be82 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 @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -15,18 +16,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.development" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("development") - configPath := "configs/config.development.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 27b6e08..6543ef9 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 @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -16,18 +17,18 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - ctx := context.Background() - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) } + ctx := context.Background() client, err := auth.NewClient(ctx, cfg, secrets) if err != nil { log.Fatalf("Failed to initialize authentication client: %v", err) 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 index e672077..1f02a11 100644 --- 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 @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "time" "atlas-sdk-go/internal/archive" @@ -14,20 +15,19 @@ import ( ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - if err := godotenv.Load(); err != nil { - log.Printf("Warning: could not load .env file: %v", err) + envFile := ".env.production" + if err := godotenv.Load(envFile); err != nil { + log.Printf("Warning: could not load %s file: %v", envFile, err) } - envName := config.Environment("production") - configPath := "configs/config.production.json" - secrets, cfg, err := config.LoadAll(envName, configPath) + configPath := os.Getenv("CONFIG_FILE") + secrets, cfg, err := config.LoadAll(configPath) 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) @@ -40,7 +40,7 @@ func main() { fmt.Printf("Starting archive analysis for project: %s\n", projectID) - // List all clusters in the project + // 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) @@ -48,22 +48,22 @@ func main() { fmt.Printf("\nFound %d clusters to analyze\n", len(clusters.GetResults())) - // Process each cluster + // Connect to each cluster and analyze collections for archiving failedArchives := 0 totalCandidates := 0 for _, cluster := range clusters.GetResults() { clusterName := cluster.GetName() fmt.Printf("\n=== Analyzing cluster: %s ===", clusterName) - // Find collections suitable for archiving - // NOTE: This function passes example database/collection names. - // In a real production scenario, you would analyze data patterns and customize the selection logic. + // Find collections suitable for archiving based on specific criteria. + // NOTE: The actual implementation of this function would involve more complex logic + // to determine which collections are eligible for archiving. candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) totalCandidates += len(candidates) fmt.Printf("\nFound %d collections eligible for archiving in cluster %s\n", - totalCandidates, clusterName) + len(candidates), clusterName) - // Step 4: Configure online archive for each candidate collection + // Configure online archive for each candidate collection for _, candidate := range candidates { fmt.Printf("- Configuring archive for %s.%s\n", candidate.DatabaseName, candidate.CollectionName) 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 e7afd81..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 @@ -5,6 +5,7 @@ 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 ( @@ -13,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 index c4299fd..6832fec 100644 --- 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 @@ -3,10 +3,16 @@ package archive import ( "context" "fmt" - - "go.mongodb.org/atlas-sdk/v20250219001/admin" + "time" "atlas-sdk-go/internal/errors" + + "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 @@ -31,7 +37,7 @@ type Options struct { ArchiveSchedule string } -// DefaultOptions provides sensible defaults for archiving +// DefaultOptions provides defaults for archiving func DefaultOptions() Options { return Options{ DefaultRetentionMultiplier: 2, @@ -41,37 +47,6 @@ func DefaultOptions() Options { } } -type ExpireAfterDays struct { - // NOTE: this placeholder struct can be extended to include more complex rules if needed - ExpireAfterDays int `json:"expireAfterDays,omitempty"` -} - -// CollectionsForArchiving identifies collections suitable for archiving as a simplified example for demonstration purposes. -// This function returns a list of Candidates that meet the archiving criteria -// NOTE: In a real implementation, you would determine which collections are eligible based on criteria analysis such as size, age, and access patterns. -func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, - projectID, clusterName string) []Candidate { - // For demonstration purposes, we specify example Candidates - return []Candidate{ - { - DatabaseName: "sample_analytics", - CollectionName: "transactions", - DateField: "transaction_date", - DateFormat: "DATE", - RetentionDays: 90, - PartitionFields: []string{"customer_id", "merchant"}, - }, - { - DatabaseName: "sample_logs", - CollectionName: "application_logs", - DateField: "timestamp", - DateFormat: "EPOCH_MILLIS", - RetentionDays: 30, - PartitionFields: []string{"service_name", "log_level"}, - }, - } -} - // ValidateCandidate ensures the archiving candidate meets requirements func ValidateCandidate(candidate Candidate, opts Options) error { if candidate.DatabaseName == "" || candidate.CollectionName == "" { @@ -115,7 +90,9 @@ func ValidateCandidate(candidate Candidate, opts Options) error { return nil } -// ConfigureOnlineArchive configures online archive for a collection +// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. +// It validates the candidate, sets up partition fields, and creates the archive schedule. +// 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) error { @@ -179,3 +156,70 @@ func ConfigureOnlineArchive(ctx context.Context, sdk *admin.APIClient, return nil } + +// CollectionsForArchiving retrieves collections from a MongoDB Atlas cluster that are candidates for archiving. +// It connects to the cluster using the official MongoDB Go Driver and lists collections based on specified criteria. +// NOTE: This is a simplified example; in a real implementation, you would analyze collections based on size, age, +// access patterns, and other factors to determine candidates for archiving. +func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { + candidates := make([]Candidate, 0) + + // Get the SRV connection string for the cluster + srv, err := clusters.GetClusterSRVConnectionString(ctx, sdk, projectID, clusterName) + if err != nil || srv == "" { + return candidates + } + + 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 candidates + } + defer func() { _ = client.Disconnect(context.Background()) }() + + _ = client.Ping(ctxConn, nil) + + dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) + if err != nil { + return candidates + } + + // Set the simple demo criteria for archiving collections, skipping internal databases. + // NOTE: For this example, we assume collections with more than 100,000 documents are candidates + // for archiving. + const docThreshold = 100000 + for _, dbName := range dbNames { + if dbName == "admin" || dbName == "local" || dbName == "config" { + continue + } + 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 + } + if count >= docThreshold { + candidates = append(candidates, Candidate{ + DatabaseName: dbName, + CollectionName: collName, + DateField: "createdAt", + DateFormat: "DATE", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + }) + } + } + } + return candidates +} 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 index 693d032..6f1f51f 100644 --- 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 @@ -33,7 +33,6 @@ func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *admin.ListAtlasProcessesApiParams, clusterName string) (string, error) { req := sdk.ListAtlasProcesses(ctx, p.GroupId) - // List all processes in the project r, _, err := req.Execute() if err != nil { return "", errors.FormatError("list atlas processes", p.GroupId, err) @@ -55,3 +54,18 @@ func GetProcessIdForCluster(ctx context.Context, sdk admin.MonitoringAndLogsApi, 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/appconfig.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go deleted file mode 100644 index 5243154..0000000 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/appconfig.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - goErr "errors" - "os" - - "atlas-sdk-go/internal/errors" -) - -// Environment defines the runtime environment for the application -type Environment string - -const ( - envDevelopment Environment = "development" - envStaging Environment = "test" - envProduction Environment = "production" -) - -// AppConfig contains all environment-specific configurations -type AppConfig struct { - environment Environment - config Config - secrets Secrets -} - -// LoadAppConfig loads the application configuration based on the provided config file and environment. -// Returns the app's configuration or any error encountered during loading -func LoadAppConfig(configFile string, env Environment) (AppConfig, error) { - if configFile == "" { - return AppConfig{}, goErr.New("config file path must be provided") - } - if _, err := os.Stat(configFile); os.IsNotExist(err) { - return AppConfig{}, &errors.NotFoundError{Resource: "configuration file", ID: configFile} - } - secrets, err := LoadSecrets() - if err != nil { - return AppConfig{}, errors.WithContext(err, "loading secrets") - } - config, err := LoadConfig(configFile) - if err != nil { - return AppConfig{}, errors.WithContext(err, "loading config") - } - return AppConfig{ - environment: env, - config: config, - secrets: secrets, - }, 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 8c819df..15e1005 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 @@ -2,43 +2,31 @@ package config import ( "fmt" + "os" + "strings" + + "atlas-sdk-go/internal/errors" ) -// EnvironmentNames defines valid runtime environments -var allowedEnvironments = map[Environment]struct{}{ - envDevelopment: {}, - envStaging: {}, - envProduction: {}, -} +const defaultConfigDir = "configs" -func ValidateEnvironment(env string) bool { - _, ok := allowedEnvironments[Environment(env)] - return ok -} +// LoadAll loads both secrets and configuration from the specified paths. +func LoadAll(configPath string) (Secrets, Config, error) { + if strings.TrimSpace(configPath) == "" { + configPath = fmt.Sprintf("%s/config.json", defaultConfigDir) // Default path if not specified in environment + } -// LoadAll loads the application configuration and secrets based on the provided environment name and optional config file path. -// If configPath is empty, it defaults to "configs/config.{env}.json" based on the environment name. -// If envName is empty, it defaults to "configs/config.json". -// Parameters: -// - envName: Environment to load configuration for (development, staging, production) -// - configPath: Optional explicit path to the configuration file -// -// Returns: -// - Secrets: Loaded secrets -// - Config: Loaded application configuration -// - error: Any error encountered during loading -func LoadAll(envName Environment, configPath string) (Secrets, Config, error) { - var configFile string - if configPath != "" { - configFile = configPath - } else if envName != "" { - configFile = fmt.Sprintf("configs/config.%s.json", envName) - } else { - configFile = "configs/config.json" + if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { + return Secrets{}, Config{}, &errors.NotFoundError{Resource: "configuration file", ID: configPath} + } + + secrets, err := LoadSecrets() + if err != nil { + return Secrets{}, Config{}, errors.WithContext(err, "loading secrets") } - appConfig, err := LoadAppConfig(configFile, envName) + cfg, err := LoadConfig(configPath) if err != nil { - return Secrets{}, Config{}, err + return Secrets{}, Config{}, errors.WithContext(err, "loading config") } - return appConfig.secrets, appConfig.config, nil + return secrets, cfg, 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 index ed484ed..1e2605a 100644 --- 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 @@ -6,7 +6,6 @@ import ( "os" ) -// Environment variable constants const ( envServiceAccountID = "MONGODB_ATLAS_SERVICE_ACCOUNT_ID" envServiceAccountSecret = "MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET" diff --git a/usage-examples/go/atlas-sdk-go/.env.example b/usage-examples/go/atlas-sdk-go/.env.example index 02b6033..66f7781 100644 --- a/usage-examples/go/atlas-sdk-go/.env.example +++ b/usage-examples/go/atlas-sdk-go/.env.example @@ -1,5 +1,4 @@ MONGODB_ATLAS_SERVICE_ACCOUNT_ID= MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET= ATLAS_DOWNLOADS_DIR=tmp/atlas_downloads # optional directory for downloads -APP_ENV= # optional env (development or production) -CONFIG_FILE=./configs/config..json # path to corresponding config file \ No newline at end of file +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 8e9c5d6..f65e948 100644 --- a/usage-examples/go/atlas-sdk-go/.gitignore +++ b/usage-examples/go/atlas-sdk-go/.gitignore @@ -1,9 +1,9 @@ # Secrets (keep example) -.env +tmp/.env !.env.example -.env.development +tmp/.env.development .env.production -.env.test +tmp/.env.test # config files (keep example) configs diff --git a/usage-examples/go/atlas-sdk-go/README.md b/usage-examples/go/atlas-sdk-go/README.md index ddb3e6b..6703e06 100644 --- a/usage-examples/go/atlas-sdk-go/README.md +++ b/usage-examples/go/atlas-sdk-go/README.md @@ -61,9 +61,10 @@ and improvements to existing code. 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" ``` > **NOTE:** For production, use a secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) > instead of environment variables. 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 b0a31d3..8724de7 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 @@ -26,7 +26,7 @@ func main() { if err := godotenv.Load(envFile); err != nil { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") + configPath := os.Getenv("CONFIG_PATH") secrets, cfg, err := config.LoadAll(configPath) if err != nil { log.Fatalf("Failed to load configuration %v", err) diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go index 79e5acd..6832fec 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -1,11 +1,12 @@ package archive import ( - "atlas-sdk-go/internal/errors" "context" "fmt" "time" + "atlas-sdk-go/internal/errors" + "atlas-sdk-go/internal/clusters" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -36,7 +37,7 @@ type Options struct { ArchiveSchedule string } -// DefaultOptions provides sensible defaults for archiving +// DefaultOptions provides defaults for archiving func DefaultOptions() Options { return Options{ DefaultRetentionMultiplier: 2, @@ -46,37 +47,6 @@ func DefaultOptions() Options { } } -type ExpireAfterDays struct { - // NOTE: this placeholder struct can be extended to include more complex rules if needed - ExpireAfterDays int `json:"expireAfterDays,omitempty"` -} - -// CollectionsForArchivingExample identifies collections suitable for archiving as a simplified example for demonstration purposes. -// This function returns a list of Candidates that meet the archiving criteria -// NOTE: In a real implementation, you would determine which collections are eligible based on criteria analysis such as size, age, and access patterns. -func CollectionsForArchivingExample(ctx context.Context, sdk *admin.APIClient, - projectID, clusterName string) []Candidate { - // For demonstration purposes, we specify example Candidates - return []Candidate{ - { - DatabaseName: "sample_analytics", - CollectionName: "transactions", - DateField: "transaction_date", - DateFormat: "DATE", - RetentionDays: 90, - PartitionFields: []string{"customer_id", "merchant"}, - }, - { - DatabaseName: "sample_logs", - CollectionName: "application_logs", - DateField: "timestamp", - DateFormat: "EPOCH_MILLIS", - RetentionDays: 30, - PartitionFields: []string{"service_name", "log_level"}, - }, - } -} - // ValidateCandidate ensures the archiving candidate meets requirements func ValidateCandidate(candidate Candidate, opts Options) error { if candidate.DatabaseName == "" || candidate.CollectionName == "" { @@ -120,7 +90,9 @@ func ValidateCandidate(candidate Candidate, opts Options) error { return nil } -// ConfigureOnlineArchive configures online archive for a collection +// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. +// It validates the candidate, sets up partition fields, and creates the archive schedule. +// 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) error { @@ -220,7 +192,7 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI // Set the simple demo criteria for archiving collections, skipping internal databases. // NOTE: For this example, we assume collections with more than 100,000 documents are candidates - // for archiving, but this threshold can be adjusted based on your requirements. + // for archiving. const docThreshold = 100000 for _, dbName := range dbNames { if dbName == "admin" || dbName == "local" || dbName == "config" { 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 index ba70397..d33e39f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go @@ -1,197 +1,102 @@ package archive import ( - "context" - "net/http" - "net/http/httptest" - "sort" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mongodb.org/atlas-sdk/v20250219001/admin" ) -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 TestValidateCandidate_ReturnsError_WhenDatabaseOrCollectionNameMissing(t *testing.T) { + opts := DefaultOptions() + 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 TestCollectionsForArchiving_ReturnsEmpty_WhenSRVLookupFails(t *testing.T) { - ctx := context.Background() - - // Simulate Atlas returning an error for GetCluster - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(`{"error":"true"}`)) - } - client := newTestAtlasClient(t, handler) - - candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - - require.NotNil(t, candidates) - assert.Len(t, candidates, 0) -} - -func TestCollectionsForArchiving_ReturnsEmpty_WhenMongoConnectFails(t *testing.T) { - ctx := context.Background() - - // Use closed port to fail quickly - 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://127.0.0.1:1"} - }`)) - } - client := newTestAtlasClient(t, handler) - - candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - - require.NotNil(t, candidates) - assert.Len(t, candidates, 0) -} - -// ---- Fakes for mongo client interfaces ---- - -type fakeCollection struct { - count int64 - countErr error -} - -func (f *fakeCollection) EstimatedDocumentCount(ctx context.Context) (int64, error) { - if f.countErr != nil { - return 0, f.countErr - } - return f.count, nil -} - -type fakeDatabase struct { - collections map[string]*fakeCollection - listErr error -} - -func (f *fakeDatabase) ListCollectionNames(ctx context.Context, filter interface{}) ([]string, error) { - if f.listErr != nil { - return nil, f.listErr - } - names := make([]string, 0, len(f.collections)) - for k := range f.collections { - names = append(names, k) - } - // keep deterministic order - sort.Strings(names) - return names, nil -} -func (f *fakeDatabase) Collection(name string) mongoCollection { - c, ok := f.collections[name] - if !ok { - return &fakeCollection{} - } - return c +func TestValidateCandidate_ReturnsError_WhenRetentionDaysTooLow(t *testing.T) { + opts := DefaultOptions() + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 10, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) } -type fakeClient struct { - dbs map[string]*fakeDatabase -} - -func (f *fakeClient) Ping(ctx context.Context) error { return nil } -func (f *fakeClient) ListDatabaseNames(ctx context.Context, filter interface{}) ([]string, error) { - names := make([]string, 0, len(f.dbs)) - for k := range f.dbs { - names = append(names, k) - } - return names, nil -} -func (f *fakeClient) Database(name string) mongoDatabase { - db, ok := f.dbs[name] - if !ok { - return &fakeDatabase{collections: map[string]*fakeCollection{}} - } - return db +func TestValidateCandidate_ReturnsError_WhenNoPartitionFields(t *testing.T) { + opts := DefaultOptions() + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) } -func (f *fakeClient) Disconnect(ctx context.Context) error { return nil } -// helper to stub Atlas SRV lookup with any valid URI string -func okSRVHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"connectionStrings": {"standardSrv": "mongodb://fake"}}`)) - } +func TestValidateCandidate_ReturnsError_WhenInvalidDateFormat(t *testing.T) { + opts := DefaultOptions() + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "INVALID", + }, opts) + assert.Error(t, err) } -func TestCollectionsForArchiving_SuccessThresholdAndSkipInternal(t *testing.T) { - ctx := context.Background() - - // Arrange fake mongo client - old := newMongoClient - t.Cleanup(func() { newMongoClient = old }) - newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { - return &fakeClient{dbs: map[string]*fakeDatabase{ - "admin": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, - "local": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, - "config": {collections: map[string]*fakeCollection{"sys": {count: 999999}}}, - "appdb": {collections: map[string]*fakeCollection{ - "small": {count: 10}, - "big": {count: 100000}, // threshold match - }}, - }}, nil - } - client := newTestAtlasClient(t, okSRVHandler()) - - // Act - candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - - // Assert - require.NotNil(t, candidates) - assert.Equal(t, []Candidate{ // order is deterministic in our fake - {DatabaseName: "appdb", CollectionName: "big", DateField: "createdAt", DateFormat: "DATE", RetentionDays: 90, PartitionFields: []string{"createdAt"}}, - }, candidates) +func TestValidateCandidate_ReturnsError_WhenDateFieldNotInPartitionFields(t *testing.T) { + opts := DefaultOptions() + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"otherField"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.Error(t, err) } -func TestCollectionsForArchiving_SkipsOnListCollectionError(t *testing.T) { - ctx := context.Background() - old := newMongoClient - t.Cleanup(func() { newMongoClient = old }) - newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { - return &fakeClient{dbs: map[string]*fakeDatabase{ - "appdb": {collections: map[string]*fakeCollection{"big": {count: 200000}}}, - "bad_db": {collections: map[string]*fakeCollection{"ignored": {count: 999999}}, listErr: assert.AnError}, - }}, nil - } - client := newTestAtlasClient(t, okSRVHandler()) - - candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - - require.NotNil(t, candidates) - // Should include appdb.big only; bad_db is skipped due to error - assert.Equal(t, 1, len(candidates)) - assert.Equal(t, "appdb", candidates[0].DatabaseName) - assert.Equal(t, "big", candidates[0].CollectionName) +func TestValidateCandidate_Succeeds_WithValidInput(t *testing.T) { + opts := DefaultOptions() + err := ValidateCandidate(Candidate{ + DatabaseName: "db", + CollectionName: "coll", + RetentionDays: 90, + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + }, opts) + assert.NoError(t, err) } -func TestCollectionsForArchiving_SkipsOnCountError(t *testing.T) { - ctx := context.Background() - old := newMongoClient - t.Cleanup(func() { newMongoClient = old }) - newMongoClient = func(ctx context.Context, uri string) (mongoClient, error) { - return &fakeClient{dbs: map[string]*fakeDatabase{ - "appdb": {collections: map[string]*fakeCollection{ - "bad": {count: 0, countErr: assert.AnError}, - "ok": {count: 150000}, - }}, - }}, nil - } - client := newTestAtlasClient(t, okSRVHandler()) - - candidates := CollectionsForArchiving(ctx, client, "proj1", "Cluster0") - - require.NotNil(t, candidates) - assert.Equal(t, 1, len(candidates)) - assert.Equal(t, "ok", candidates[0].CollectionName) +func TestDefaultOptions_ReturnsExpectedDefaults(t *testing.T) { + opts := DefaultOptions() + assert.Equal(t, 2, opts.DefaultRetentionMultiplier) + assert.Equal(t, 30, opts.MinimumRetentionDays) + assert.True(t, opts.EnableDataExpiration) + assert.Equal(t, "DAILY", opts.ArchiveSchedule) } diff --git a/usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go deleted file mode 100644 index 241c5df..0000000 --- a/usage-examples/go/atlas-sdk-go/internal/clusters/utils_srv_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package clusters - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mongodb.org/atlas-sdk/v20250219001/admin" -) - -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/clusters/utils_test.go b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go index bf0103f..06b9e7f 100644 --- a/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/clusters/utils_test.go @@ -2,6 +2,8 @@ package clusters import ( "context" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -168,3 +170,72 @@ func TestGetProcessIdForCluster_NoProcesses(t *testing.T) { 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 bb7bee4..15e1005 100644 --- a/usage-examples/go/atlas-sdk-go/internal/config/loadall.go +++ b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go @@ -1,10 +1,11 @@ package config import ( - "atlas-sdk-go/internal/errors" "fmt" "os" "strings" + + "atlas-sdk-go/internal/errors" ) const defaultConfigDir = "configs" From 871702562fe0166cba3c88ab12d5b47d6d5e5ecf Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 20 Aug 2025 08:14:45 -0400 Subject: [PATCH 13/19] Fix failing test --- usage-examples/go/atlas-sdk-go/internal/auth/client.go | 3 +++ usage-examples/go/atlas-sdk-go/internal/auth/client_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 9114ae0..c89bc16 100644 --- a/usage-examples/go/atlas-sdk-go/internal/auth/client.go +++ b/usage-examples/go/atlas-sdk-go/internal/auth/client.go @@ -12,6 +12,9 @@ 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(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.ServiceAccountID() == "" || secrets.ServiceAccountSecret() == "" { return nil, &errors.ValidationError{Message: "secrets cannot be 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 faf4888..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 @@ -36,7 +36,7 @@ func TestNewClient_returnsErrorWhenConfigIsNil(t *testing.T) { require.Nil(t, client) var validationErr *internalerrors.ValidationError require.True(t, assert.ErrorAs(t, err, &validationErr), "expected error to be *errors.ValidationError") - assert.Equal(t, "config cannot be nil", validationErr.Message) + assert.Equal(t, "config cannot be empty", validationErr.Message) } func TestNewClient_returnsErrorWhenSecretsAreNil(t *testing.T) { From ef1eb454f010b7902be3336e2c452f10a55d9389 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 21 Aug 2025 08:56:56 -0400 Subject: [PATCH 14/19] Apply Dachary feedback 2 --- .../main.snippet.archive-collections.go | 56 +++++- .../go/atlas-sdk-go/main.snippet.get-logs.go | 4 +- .../main.snippet.get-metrics-dev.go | 4 +- .../main.snippet.get-metrics-prod.go | 4 +- .../main.snippet.historical-billing.go | 4 +- .../atlas-sdk-go/main.snippet.line-items.go | 4 +- .../main.snippet.linked-billing.go | 4 +- .../go/atlas-sdk-go/project-copy/.gitignore | 8 +- .../go/atlas-sdk-go/project-copy/README.md | 2 +- .../project-copy/configs/config.example.json | 7 + .../examples/billing/historical/main.go | 4 +- .../examples/billing/line_items/main.go | 4 +- .../examples/billing/linked_orgs/main.go | 4 +- .../examples/monitoring/logs/main.go | 4 +- .../examples/monitoring/metrics_disk/main.go | 4 +- .../monitoring/metrics_process/main.go | 4 +- .../examples/performance/archiving/main.go | 56 +++++- .../project-copy/internal/archive/analyze.go | 128 +++---------- .../internal/archive/configure.go | 75 ++++++++ .../project-copy/internal/auth/client.go | 3 + .../project-copy/internal/config/loadall.go | 16 +- .../config/{loadenv.go => loadsecrets.go} | 0 .../project-copy/internal/fileutils/env.go | 23 +++ .../project-copy/internal/fileutils/path.go | 5 +- usage-examples/go/atlas-sdk-go/.gitignore | 8 +- usage-examples/go/atlas-sdk-go/README.md | 2 +- .../configs/config.development.json | 7 - .../configs/config.production.json | 7 - .../examples/billing/historical/main.go | 4 +- .../examples/billing/line_items/main.go | 4 +- .../examples/billing/linked_orgs/main.go | 4 +- .../examples/monitoring/logs/main.go | 4 +- .../examples/monitoring/metrics_disk/main.go | 4 +- .../monitoring/metrics_process/main.go | 4 +- .../examples/performance/archiving/main.go | 56 +++++- .../atlas-sdk-go/internal/archive/analyze.go | 128 +++---------- .../internal/archive/analyze_test.go | 20 +- .../internal/archive/configure.go | 75 ++++++++ .../internal/archive/configure_test.go | 171 ++++++++++++++++++ .../atlas-sdk-go/internal/config/loadall.go | 16 +- .../config/{loadenv.go => loadsecrets.go} | 0 .../go/atlas-sdk-go/internal/fileutils/env.go | 23 +++ .../go/atlas-sdk-go/internal/fileutils/io.go | 5 +- .../atlas-sdk-go/internal/fileutils/path.go | 5 +- .../go/atlas-sdk-go/scripts/bluehawk.sh | 15 ++ 45 files changed, 648 insertions(+), 341 deletions(-) create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/configure.go rename generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/{loadenv.go => loadsecrets.go} (100%) create mode 100644 generated-usage-examples/go/atlas-sdk-go/project-copy/internal/fileutils/env.go delete mode 100644 usage-examples/go/atlas-sdk-go/configs/config.development.json delete mode 100644 usage-examples/go/atlas-sdk-go/configs/config.production.json create mode 100644 usage-examples/go/atlas-sdk-go/internal/archive/configure.go create mode 100644 usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go rename usage-examples/go/atlas-sdk-go/internal/config/{loadenv.go => loadsecrets.go} (100%) create mode 100644 usage-examples/go/atlas-sdk-go/internal/fileutils/env.go 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 index 6e79876..18f5a61 100644 --- 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 @@ -5,7 +5,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/archive" @@ -21,8 +20,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } @@ -51,25 +49,62 @@ func main() { // 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 specific criteria. - // NOTE: The actual implementation of this function would involve more complex logic - // to determine which collections are eligible for archiving. - candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filtering 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) + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) if configureErr != nil { fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ @@ -81,8 +116,11 @@ func main() { } } + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } if failedArchives > 0 { - fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) + 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 2774427..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 @@ -5,7 +5,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -22,8 +21,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 16f2873..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 @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -22,8 +21,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 f492013..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 @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -23,8 +22,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 646722f..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 @@ -5,7 +5,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/auth" @@ -23,8 +22,7 @@ func main() { if err := godotenv.Load(envFile); err != nil { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_PATH") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 a104f2b..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 @@ -5,7 +5,6 @@ import ( "context" "fmt" "log" - "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -23,8 +22,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 f4236f4..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 @@ -5,7 +5,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -21,8 +20,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 f65e948..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,12 +1,10 @@ # Secrets (keep example) -tmp/.env +.env !.env.example -tmp/.env.development .env.production -tmp/.env.test -# config files (keep example) -configs +# Configs (keep example) +configs/config.json !configs/config.example.json # temporary files 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 6703e06..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 @@ -64,7 +64,7 @@ and improvements to existing code. 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" + 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. diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json new file mode 100644 index 0000000..9034951 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/configs/config.example.json @@ -0,0 +1,7 @@ +{ + "MONGODB_ATLAS_BASE_URL": "https://cloud.mongodb.com", + "ATLAS_ORG_ID": "", + "ATLAS_PROJECT_ID": "", + "ATLAS_CLUSTER_NAME": "", + "ATLAS_PROCESS_ID": "" +} 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 20f3afd..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 @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/auth" @@ -22,8 +21,7 @@ func main() { if err := godotenv.Load(envFile); err != nil { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_PATH") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 313bcdd..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 @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -22,8 +21,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 6b11bd4..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 @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -20,8 +19,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 e55ce14..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 @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -21,8 +20,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 296be82..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 @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -21,8 +20,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 6543ef9..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 @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -22,8 +21,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 index 1f02a11..894cde9 100644 --- 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 @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/archive" @@ -20,8 +19,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } @@ -50,25 +48,62 @@ func main() { // 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 specific criteria. - // NOTE: The actual implementation of this function would involve more complex logic - // to determine which collections are eligible for archiving. - candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filtering 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) + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) if configureErr != nil { fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ @@ -80,8 +115,11 @@ func main() { } } + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } if failedArchives > 0 { - fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) + 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/internal/archive/analyze.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/analyze.go index 6832fec..d01c43d 100644 --- 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 @@ -5,8 +5,6 @@ import ( "fmt" "time" - "atlas-sdk-go/internal/errors" - "atlas-sdk-go/internal/clusters" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -37,17 +35,16 @@ type Options struct { ArchiveSchedule string } -// DefaultOptions provides defaults for archiving +// 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{ - DefaultRetentionMultiplier: 2, - MinimumRetentionDays: 30, - EnableDataExpiration: true, - ArchiveSchedule: "DAILY", - } + return Options{} } -// ValidateCandidate ensures the archiving candidate meets requirements +// 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") @@ -90,84 +87,23 @@ func ValidateCandidate(candidate Candidate, opts Options) error { return nil } -// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. -// It validates the candidate, sets up partition fields, and creates the archive schedule. -// 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) error { - - opts := DefaultOptions() - - if err := ValidateCandidate(candidate, opts); err != nil { - return errors.FormatError("validate archive candidate", - fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), - err) - } - - // 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 +// CollectionStat represents basic statistics about a collection. +type CollectionStat struct { + DatabaseName string + CollectionName string + EstimatedCount int64 } -// CollectionsForArchiving retrieves collections from a MongoDB Atlas cluster that are candidates for archiving. -// It connects to the cluster using the official MongoDB Go Driver and lists collections based on specified criteria. -// NOTE: This is a simplified example; in a real implementation, you would analyze collections based on size, age, -// access patterns, and other factors to determine candidates for archiving. -func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { - candidates := make([]Candidate, 0) +// 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 candidates + return stats } ctxConn, cancel := context.WithTimeout(ctx, 2*time.Second) @@ -179,7 +115,7 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI // Connect to the cluster using the official MongoDB Go Driver client, err := mongo.Connect(ctxConn, clientOpts) if err != nil { - return candidates + return stats } defer func() { _ = client.Disconnect(context.Background()) }() @@ -187,17 +123,10 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) if err != nil { - return candidates + return stats } - // Set the simple demo criteria for archiving collections, skipping internal databases. - // NOTE: For this example, we assume collections with more than 100,000 documents are candidates - // for archiving. - const docThreshold = 100000 for _, dbName := range dbNames { - if dbName == "admin" || dbName == "local" || dbName == "config" { - continue - } collNames, err := client.Database(dbName).ListCollectionNames(ctx, bson.D{}) if err != nil { continue @@ -209,17 +138,12 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI if err != nil { continue } - if count >= docThreshold { - candidates = append(candidates, Candidate{ - DatabaseName: dbName, - CollectionName: collName, - DateField: "createdAt", - DateFormat: "DATE", - RetentionDays: 90, - PartitionFields: []string{"createdAt"}, - }) - } + stats = append(stats, CollectionStat{ + DatabaseName: dbName, + CollectionName: collName, + EstimatedCount: count, + }) } } - return candidates + 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..dd95b7b --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/archive/configure.go @@ -0,0 +1,75 @@ +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 validates the candidate, sets up partition fields, and creates the archive schedule. +// 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 { + + if err := ValidateCandidate(candidate, opts); err != nil { + return errors.FormatError("validate archive candidate", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + + // 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 9114ae0..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 @@ -12,6 +12,9 @@ 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(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.ServiceAccountID() == "" || secrets.ServiceAccountSecret() == "" { return nil, &errors.ValidationError{Message: "secrets cannot be 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 15e1005..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,19 +1,20 @@ package config import ( - "fmt" "os" "strings" "atlas-sdk-go/internal/errors" ) -const defaultConfigDir = "configs" +const defaultConfigPath = "configs/config.json" // Default path if not specified in environment -// LoadAll loads both secrets and configuration from the specified paths. +// 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 = fmt.Sprintf("%s/config.json", defaultConfigDir) // Default path if not specified in environment + configPath = defaultConfigPath } if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { @@ -30,3 +31,10 @@ func LoadAll(configPath string) (Secrets, Config, error) { } return secrets, cfg, 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/loadenv.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadsecrets.go similarity index 100% rename from generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadenv.go rename to generated-usage-examples/go/atlas-sdk-go/project-copy/internal/config/loadsecrets.go 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/.gitignore b/usage-examples/go/atlas-sdk-go/.gitignore index f65e948..ac48429 100644 --- a/usage-examples/go/atlas-sdk-go/.gitignore +++ b/usage-examples/go/atlas-sdk-go/.gitignore @@ -1,12 +1,10 @@ # Secrets (keep example) -tmp/.env +.env !.env.example -tmp/.env.development .env.production -tmp/.env.test -# config files (keep example) -configs +# Configs (keep example) +configs/config.json !configs/config.example.json # temporary files diff --git a/usage-examples/go/atlas-sdk-go/README.md b/usage-examples/go/atlas-sdk-go/README.md index 6703e06..96b9817 100644 --- a/usage-examples/go/atlas-sdk-go/README.md +++ b/usage-examples/go/atlas-sdk-go/README.md @@ -64,7 +64,7 @@ and improvements to existing code. 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" + 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. diff --git a/usage-examples/go/atlas-sdk-go/configs/config.development.json b/usage-examples/go/atlas-sdk-go/configs/config.development.json deleted file mode 100644 index ab61f2f..0000000 --- a/usage-examples/go/atlas-sdk-go/configs/config.development.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", - "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", - "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", - "ATLAS_PROJECT_NAME": "Customer Portal - Dev", - "ATLAS_PROCESS_ID": "CustomerPortalDev-shard-00-00.ajlj3.mongodb.net:27017", -} diff --git a/usage-examples/go/atlas-sdk-go/configs/config.production.json b/usage-examples/go/atlas-sdk-go/configs/config.production.json deleted file mode 100644 index 59f9033..0000000 --- a/usage-examples/go/atlas-sdk-go/configs/config.production.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "MONGODB_ATLAS_BASE_URL": "https://cloud-dev.mongodb.com", - "ATLAS_ORG_ID": "32b6e34b3d91647abb20e7b8", - "ATLAS_PROJECT_ID": "5e2211c17a3e5a48f5497de3", - "ATLAS_PROJECT_NAME": "Customer Portal - Prod", - "ATLAS_PROCESS_ID": "CustomerPortalProd-shard-00-00.ajlj3.mongodb.net:27017", -} 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 8724de7..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 @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/auth" @@ -26,8 +25,7 @@ func main() { if err := godotenv.Load(envFile); err != nil { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_PATH") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 bd944f1..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 @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "github.com/joho/godotenv" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -26,8 +25,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 ee122b6..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 @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/billing" @@ -24,8 +23,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 71c2e52..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 @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -25,8 +24,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 1f9182d..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 @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -25,8 +24,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 4700377..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 @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "log" - "os" "atlas-sdk-go/internal/auth" "atlas-sdk-go/internal/config" @@ -26,8 +25,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } 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 index 40c1e91..1c770d7 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "time" "atlas-sdk-go/internal/archive" @@ -24,8 +23,7 @@ func main() { log.Printf("Warning: could not load %s file: %v", envFile, err) } - configPath := os.Getenv("CONFIG_FILE") - secrets, cfg, err := config.LoadAll(configPath) + secrets, cfg, err := config.LoadAllFromEnv() if err != nil { log.Fatalf("Failed to load configuration %v", err) } @@ -54,25 +52,62 @@ func main() { // 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 specific criteria. - // NOTE: The actual implementation of this function would involve more complex logic - // to determine which collections are eligible for archiving. - candidates := archive.CollectionsForArchiving(ctx, client, projectID, clusterName) + // Find collections suitable for archiving based on demo criteria. + // This simplified example first selects all collections with counts, and then filtering 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) + configureErr := archive.ConfigureOnlineArchive(ctx, client, projectID, clusterName, candidate, opts) if configureErr != nil { fmt.Printf(" Failed to configure archive: %v\n", configureErr) failedArchives++ @@ -84,8 +119,11 @@ func main() { } } + if skippedCandidates > 0 { + fmt.Printf("\nINFO: Skipped %d of %d candidates due to validation errors\n", skippedCandidates, totalCandidates) + } if failedArchives > 0 { - fmt.Printf("\nWARNING: %d of %d archive configurations failed\n", failedArchives, totalCandidates) + 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/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go index 6832fec..d01c43d 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "atlas-sdk-go/internal/errors" - "atlas-sdk-go/internal/clusters" "go.mongodb.org/atlas-sdk/v20250219001/admin" @@ -37,17 +35,16 @@ type Options struct { ArchiveSchedule string } -// DefaultOptions provides defaults for archiving +// 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{ - DefaultRetentionMultiplier: 2, - MinimumRetentionDays: 30, - EnableDataExpiration: true, - ArchiveSchedule: "DAILY", - } + return Options{} } -// ValidateCandidate ensures the archiving candidate meets requirements +// 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") @@ -90,84 +87,23 @@ func ValidateCandidate(candidate Candidate, opts Options) error { return nil } -// ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. -// It validates the candidate, sets up partition fields, and creates the archive schedule. -// 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) error { - - opts := DefaultOptions() - - if err := ValidateCandidate(candidate, opts); err != nil { - return errors.FormatError("validate archive candidate", - fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), - err) - } - - // 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 +// CollectionStat represents basic statistics about a collection. +type CollectionStat struct { + DatabaseName string + CollectionName string + EstimatedCount int64 } -// CollectionsForArchiving retrieves collections from a MongoDB Atlas cluster that are candidates for archiving. -// It connects to the cluster using the official MongoDB Go Driver and lists collections based on specified criteria. -// NOTE: This is a simplified example; in a real implementation, you would analyze collections based on size, age, -// access patterns, and other factors to determine candidates for archiving. -func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectID, clusterName string) []Candidate { - candidates := make([]Candidate, 0) +// 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 candidates + return stats } ctxConn, cancel := context.WithTimeout(ctx, 2*time.Second) @@ -179,7 +115,7 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI // Connect to the cluster using the official MongoDB Go Driver client, err := mongo.Connect(ctxConn, clientOpts) if err != nil { - return candidates + return stats } defer func() { _ = client.Disconnect(context.Background()) }() @@ -187,17 +123,10 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI dbNames, err := client.ListDatabaseNames(ctx, bson.D{}) if err != nil { - return candidates + return stats } - // Set the simple demo criteria for archiving collections, skipping internal databases. - // NOTE: For this example, we assume collections with more than 100,000 documents are candidates - // for archiving. - const docThreshold = 100000 for _, dbName := range dbNames { - if dbName == "admin" || dbName == "local" || dbName == "config" { - continue - } collNames, err := client.Database(dbName).ListCollectionNames(ctx, bson.D{}) if err != nil { continue @@ -209,17 +138,12 @@ func CollectionsForArchiving(ctx context.Context, sdk *admin.APIClient, projectI if err != nil { continue } - if count >= docThreshold { - candidates = append(candidates, Candidate{ - DatabaseName: dbName, - CollectionName: collName, - DateField: "createdAt", - DateFormat: "DATE", - RetentionDays: 90, - PartitionFields: []string{"createdAt"}, - }) - } + stats = append(stats, CollectionStat{ + DatabaseName: dbName, + CollectionName: collName, + EstimatedCount: count, + }) } } - return candidates + 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 index d33e39f..e586e31 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/analyze_test.go @@ -7,7 +7,7 @@ import ( ) func TestValidateCandidate_ReturnsError_WhenDatabaseOrCollectionNameMissing(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "", CollectionName: "coll", @@ -29,7 +29,7 @@ func TestValidateCandidate_ReturnsError_WhenDatabaseOrCollectionNameMissing(t *t } func TestValidateCandidate_ReturnsError_WhenRetentionDaysTooLow(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "db", CollectionName: "coll", @@ -42,7 +42,7 @@ func TestValidateCandidate_ReturnsError_WhenRetentionDaysTooLow(t *testing.T) { } func TestValidateCandidate_ReturnsError_WhenNoPartitionFields(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "db", CollectionName: "coll", @@ -55,7 +55,7 @@ func TestValidateCandidate_ReturnsError_WhenNoPartitionFields(t *testing.T) { } func TestValidateCandidate_ReturnsError_WhenInvalidDateFormat(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "db", CollectionName: "coll", @@ -68,7 +68,7 @@ func TestValidateCandidate_ReturnsError_WhenInvalidDateFormat(t *testing.T) { } func TestValidateCandidate_ReturnsError_WhenDateFieldNotInPartitionFields(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "db", CollectionName: "coll", @@ -81,7 +81,7 @@ func TestValidateCandidate_ReturnsError_WhenDateFieldNotInPartitionFields(t *tes } func TestValidateCandidate_Succeeds_WithValidInput(t *testing.T) { - opts := DefaultOptions() + opts := Options{MinimumRetentionDays: 30} err := ValidateCandidate(Candidate{ DatabaseName: "db", CollectionName: "coll", @@ -95,8 +95,8 @@ func TestValidateCandidate_Succeeds_WithValidInput(t *testing.T) { func TestDefaultOptions_ReturnsExpectedDefaults(t *testing.T) { opts := DefaultOptions() - assert.Equal(t, 2, opts.DefaultRetentionMultiplier) - assert.Equal(t, 30, opts.MinimumRetentionDays) - assert.True(t, opts.EnableDataExpiration) - assert.Equal(t, "DAILY", opts.ArchiveSchedule) + 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..dd95b7b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure.go @@ -0,0 +1,75 @@ +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 validates the candidate, sets up partition fields, and creates the archive schedule. +// 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 { + + if err := ValidateCandidate(candidate, opts); err != nil { + return errors.FormatError("validate archive candidate", + fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), + err) + } + + // 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..2e2224e --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go @@ -0,0 +1,171 @@ +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_ValidationError(t *testing.T) { + // Ensure no HTTP call is made when validation fails + 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: 10, // below minimum + PartitionFields: []string{"createdAt"}, + DateField: "createdAt", + DateFormat: "DATE", + } + opts := Options{MinimumRetentionDays: 30, EnableDataExpiration: true, DefaultRetentionMultiplier: 2, ArchiveSchedule: "DAILY"} + + err := ConfigureOnlineArchive(ctx, sdk, "groupId", "cluster", candidate, opts) + assert.Error(t, err) + assert.Equal(t, int32(0), atomic.LoadInt32(&hit), "should not call API when validation fails") +} + +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/config/loadall.go b/usage-examples/go/atlas-sdk-go/internal/config/loadall.go index 15e1005..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,19 +1,20 @@ package config import ( - "fmt" "os" "strings" "atlas-sdk-go/internal/errors" ) -const defaultConfigDir = "configs" +const defaultConfigPath = "configs/config.json" // Default path if not specified in environment -// LoadAll loads both secrets and configuration from the specified paths. +// 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 = fmt.Sprintf("%s/config.json", defaultConfigDir) // Default path if not specified in environment + configPath = defaultConfigPath } if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) { @@ -30,3 +31,10 @@ func LoadAll(configPath string) (Secrets, Config, error) { } return secrets, cfg, 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/loadenv.go b/usage-examples/go/atlas-sdk-go/internal/config/loadsecrets.go similarity index 100% rename from usage-examples/go/atlas-sdk-go/internal/config/loadenv.go rename to usage-examples/go/atlas-sdk-go/internal/config/loadsecrets.go 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 fac650b..64de73c 100644 --- a/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go +++ b/usage-examples/go/atlas-sdk-go/internal/fileutils/io.go @@ -55,10 +55,7 @@ func SafeCopy(dst io.Writer, src io.Reader) error { // 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/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 From 7eed57d8a76c01a887a251b91f4be0ac119188d9 Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:01:45 -0400 Subject: [PATCH 15/19] Update usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go Co-authored-by: Dachary --- .../go/atlas-sdk-go/examples/performance/archiving/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1c770d7..1e5c41b 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -67,7 +67,7 @@ func main() { 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 filtering them. + // 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) From 5fabaa3231437c9a04f88322efd41b2bb8a3fd8b Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:05:42 -0400 Subject: [PATCH 16/19] Apply suggestion from @dacharyc Co-authored-by: Dachary --- .../go/atlas-sdk-go/examples/performance/archiving/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1e5c41b..aecfbb6 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -52,7 +52,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - skippedCandidates := 0 + invalidCandidates := 0 totalCandidates := 0 // Create archive options with custom settings From 39cc41b021e8bff1ef931613f928909fd6aa30fb Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 21 Aug 2025 14:17:50 -0400 Subject: [PATCH 17/19] Remove redundant ValidateCandidate calls --- .../internal/archive/configure.go | 11 +++----- .../internal/archive/configure_test.go | 27 ------------------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/usage-examples/go/atlas-sdk-go/internal/archive/configure.go b/usage-examples/go/atlas-sdk-go/internal/archive/configure.go index dd95b7b..4a0f38b 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/configure.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure.go @@ -10,17 +10,12 @@ import ( ) // ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. -// It validates the candidate, sets up partition fields, and creates the archive schedule. -// If data expiration is enabled, it also configures the data expiration rule based on retention days +// 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 { - if err := ValidateCandidate(candidate, opts); err != nil { - return errors.FormatError("validate archive candidate", - fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), - err) - } - // Create partition fields configuration var partitionFields []admin.PartitionField for idx, field := range candidate.PartitionFields { 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 index 2e2224e..9482c55 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go @@ -26,33 +26,6 @@ func testClient(baseURL string, t *testing.T) *admin.APIClient { return sdk } -func TestConfigureOnlineArchive_ValidationError(t *testing.T) { - // Ensure no HTTP call is made when validation fails - 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: 10, // below minimum - PartitionFields: []string{"createdAt"}, - DateField: "createdAt", - DateFormat: "DATE", - } - opts := Options{MinimumRetentionDays: 30, EnableDataExpiration: true, DefaultRetentionMultiplier: 2, ArchiveSchedule: "DAILY"} - - err := ConfigureOnlineArchive(ctx, sdk, "groupId", "cluster", candidate, opts) - assert.Error(t, err) - assert.Equal(t, int32(0), atomic.LoadInt32(&hit), "should not call API when validation fails") -} func TestConfigureOnlineArchive_SendsExpectedRequest_WhenExpirationEnabled(t *testing.T) { // Capture request and validate JSON body From 7b1e73c98d51d9577c3dca8103ce39c909f0f5e6 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 21 Aug 2025 14:23:28 -0400 Subject: [PATCH 18/19] Re-generate copied files --- .../atlas-sdk-go/main.snippet.archive-collections.go | 4 ++-- .../examples/performance/archiving/main.go | 4 ++-- .../project-copy/internal/archive/configure.go | 11 +++-------- .../atlas-sdk-go/internal/archive/configure_test.go | 1 - 4 files changed, 7 insertions(+), 13 deletions(-) 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 index 18f5a61..348a316 100644 --- 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 @@ -49,7 +49,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - skippedCandidates := 0 + invalidCandidates := 0 totalCandidates := 0 // Create archive options with custom settings @@ -64,7 +64,7 @@ func main() { 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 filtering them. + // 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) 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 index 894cde9..a3d6275 100644 --- 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 @@ -48,7 +48,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - skippedCandidates := 0 + invalidCandidates := 0 totalCandidates := 0 // Create archive options with custom settings @@ -63,7 +63,7 @@ func main() { 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 filtering them. + // 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) 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 index dd95b7b..4a0f38b 100644 --- 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 @@ -10,17 +10,12 @@ import ( ) // ConfigureOnlineArchive configures online archive for a collection in a MongoDB Atlas cluster. -// It validates the candidate, sets up partition fields, and creates the archive schedule. -// If data expiration is enabled, it also configures the data expiration rule based on retention days +// 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 { - if err := ValidateCandidate(candidate, opts); err != nil { - return errors.FormatError("validate archive candidate", - fmt.Sprintf("%s.%s", candidate.DatabaseName, candidate.CollectionName), - err) - } - // Create partition fields configuration var partitionFields []admin.PartitionField for idx, field := range candidate.PartitionFields { 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 index 9482c55..61b9211 100644 --- a/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go +++ b/usage-examples/go/atlas-sdk-go/internal/archive/configure_test.go @@ -26,7 +26,6 @@ func testClient(baseURL string, t *testing.T) *admin.APIClient { return sdk } - func TestConfigureOnlineArchive_SendsExpectedRequest_WhenExpirationEnabled(t *testing.T) { // Capture request and validate JSON body var capturedMethod string From 28416f924ab9010314dbbc33dda5a5ca721b9c35 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 21 Aug 2025 14:25:21 -0400 Subject: [PATCH 19/19] Re-generate files ... again --- .../go/atlas-sdk-go/main.snippet.archive-collections.go | 2 +- .../project-copy/examples/performance/archiving/main.go | 2 +- .../go/atlas-sdk-go/examples/performance/archiving/main.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 348a316..88467fb 100644 --- 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 @@ -49,7 +49,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - invalidCandidates := 0 + skippedCandidates := 0 totalCandidates := 0 // Create archive options with custom settings 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 index a3d6275..dd8e7be 100644 --- 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 @@ -48,7 +48,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - invalidCandidates := 0 + skippedCandidates := 0 totalCandidates := 0 // Create archive options with custom settings 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 index aecfbb6..1e5c41b 100644 --- a/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go +++ b/usage-examples/go/atlas-sdk-go/examples/performance/archiving/main.go @@ -52,7 +52,7 @@ func main() { // Connect to each cluster and analyze collections for archiving failedArchives := 0 - invalidCandidates := 0 + skippedCandidates := 0 totalCandidates := 0 // Create archive options with custom settings