diff --git a/config.json b/config.json index be899e6..5783618 100644 --- a/config.json +++ b/config.json @@ -1,11 +1,4 @@ [ - { - "notes": "Copy atlas-sdk-go project files to user-facing Atlas Architecture Center artifact repo root", - "source_directory" : "generated-usage-examples/go/atlas-sdk-go/project-copy/", - "target_repo" : "atlas-architecture-go-sdk", - "target_branch" : "main", - "target_directory" : "." - }, { "notes": "Example config for copying files from a directory to another repo", "source_directory" : "generated-examples", diff --git a/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-linked-orgs.go b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-linked-orgs.go new file mode 100644 index 0000000..990e0dd --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/main.snippet.get-linked-orgs.go @@ -0,0 +1,58 @@ +// See entire project at https://github.com/mongodb/atlas-architecture-go-sdk +package main + +import ( + "context" + "fmt" + "log" + + "github.com/joho/godotenv" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/billing" + "atlas-sdk-go/internal/config" +) + +func main() { + _ = godotenv.Load() + + secrets, cfg, err := config.LoadAll("configs/config.json") + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + sdk, err := auth.NewClient(cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize client: %v", err) + } + + ctx := context.Background() + params := &admin.ListInvoicesApiParams{ + OrgId: cfg.OrgID, + } + + fmt.Printf("Fetching cross-org billing info for organization: %s\n", params.OrgId) + results, err := billing.GetCrossOrgBilling(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve invoices: %v", err) + } + if len(results) == 0 { + fmt.Println("No invoices found for the billing organization") + return + } + + linkedOrgs, err := billing.GetLinkedOrgs(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve linked organizations: %v", err) + } + if len(linkedOrgs) == 0 { + fmt.Println("No linked organizations found for the billing org") + return + } + fmt.Println("Linked organizations:") + for i, org := range linkedOrgs { + fmt.Printf(" %d. %v\n", i+1, org) + } +} + 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 new file mode 100644 index 0000000..a1f8da0 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/.env.example @@ -0,0 +1,2 @@ +MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id +MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret 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 new file mode 100644 index 0000000..1675a9b --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +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/LICENSE.md b/generated-usage-examples/go/atlas-sdk-go/project-copy/LICENSE.md new file mode 100644 index 0000000..c319da3 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. 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 ce2bbb0..65d80bc 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 @@ -1,24 +1,126 @@ -# MongoDB Atlas Architecture Center Go SDK Code Examples +# Atlas Go SDK Examples for the MongoDB Atlas Architecture Center -This repository contains [Atlas Go SDK](https://www.mongodb.com/docs/atlas/sdk/) -code examples that follow recommendations in MongoDB's official -[Atlas Architecture Center documentation](https://www.mongodb.com/docs/atlas/architecture/current/). -You can run, download, and modify these code examples as starting points for -configuring your MongoDB Atlas architecture for your use case. +This repository contains runnable examples for the +[Atlas Go SDK](https://www.mongodb.com/docs/atlas/sdk/) +that align with best practices from the MongoDB +[Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/). -## License +Use these examples as starting points for your own Atlas integration. + +## Features + +Currently, the repository includes examples that demonstrate the following: + +- Authenticate with service accounts +- Return cluster and database metrics +- Download logs for a specific host +- Return all linked organizations from a specific billing organization +- Programmatically manage Atlas resources + +As the Architecture Center documentation evolves, this repository will be updated with new examples +and improvements to existing code. + +## Project Structure + +```text +. +├── cmd # Runnable examples by category +│ ├── get_linked_orgs/main.go +│ ├── get_logs/main.go +│ ├── get_metrics_disk/main.go +│ └── get_metrics_process/main.go +├── configs # Atlas configuration template +│ └── config.json +├── internal # Shared utilities and helpers +│ ├── auth/ +│ ├── billing/ +│ ├── config/ +│ ├── logs/ +│ ├── metrics/ +│ └── utils.go +├── go.mod +├── go.sum +├── CHANGELOG.md # List of major changes to the project +├── .gitignore # Ignores .env file and log output +└── .env.example # Example environment variables +``` + +## Prerequisites + +- Go 1.16 or later +- A MongoDB Atlas project and cluster +- Service account credentials with appropriate permissions. See + [Service Account Overview](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/). + +## Setting Environment Variables + +1. Create a `.env` file in the root directory with your MongoDB Atlas service account credentials: + ```dotenv + MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_service_account_id + MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_service_account_secret + ``` + > **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`: + ```json + { + "MONGODB_ATLAS_BASE_URL": "", + "ATLAS_ORG_ID": "", + "ATLAS_PROJECT_ID": "", + "ATLAS_CLUSTER_NAME": "", + "ATLAS_PROCESS_ID": "" + } + ``` + > **NOTE:** The base URL defaults to `https://cloud.mongodb.com` if not specified. -This project is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). +## Running Examples -## Issues +Examples in this project are intended to be run as individual scripts. +You can also adjust them to suit your needs: -To report an issue with any of these code examples, please leave feedback -through the corresponding documentation page in the -[MongoDB Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/). -Using the `Rate This Page` button, you can add a comment about the issue after -leaving a star rating. +- Modify time ranges +- Add filtering parameters +- Change output formats -## Contributing +### Billing +#### Get All Linked Organizations +```bash +go run cmd/get_linked_orgs/main.go +``` + +### Logs +Logs output to `./logs` as `.gz` and `.txt`. + +#### Fetch All Host Logs +```bash +go run cmd/get_logs/main.go +``` + +### Metrics +Metrics print to the console. + +#### Get Disk Measurements +```bash +go run cmd/get_metrics_disk/main.go +``` + +#### Get Cluster Metrics +```bash +go run cmd/get_metrics_process/main.go +``` + +## Changelog + +For list of major changes to this project, see [CHANGELOG](CHANGELOG.md). + +## Reporting Issues + +Use the "Rate this page" widget on the +[Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/) +docs to leave feedback or file issues. + +## License -We are not currently accepting public contributions to this repository at this -time. \ No newline at end of file +This project is licensed under Apache 2.0. See [LICENSE](LICENSE.md). diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/cmd/get_linked_orgs/main.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/cmd/get_linked_orgs/main.go new file mode 100644 index 0000000..f93ae38 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/cmd/get_linked_orgs/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/joho/godotenv" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal/auth" + "atlas-sdk-go/internal/billing" + "atlas-sdk-go/internal/config" +) + +func main() { + _ = godotenv.Load() + + secrets, cfg, err := config.LoadAll("configs/config.json") + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + sdk, err := auth.NewClient(cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize client: %v", err) + } + + ctx := context.Background() + params := &admin.ListInvoicesApiParams{ + OrgId: cfg.OrgID, + } + + fmt.Printf("Fetching cross-org billing info for organization: %s\n", params.OrgId) + results, err := billing.GetCrossOrgBilling(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve invoices: %v", err) + } + if len(results) == 0 { + fmt.Println("No invoices found for the billing organization") + return + } + + linkedOrgs, err := billing.GetLinkedOrgs(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve linked organizations: %v", err) + } + if len(linkedOrgs) == 0 { + fmt.Println("No linked organizations found for the billing org") + return + } + fmt.Println("Linked organizations:") + for i, org := range linkedOrgs { + fmt.Printf(" %d. %v\n", i+1, org) + } +} + 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 29ef1b0..6b94c9f 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,18 +1,14 @@ module atlas-sdk-go go 1.24 - require ( github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.10.0 go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 ) 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 - github.com/stretchr/objx v0.5.2 // indirect golang.org/x/oauth2 v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/crossorg.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/crossorg.go new file mode 100644 index 0000000..e68edbd --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/crossorg.go @@ -0,0 +1,43 @@ +package billing + +import ( + "context" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal" +) + +// GetCrossOrgBilling returns all invoices for the billing organization and any linked organizations. +// NOTE: Organization Billing Admin or Organization Owner role required to view linked invoices. +func GetCrossOrgBilling(ctx context.Context, sdk admin.InvoicesApi, p *admin.ListInvoicesApiParams) (map[string][]admin.BillingInvoiceMetadata, error) { + req := sdk.ListInvoices(ctx, p.OrgId) + + r, _, err := req.Execute() + + if err != nil { + return nil, internal.FormatAPIError("list invoices", p.OrgId, err) + } + + crossOrgBilling := make(map[string][]admin.BillingInvoiceMetadata) + if r == nil || !r.HasResults() || len(r.GetResults()) == 0 { + return crossOrgBilling, nil + } + + crossOrgBilling[p.OrgId] = r.GetResults() + for _, invoice := range r.GetResults() { + if !invoice.HasLinkedInvoices() || len(invoice.GetLinkedInvoices()) == 0 { + continue + } + + for _, linkedInvoice := range invoice.GetLinkedInvoices() { + orgID := linkedInvoice.GetOrgId() + if orgID == "" { + continue + } + crossOrgBilling[orgID] = append(crossOrgBilling[orgID], linkedInvoice) + } + } + + return crossOrgBilling, nil +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/linkedorgs.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/linkedorgs.go new file mode 100644 index 0000000..7d35a83 --- /dev/null +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/billing/linkedorgs.go @@ -0,0 +1,25 @@ +package billing + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// GetLinkedOrgs returns all linked organizations for a given billing organization. +func GetLinkedOrgs(ctx context.Context, sdk admin.InvoicesApi, p *admin.ListInvoicesApiParams) ([]string, error) { + invoices, err := GetCrossOrgBilling(ctx, sdk, p) + if err != nil { + return nil, fmt.Errorf("get cross-org billing: %w", err) + } + + var linkedOrgs []string + for orgID := range invoices { + if orgID != p.OrgId { + linkedOrgs = append(linkedOrgs, orgID) + } + } + + return linkedOrgs, nil +} diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/logs/fetch.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/logs/fetch.go index 48ca8bc..b525983 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/logs/fetch.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/logs/fetch.go @@ -2,25 +2,19 @@ package logs import ( "context" - "fmt" "io" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) // FetchHostLogs calls the Atlas SDK and returns the raw, compressed log stream. -func FetchHostLogs( - ctx context.Context, - sdk admin.MonitoringAndLogsApi, - p *admin.GetHostLogsApiParams, -) (io.ReadCloser, error) { +func FetchHostLogs(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *admin.GetHostLogsApiParams) (io.ReadCloser, error) { req := sdk.GetHostLogs(ctx, p.GroupId, p.HostName, p.LogName) rc, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("failed to fetch logs: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("failed to fetch logs: %w", err) + return nil, internal.FormatAPIError("fetch logs", p.HostName, err) } return rc, nil } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/disk.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/disk.go index e5b1636..2d8900c 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/disk.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/disk.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -14,12 +16,9 @@ func FetchDiskMetrics(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *ad r, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("fetch disk metrics: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("fetch disk metrics: %w", err) + return nil, internal.FormatAPIError("fetch disk metrics", p.PartitionName, err) } - if r == nil || !r.HasMeasurements() { + if r == nil || !r.HasMeasurements() || len(r.GetMeasurements()) == 0 { return nil, fmt.Errorf("no metrics for partition %q on process %q", p.PartitionName, p.ProcessId) } diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/process.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/process.go index c250f72..feda72a 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/process.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/metrics/process.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -14,12 +16,9 @@ func FetchProcessMetrics(ctx context.Context, sdk admin.MonitoringAndLogsApi, p r, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("failed to fetch process metrics: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("failed to fetch process metrics: %w", err) + return nil, internal.FormatAPIError("fetch process metrics", p.GroupId, err) } - if r == nil || !r.HasMeasurements() { + if r == nil || !r.HasMeasurements() || len(r.GetMeasurements()) == 0 { return nil, fmt.Errorf("no metrics for process %q", p.ProcessId) } return r, nil diff --git a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/utils.go b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/utils.go index f105de6..a767281 100644 --- a/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/utils.go +++ b/generated-usage-examples/go/atlas-sdk-go/project-copy/internal/utils.go @@ -1,10 +1,21 @@ package internal import ( + "fmt" "io" "log" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) +// FormatAPIError formats an error returned by the Atlas API with additional context. +func FormatAPIError(operation string, params interface{}, err error) error { + if apiErr, ok := admin.AsError(err); ok && apiErr.GetDetail() != "" { + return fmt.Errorf("%s %v: %w: %s", operation, params, err, apiErr.GetDetail()) + } + return fmt.Errorf("%s %v: %w", operation, params, err) +} + // SafeClose closes c and logs a warning on error. func SafeClose(c io.Closer) { if c != nil { diff --git a/usage-examples/go/atlas-sdk-go/.env.example b/usage-examples/go/atlas-sdk-go/.env.example new file mode 100644 index 0000000..a1f8da0 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/.env.example @@ -0,0 +1,2 @@ +MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_mdb_service_account_id +MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_mdb_service_account_secret diff --git a/usage-examples/go/atlas-sdk-go/CHANGELOG.md b/usage-examples/go/atlas-sdk-go/CHANGELOG.md new file mode 100644 index 0000000..1675a9b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +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/usage-examples/go/atlas-sdk-go/INTERNAL_README.md b/usage-examples/go/atlas-sdk-go/INTERNAL_README.md index be9cd5d..ab4d2c3 100644 --- a/usage-examples/go/atlas-sdk-go/INTERNAL_README.md +++ b/usage-examples/go/atlas-sdk-go/INTERNAL_README.md @@ -1,36 +1,34 @@ -# Atlas SDK for Go +# Atlas SDK for Go + +> NOTE: This is an internal-only file and refers to the internal project details. +> The external project details are documented in the README.md file. This project demonstrates how to script specific functionality using the Atlas SDK for Go. Code examples are used in the Atlas Architecture Center docs, and -the project is made available in a user-facing repo. +a sanitized copy of the project is available in a user-facing repo: +https://github.com/mongodb/atlas-architecture-go-sdk. ## Project Structure ```text -atlas-sdk-go/ -│── cmd/ # Self-contained, runnable scripts -│ ├── get_logs/ -│ ├── main.go -│ ├── get_metrics_disk/ -│ ├── main.go -│ ├── get_metrics_process/ -│ ├── main.go -│── config/ # Atlas configuration settings -│ ├── config.json -│── internal/ # Shared internal logic -│ ├── auth/ -| ├── client.go -│ ├── config/ -| ├── json.go -| ├── secrets.go -| ├── loader.go -│── .env # Secrets file (excluded from Git) -│── go.mod -│── go.sum -│── README.md # Internal-only README (do not copy with Copier Tool) -│── scripts/ # Internal-only Bluehawk scripts to snip and copy code examples -│ ├── bluehawk.sh +. +├── cmd/ # Self-contained, runnable examples by category +├── configs/ # Atlas details +├── internal # Shared utilities and helpers (NOTE: ANY TEST FILES ARE INTERNAL ONLY) +├── go.mod +├── CHANGELOG.md # User-facing list of major project changes +│── README.md # User-facing README for copied project +│── INTERNAL_README.md # (NOTE: INTERNAL ONLY - DON'T COPY TO ARTIFACT REPO) +│── scripts/ # (NOTE: INTERNAL ONLY) snip and copy code examples +│ └── bluehawk.sh +├── .gitignore +└── .env.example ``` +## Adding Examples +To add examples to the project: + + + ## Runnable Scripts You can run individual scripts from the terminal. For example, to run `get_logs/main.go`: ```shell @@ -41,50 +39,55 @@ go run cmd/get_logs/main.go ### Prerequisites -- A [service account](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/#std-label-service-accounts-overview) with access to your Atlas project +Contact the Developer Docs team with any setup questions or issues. + +- A [service account](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/#std-label-service-accounts-overview) with access to your Atlas project. > **NOTE:** Some scripts require an M10+ cluster ### Set environment variables and config file 1. Set the following variable values, either as a `.env` file in the root directory or through your IDE: - ```shell - MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your-service-account-id - MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your-service-account-secret - ``` + ```dotenv + MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_service_account_id + MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_service_account_secret 2. Update the placeholders in the `configs/config.json` file with your Atlas cluster information: ```json { - "ATLAS_BASE_URL": "https://cloud.mongodb.com", "ATLAS_ORG_ID": "", "ATLAS_PROJECT_ID": "", "ATLAS_CLUSTER_NAME": "Cluster0", "ATLAS_PROCESS_ID": "cluster0-shard-00-00.ab1cd.mongodb.net:27017" - } ``` > **NOTE: Group ID == Project ID** Groups and projects are synonymous terms. Groups and projects are synonymous terms. Your group id is the same as your project id. ## Write Tests -# TODO +... TODO ## Generate Examples -This project uses Bluehawk to generate code examples from the source code. +This project uses [Bluehawk](https://mongodb-university.github.io/Bluehawk/) markup to generate code examples from +the source code. + +We generate two types of code examples, intended for different use and destination: code snippets and full copied files + +- To generate a new code snippet to use directly in the docs, use Bluehawk's [`snippet` tag](https://mongodb-university.github.io/Bluehawk/reference/tags#snippet) to mark the snippet content, then run the following script: + ```bash + ./scripts/bluehawk.sh snip + ``` + Generated snippets output to `generated-usage-examples/go/atlas-sdk-go` +- To copy an entire file for the user-facing artifact repo, run the following script: + ```bash + ./scripts/bluehawk.sh copy + ``` + Copied files output to `generated-usage-examples/go/atlas-sdk-go/project-copy` in their original directory structure. -- Usage examples for the docs. These are generated using the `bluehawk snip` - command based on the `snippet` markup in the code file. -- Full project files for the user-facing project repo. These are generated using - the `bluehawk copy` command. - -Run the bluehawk script and enter either `snip` or `copy`. The selected command -runs with the defined defaults. ```shell ./scripts/bluehawk.sh ``` -> **NOTE: "Copy" State** This project uses a state named "copy" specifically for any manipulations needed for code copied to the artifact repo. - +> **TIP: "Copy" State** This project uses a state named "copy" specifically for any manipulations needed for outputted copied files intended for the artifact repo. See `cmd/get_linked_orgs/main.go` for an example of using the "copy" state to remove lines from the outputted file. Refer to Bluehawk's [`state` tag](https://mongodb-university.github.io/Bluehawk/reference/tags#state) docs for more info. diff --git a/usage-examples/go/atlas-sdk-go/LICENSE.md b/usage-examples/go/atlas-sdk-go/LICENSE.md new file mode 100644 index 0000000..c319da3 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/usage-examples/go/atlas-sdk-go/README.md b/usage-examples/go/atlas-sdk-go/README.md index ce2bbb0..65d80bc 100644 --- a/usage-examples/go/atlas-sdk-go/README.md +++ b/usage-examples/go/atlas-sdk-go/README.md @@ -1,24 +1,126 @@ -# MongoDB Atlas Architecture Center Go SDK Code Examples +# Atlas Go SDK Examples for the MongoDB Atlas Architecture Center -This repository contains [Atlas Go SDK](https://www.mongodb.com/docs/atlas/sdk/) -code examples that follow recommendations in MongoDB's official -[Atlas Architecture Center documentation](https://www.mongodb.com/docs/atlas/architecture/current/). -You can run, download, and modify these code examples as starting points for -configuring your MongoDB Atlas architecture for your use case. +This repository contains runnable examples for the +[Atlas Go SDK](https://www.mongodb.com/docs/atlas/sdk/) +that align with best practices from the MongoDB +[Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/). -## License +Use these examples as starting points for your own Atlas integration. + +## Features + +Currently, the repository includes examples that demonstrate the following: + +- Authenticate with service accounts +- Return cluster and database metrics +- Download logs for a specific host +- Return all linked organizations from a specific billing organization +- Programmatically manage Atlas resources + +As the Architecture Center documentation evolves, this repository will be updated with new examples +and improvements to existing code. + +## Project Structure + +```text +. +├── cmd # Runnable examples by category +│ ├── get_linked_orgs/main.go +│ ├── get_logs/main.go +│ ├── get_metrics_disk/main.go +│ └── get_metrics_process/main.go +├── configs # Atlas configuration template +│ └── config.json +├── internal # Shared utilities and helpers +│ ├── auth/ +│ ├── billing/ +│ ├── config/ +│ ├── logs/ +│ ├── metrics/ +│ └── utils.go +├── go.mod +├── go.sum +├── CHANGELOG.md # List of major changes to the project +├── .gitignore # Ignores .env file and log output +└── .env.example # Example environment variables +``` + +## Prerequisites + +- Go 1.16 or later +- A MongoDB Atlas project and cluster +- Service account credentials with appropriate permissions. See + [Service Account Overview](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/). + +## Setting Environment Variables + +1. Create a `.env` file in the root directory with your MongoDB Atlas service account credentials: + ```dotenv + MONGODB_ATLAS_SERVICE_ACCOUNT_ID=your_service_account_id + MONGODB_ATLAS_SERVICE_ACCOUNT_SECRET=your_service_account_secret + ``` + > **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`: + ```json + { + "MONGODB_ATLAS_BASE_URL": "", + "ATLAS_ORG_ID": "", + "ATLAS_PROJECT_ID": "", + "ATLAS_CLUSTER_NAME": "", + "ATLAS_PROCESS_ID": "" + } + ``` + > **NOTE:** The base URL defaults to `https://cloud.mongodb.com` if not specified. -This project is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). +## Running Examples -## Issues +Examples in this project are intended to be run as individual scripts. +You can also adjust them to suit your needs: -To report an issue with any of these code examples, please leave feedback -through the corresponding documentation page in the -[MongoDB Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/). -Using the `Rate This Page` button, you can add a comment about the issue after -leaving a star rating. +- Modify time ranges +- Add filtering parameters +- Change output formats -## Contributing +### Billing +#### Get All Linked Organizations +```bash +go run cmd/get_linked_orgs/main.go +``` + +### Logs +Logs output to `./logs` as `.gz` and `.txt`. + +#### Fetch All Host Logs +```bash +go run cmd/get_logs/main.go +``` + +### Metrics +Metrics print to the console. + +#### Get Disk Measurements +```bash +go run cmd/get_metrics_disk/main.go +``` + +#### Get Cluster Metrics +```bash +go run cmd/get_metrics_process/main.go +``` + +## Changelog + +For list of major changes to this project, see [CHANGELOG](CHANGELOG.md). + +## Reporting Issues + +Use the "Rate this page" widget on the +[Atlas Architecture Center](https://www.mongodb.com/docs/atlas/architecture/current/) +docs to leave feedback or file issues. + +## License -We are not currently accepting public contributions to this repository at this -time. \ No newline at end of file +This project is licensed under Apache 2.0. See [LICENSE](LICENSE.md). diff --git a/usage-examples/go/atlas-sdk-go/cmd/get_linked_orgs/main.go b/usage-examples/go/atlas-sdk-go/cmd/get_linked_orgs/main.go new file mode 100644 index 0000000..fe8b0f7 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/cmd/get_linked_orgs/main.go @@ -0,0 +1,62 @@ +// :snippet-start: get-linked-orgs +// :state-remove-start: copy +// See entire project at https://github.com/mongodb/atlas-architecture-go-sdk +// :state-remove-end: [copy] +package main + +import ( + "context" + "fmt" + "log" + + "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" +) + +func main() { + _ = godotenv.Load() + + secrets, cfg, err := config.LoadAll("configs/config.json") + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + sdk, err := auth.NewClient(cfg, secrets) + if err != nil { + log.Fatalf("Failed to initialize client: %v", err) + } + + ctx := context.Background() + params := &admin.ListInvoicesApiParams{ + OrgId: cfg.OrgID, + } + + fmt.Printf("Fetching cross-org billing info for organization: %s\n", params.OrgId) + results, err := billing.GetCrossOrgBilling(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve invoices: %v", err) + } + if len(results) == 0 { + fmt.Println("No invoices found for the billing organization") + return + } + + linkedOrgs, err := billing.GetLinkedOrgs(ctx, sdk.InvoicesApi, params) + if err != nil { + log.Fatalf("Failed to retrieve linked organizations: %v", err) + } + if len(linkedOrgs) == 0 { + fmt.Println("No linked organizations found for the billing org") + return + } + fmt.Println("Linked organizations:") + for i, org := range linkedOrgs { + fmt.Printf(" %d. %v\n", i+1, org) + } +} + +// :snippet-end: [get-linked-orgs] diff --git a/usage-examples/go/atlas-sdk-go/cmd/get_logs/main.go b/usage-examples/go/atlas-sdk-go/cmd/get_logs/main.go index bf3f84b..a90ced0 100644 --- a/usage-examples/go/atlas-sdk-go/cmd/get_logs/main.go +++ b/usage-examples/go/atlas-sdk-go/cmd/get_logs/main.go @@ -62,7 +62,7 @@ func main() { } fmt.Println("Uncompressed log to", txtPath) // :remove-start: - // NOTE Internal function to clean up any downloaded files + // NOTE: Internal-only function to clean up any downloaded files if err := internal.SafeDelete(outDir); err != nil { log.Printf("Cleanup error: %v", err) } diff --git a/usage-examples/go/atlas-sdk-go/go.mod b/usage-examples/go/atlas-sdk-go/go.mod index 29ef1b0..fd4200e 100644 --- a/usage-examples/go/atlas-sdk-go/go.mod +++ b/usage-examples/go/atlas-sdk-go/go.mod @@ -1,18 +1,21 @@ 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 +// :remove-end: require ( github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.10.0 // :remove: go.mongodb.org/atlas-sdk/v20250219001 v20250219001.1.0 ) 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 - github.com/stretchr/objx v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect // :remove: + github.com/stretchr/objx v0.5.2 // 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/billing/crossorg.go b/usage-examples/go/atlas-sdk-go/internal/billing/crossorg.go new file mode 100644 index 0000000..e68edbd --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/billing/crossorg.go @@ -0,0 +1,43 @@ +package billing + +import ( + "context" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" + + "atlas-sdk-go/internal" +) + +// GetCrossOrgBilling returns all invoices for the billing organization and any linked organizations. +// NOTE: Organization Billing Admin or Organization Owner role required to view linked invoices. +func GetCrossOrgBilling(ctx context.Context, sdk admin.InvoicesApi, p *admin.ListInvoicesApiParams) (map[string][]admin.BillingInvoiceMetadata, error) { + req := sdk.ListInvoices(ctx, p.OrgId) + + r, _, err := req.Execute() + + if err != nil { + return nil, internal.FormatAPIError("list invoices", p.OrgId, err) + } + + crossOrgBilling := make(map[string][]admin.BillingInvoiceMetadata) + if r == nil || !r.HasResults() || len(r.GetResults()) == 0 { + return crossOrgBilling, nil + } + + crossOrgBilling[p.OrgId] = r.GetResults() + for _, invoice := range r.GetResults() { + if !invoice.HasLinkedInvoices() || len(invoice.GetLinkedInvoices()) == 0 { + continue + } + + for _, linkedInvoice := range invoice.GetLinkedInvoices() { + orgID := linkedInvoice.GetOrgId() + if orgID == "" { + continue + } + crossOrgBilling[orgID] = append(crossOrgBilling[orgID], linkedInvoice) + } + } + + return crossOrgBilling, nil +} diff --git a/usage-examples/go/atlas-sdk-go/internal/billing/crossorg_test.go b/usage-examples/go/atlas-sdk-go/internal/billing/crossorg_test.go new file mode 100644 index 0000000..240c63b --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/billing/crossorg_test.go @@ -0,0 +1,144 @@ +package billing_test + +import ( + "context" + "errors" + "testing" + + "atlas-sdk-go/internal/billing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" +) + +func TestGetCrossOrgBilling_Success(t *testing.T) { + billingOrgID := "billingOrg123" + linkedOrgID1 := "linkedOrg456" + linkedOrgID2 := "linkedOrg789" + invoiceID1 := "inv_abc" + invoiceID2 := "inv_def" + + // Create mock response for billing org with two linked invoices + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID1, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: &linkedOrgID1, AmountBilledCents: admin.PtrInt64(1000)}, + }, + }, + { + Id: &invoiceID2, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: &linkedOrgID1, AmountBilledCents: admin.PtrInt64(2000)}, + {OrgId: &linkedOrgID2, AmountBilledCents: admin.PtrInt64(500)}, + }, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + results, err := billing.GetCrossOrgBilling(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Len(t, results, 3, "Should return billing org and two linked orgs") + assert.Len(t, results[linkedOrgID1], 2, "Should return two invoices for linkedOrgID1") + assert.Equal(t, int64(1000), *results[linkedOrgID1][0].AmountBilledCents, "First invoice for linkedOrgID1 should have 1000 cents billed") + assert.Equal(t, int64(2000), *results[linkedOrgID1][1].AmountBilledCents, "Second invoice for linkedOrgID1 should have 2000 cents billed") + assert.Len(t, results[linkedOrgID2], 1, "Should return one invoice for linkedOrgID2") + assert.Equal(t, int64(500), *results[linkedOrgID2][0].AmountBilledCents, "Invoice for linkedOrgID2 should have 500 cents billed") +} + +func TestGetCrossOrgBilling_ApiError(t *testing.T) { + billingOrgID := "billingOrgErr" + expectedError := errors.New("API error") + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(nil, nil, expectedError).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + _, err := billing.GetCrossOrgBilling(context.Background(), mockSvc, params) + + require.Error(t, err) + assert.Contains(t, err.Error(), expectedError.Error(), "Should return API error") +} + +func TestGetCrossOrgBilling_NoLinkedInvoices(t *testing.T) { + billingOrgID := "billingOrgEmpty" + invoiceID := "inv_empty" + + // Create mock response with no linked invoices + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{}, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + results, err := billing.GetCrossOrgBilling(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Len(t, results, 1, "Should only include the billing org invoice") +} + +func TestGetCrossOrgBilling_MissingOrgID(t *testing.T) { + billingOrgID := "billingOrgMissing" + linkedOrgID := "validOrgID" + invoiceID := "inv_missing_org" + + // Create mock response with one valid and one invalid linked invoice + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: nil, AmountBilledCents: admin.PtrInt64(500)}, // Invalid: missing OrgId + {OrgId: &linkedOrgID, AmountBilledCents: admin.PtrInt64(1000)}, // Valid + }, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + results, err := billing.GetCrossOrgBilling(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Len(t, results, 2, "Should return billing org and the valid linked invoice") + assert.Len(t, results[linkedOrgID], 1, "Should return one invoice for linkedOrgID") + assert.Equal(t, int64(1000), *results[linkedOrgID][0].AmountBilledCents, "Invoice for linkedOrgID should have 1000 cents billed") +} diff --git a/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs.go b/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs.go new file mode 100644 index 0000000..7d35a83 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs.go @@ -0,0 +1,25 @@ +package billing + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20250219001/admin" +) + +// GetLinkedOrgs returns all linked organizations for a given billing organization. +func GetLinkedOrgs(ctx context.Context, sdk admin.InvoicesApi, p *admin.ListInvoicesApiParams) ([]string, error) { + invoices, err := GetCrossOrgBilling(ctx, sdk, p) + if err != nil { + return nil, fmt.Errorf("get cross-org billing: %w", err) + } + + var linkedOrgs []string + for orgID := range invoices { + if orgID != p.OrgId { + linkedOrgs = append(linkedOrgs, orgID) + } + } + + return linkedOrgs, nil +} diff --git a/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs_test.go b/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs_test.go new file mode 100644 index 0000000..b6b0907 --- /dev/null +++ b/usage-examples/go/atlas-sdk-go/internal/billing/linkedorgs_test.go @@ -0,0 +1,145 @@ +package billing_test + +import ( + "context" + "errors" + "sort" + "testing" + + "atlas-sdk-go/internal/billing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250219001/admin" + "go.mongodb.org/atlas-sdk/v20250219001/mockadmin" +) + +func TestGetLinkedOrgs_Success(t *testing.T) { + billingOrgID := "billingOrgABC" + linkedOrgID1 := "linkedOrgABC" + linkedOrgID2 := "linkedOrgDEF" + invoiceID1 := "inv_123" + invoiceID2 := "inv_456" + + // Create mock response with linked invoices + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID1, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: &linkedOrgID1, AmountBilledCents: admin.PtrInt64(1000)}, + }, + }, + { + Id: &invoiceID2, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: &linkedOrgID1, AmountBilledCents: admin.PtrInt64(2000)}, + {OrgId: &linkedOrgID2, AmountBilledCents: admin.PtrInt64(500)}, + }, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + linkedOrgs, err := billing.GetLinkedOrgs(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Len(t, linkedOrgs, 2, "Should return two linked organizations") + sort.Strings(linkedOrgs) + assert.Contains(t, linkedOrgs, linkedOrgID1, "Should contain linkedOrgID1") + assert.Contains(t, linkedOrgs, linkedOrgID2, "Should contain linkedOrgID2") +} + +func TestGetLinkedOrgs_ApiError(t *testing.T) { + billingOrgID := "billingOrgErr" + expectedError := errors.New("API error") + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(nil, nil, expectedError).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + _, err := billing.GetLinkedOrgs(context.Background(), mockSvc, params) + + // Verify error handling + require.Error(t, err) + assert.Contains(t, err.Error(), "get cross-org billing") + assert.ErrorContains(t, err, expectedError.Error(), "Should return API error") +} + +func TestGetLinkedOrgs_NoLinkedOrgs(t *testing.T) { + billingOrgID := "billingOrg123" + invoiceID := "no_links" + + // Create mock response with no linked invoices + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{}, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + linkedOrgs, err := billing.GetLinkedOrgs(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Empty(t, linkedOrgs, "Should return empty when no linked orgs exist") +} + +func TestGetLinkedOrgs_MissingOrgID(t *testing.T) { + billingOrgID := "billingOrg123" + linkedOrgID := "validOrgID" + invoiceID := "inv_missing_org" + + // Create mock response with one valid and one invalid linked invoice + mockResponse := &admin.PaginatedApiInvoiceMetadata{ + Results: &[]admin.BillingInvoiceMetadata{ + { + Id: &invoiceID, + LinkedInvoices: &[]admin.BillingInvoiceMetadata{ + {OrgId: nil, AmountBilledCents: admin.PtrInt64(500)}, + {OrgId: &linkedOrgID, AmountBilledCents: admin.PtrInt64(1000)}, + }, + }, + }, + } + + mockSvc := mockadmin.NewInvoicesApi(t) + mockSvc.EXPECT(). + ListInvoices(mock.Anything, billingOrgID). + Return(admin.ListInvoicesApiRequest{ApiService: mockSvc}).Once() + mockSvc.EXPECT(). + ListInvoicesExecute(mock.Anything). + Return(mockResponse, nil, nil).Once() + + // Run test + params := &admin.ListInvoicesApiParams{OrgId: billingOrgID} + linkedOrgs, err := billing.GetLinkedOrgs(context.Background(), mockSvc, params) + + require.NoError(t, err) + assert.Len(t, linkedOrgs, 1, "Should return one linked organization") + assert.Equal(t, linkedOrgID, linkedOrgs[0], "Should return the valid linked organization ID") +} diff --git a/usage-examples/go/atlas-sdk-go/internal/logs/fetch.go b/usage-examples/go/atlas-sdk-go/internal/logs/fetch.go index 48ca8bc..b525983 100644 --- a/usage-examples/go/atlas-sdk-go/internal/logs/fetch.go +++ b/usage-examples/go/atlas-sdk-go/internal/logs/fetch.go @@ -2,25 +2,19 @@ package logs import ( "context" - "fmt" "io" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) // FetchHostLogs calls the Atlas SDK and returns the raw, compressed log stream. -func FetchHostLogs( - ctx context.Context, - sdk admin.MonitoringAndLogsApi, - p *admin.GetHostLogsApiParams, -) (io.ReadCloser, error) { +func FetchHostLogs(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *admin.GetHostLogsApiParams) (io.ReadCloser, error) { req := sdk.GetHostLogs(ctx, p.GroupId, p.HostName, p.LogName) rc, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("failed to fetch logs: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("failed to fetch logs: %w", err) + return nil, internal.FormatAPIError("fetch logs", p.HostName, err) } return rc, nil } diff --git a/usage-examples/go/atlas-sdk-go/internal/metrics/disk.go b/usage-examples/go/atlas-sdk-go/internal/metrics/disk.go index e5b1636..2d8900c 100644 --- a/usage-examples/go/atlas-sdk-go/internal/metrics/disk.go +++ b/usage-examples/go/atlas-sdk-go/internal/metrics/disk.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -14,12 +16,9 @@ func FetchDiskMetrics(ctx context.Context, sdk admin.MonitoringAndLogsApi, p *ad r, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("fetch disk metrics: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("fetch disk metrics: %w", err) + return nil, internal.FormatAPIError("fetch disk metrics", p.PartitionName, err) } - if r == nil || !r.HasMeasurements() { + if r == nil || !r.HasMeasurements() || len(r.GetMeasurements()) == 0 { return nil, fmt.Errorf("no metrics for partition %q on process %q", p.PartitionName, p.ProcessId) } diff --git a/usage-examples/go/atlas-sdk-go/internal/metrics/process.go b/usage-examples/go/atlas-sdk-go/internal/metrics/process.go index c250f72..feda72a 100644 --- a/usage-examples/go/atlas-sdk-go/internal/metrics/process.go +++ b/usage-examples/go/atlas-sdk-go/internal/metrics/process.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "atlas-sdk-go/internal" + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) @@ -14,12 +16,9 @@ func FetchProcessMetrics(ctx context.Context, sdk admin.MonitoringAndLogsApi, p r, _, err := req.Execute() if err != nil { - if apiErr, ok := admin.AsError(err); ok { - return nil, fmt.Errorf("failed to fetch process metrics: %w – %s", err, apiErr.GetDetail()) - } - return nil, fmt.Errorf("failed to fetch process metrics: %w", err) + return nil, internal.FormatAPIError("fetch process metrics", p.GroupId, err) } - if r == nil || !r.HasMeasurements() { + if r == nil || !r.HasMeasurements() || len(r.GetMeasurements()) == 0 { return nil, fmt.Errorf("no metrics for process %q", p.ProcessId) } return r, nil diff --git a/usage-examples/go/atlas-sdk-go/internal/utils.go b/usage-examples/go/atlas-sdk-go/internal/utils.go index bca14f0..99b4225 100644 --- a/usage-examples/go/atlas-sdk-go/internal/utils.go +++ b/usage-examples/go/atlas-sdk-go/internal/utils.go @@ -1,12 +1,23 @@ package internal import ( + "fmt" "io" "log" "os" // :remove: "path/filepath" // :remove: + + "go.mongodb.org/atlas-sdk/v20250219001/admin" ) +// FormatAPIError formats an error returned by the Atlas API with additional context. +func FormatAPIError(operation string, params interface{}, err error) error { + if apiErr, ok := admin.AsError(err); ok && apiErr.GetDetail() != "" { + return fmt.Errorf("%s %v: %w: %s", operation, params, err, apiErr.GetDetail()) + } + return fmt.Errorf("%s %v: %w", operation, params, err) +} + // SafeClose closes c and logs a warning on error. func SafeClose(c io.Closer) { if c != nil { @@ -28,6 +39,7 @@ func SafeCopy(dst io.Writer, src io.Reader) error { // :remove-start: // SafeDelete removes files generated in the specified directory. +// NOTE: INTERNAL ONLY FUNCTION func SafeDelete(dir string) error { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if 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 049a5ad..ad52dd3 100755 --- a/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh +++ b/usage-examples/go/atlas-sdk-go/scripts/bluehawk.sh @@ -18,21 +18,69 @@ EOF # defaults CMD="" PROJECT=$(git rev-parse --show-toplevel) -INPUT_DIR="$PROJECT/usage-examples/go/atlas-sdk-go" -OUTPUT_DIR="$PROJECT/generated-usage-examples/go/atlas-sdk-go/" +INPUT_DIR="$PROJECT/usage-examples/go/atlas-sdk-go" # source directory STATE="" IGNORE_PATTERNS=( - "internal_*.*" + "internal_*.*" # for INTERNAL_README.md "scripts/" - "*_test.go" + ".idea" + "*_test.go" # we're not including test files in artifact repo ".env" "*.gz" "*.log" - "./logs" + "./logs" # for generated logs directory + # NOTE: DO NOT add pattern for ".gitignore"; we are including it in the artifact repo ) +RENAME_PATTERNS=() +VERBOSE=false -# ─── Interactive mode ──────────────────────────────────────────────────────── -if [[ $# -eq 0 ]]; then +# Process command-line args +if [[ $# -gt 0 ]]; then + CMD="$1" + shift + + if [[ "$CMD" == "snip" ]]; then + OUTPUT_DIR="$PROJECT/generated-usage-examples/go/atlas-sdk-go/" + # No default STATE for snip + elif [[ "$CMD" == "copy" ]]; then + OUTPUT_DIR="$PROJECT/generated-usage-examples/go/atlas-sdk-go/project-copy" + # Default STATE for copy + STATE="copy" + else + usage + fi + + # Process additional flags + while [[ $# -gt 0 ]]; do + case "$1" in + --ignore=*) + IGNORE_PATTERNS+=("${1#*=}") + shift + ;; + --rename=*) + if [[ "$CMD" == "copy" ]]; then + RENAME_PATTERNS+=("${1#*=}") + else + echo "Warning: --rename is not supported for snip command, ignoring" + fi + shift + ;; + --state=*) + STATE="${1#*=}" + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac + done +else + # Interactive mode echo "=== Run Bluehawk ===" # 1) pick snip or copy @@ -42,29 +90,92 @@ if [[ $# -eq 0 ]]; then echo "enter 'snip' or 'copy'" done - STATE=$([[ "$CMD" == "snip" ]] && echo "" || echo "copy") - OUTPUT_DIR=$([[ "$CMD" == "snip" ]] && echo "$OUTPUT_DIR" || echo "$OUTPUT_DIR/project-copy") + # Ask for verbose logging + read -rp "Enable verbose output? (y/N): " verbose_response + # Convert to lowercase using tr instead of bash parameter expansion + verbose_response=$(echo "$verbose_response" | tr '[:upper:]' '[:lower:]') + if [[ "$verbose_response" == "y" ]]; then + VERBOSE=true + fi +fi - IGNORE_ARGS=() - for pattern in "${IGNORE_PATTERNS[@]}"; do - IGNORE_ARGS+=(--ignore="$pattern") +# Set up the command and its arguments +if [[ "$CMD" == "snip" ]]; then + OUTPUT_DIR="$PROJECT/generated-usage-examples/go/atlas-sdk-go/" + # No default STATE for snip if not already set +elif [[ "$CMD" == "copy" ]]; then + OUTPUT_DIR="$PROJECT/generated-usage-examples/go/atlas-sdk-go/project-copy" + # Default STATE for copy if not already set + STATE=${STATE:-"copy"} +else + usage +fi + +# Prepare ignore arguments +IGNORE_ARGS=() +for pattern in "${IGNORE_PATTERNS[@]}"; do + IGNORE_ARGS+=(--ignore="$pattern") +done + +# Prepare rename arguments (only for copy) +RENAME_ARGS=() +if [[ "$CMD" == "copy" ]] && [[ ${#RENAME_PATTERNS[@]} -gt 0 ]]; then + for rule in "${RENAME_PATTERNS[@]}"; do + RENAME_ARGS+=(--rename="$rule") done +fi -# RENAME_ARGS=() -#if [[ "$CMD" != "snip" ]]; then -# for rule in "${RENAME_PATTERNS[@]}"; do -# RENAME_ARGS+=(--rename="$rule") -# done -#else -# RENAME_ARGS=() -#fi - - # call bluehawk with all the args - bluehawk "$CMD" \ - --state="$STATE" \ - -o "$OUTPUT_DIR" \ - "${IGNORE_ARGS[@]}" \ - "$INPUT_DIR" +# Check for errors first +echo "Checking for Bluehawk parsing errors..." +if ! check_output=$(bluehawk check "${IGNORE_ARGS[@]}" "$INPUT_DIR" 2>&1); then + echo "Bluehawk check failed. Errors found:" + echo "$check_output" | grep -A 1 "bluehawk errors" + exit 1 +fi + +echo "Validation successful! No errors found." + +# Build the command - DO NOT use --quiet flag as it prevents file generation +CMD_ARGS=(bluehawk "$CMD" -o "$OUTPUT_DIR" "${IGNORE_ARGS[@]}") + +# Add state argument if set +if [[ -n "$STATE" ]]; then + CMD_ARGS+=(--state="$STATE") +fi + +# Add rename arguments for copy command +if [[ "$CMD" == "copy" ]] && [[ ${#RENAME_ARGS[@]} -gt 0 ]]; then + CMD_ARGS+=("${RENAME_ARGS[@]}") +fi + +# Add input directory +CMD_ARGS+=("$INPUT_DIR") + +# Execute the command +echo "Running: ${CMD_ARGS[0]} ${CMD_ARGS[1]} [options] ${CMD_ARGS[${#CMD_ARGS[@]}-1]}" +if [[ "$VERBOSE" == true ]]; then + "${CMD_ARGS[@]}" else - usage + output=$("${CMD_ARGS[@]}" 2>&1) + cmd_status=$? + + # Display summary + summary=$(echo "$output" | grep -A 3 "Processed [0-9]* files:") + if [[ -n "$summary" ]]; then + echo -e "\n$summary" + else + echo -e "\nNo summary available" + fi + + # Filter and display only important information + written_count=$(echo "$output" | grep -c "wrote text file" || true) + echo -e "\nSuccessfully wrote the following $written_count files:" + files_written=$(echo "$output" | grep "wrote text file" | sed 's/^wrote text file based on.*-> / /') + if [[ -n "$files_written" ]]; then + echo "$files_written" + else + echo " No files written" + fi + + exit ${cmd_status:-0} fi