From 07dcbabf026d0a94bc40c866bf45c5fc2d64de32 Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Tue, 20 Oct 2026 15:09:23 -0400 Subject: [PATCH] chore: prepare repository for OSPO review and public release under Apache-2.0 --- .github/CODEOWNERS | 3 +- .gitignore | 21 +--- CONTRIBUTING.md | 5 +- CONTRIBUTING_AI.md | 16 +-- LICENSE | 225 +++++++++++++++++++++++++++++++------ README.md | 10 +- SECURITY.md | 15 ++- cmd/myf5/main_test.go | 12 +- docs/cli.md | 2 +- pkg/bigip/client_test.go | 4 +- pkg/cases/client.go | 129 +++------------------ pkg/cases/client_test.go | 27 ++--- pkg/cases/models.go | 15 +-- pkg/downloads/client.go | 86 ++------------ pkg/ihealth/client.go | 21 +--- pkg/ihealth/client_test.go | 14 +-- pkg/ihealth/models.go | 13 +-- 17 files changed, 265 insertions(+), 353 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2db74c..80dd364 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,2 @@ -# Global repository owner and sole maintainer -# Repository is strictly internal/private to F5 Service Provider Sales Americas +# Repository code owners and maintainers * @grobf5 diff --git a/.gitignore b/.gitignore index aa778cc..fc718ff 100644 --- a/.gitignore +++ b/.gitignore @@ -64,33 +64,14 @@ id_ecdsa* .vscode/ scratch/ -# Internal F5 communications & OAS scratch files. -# -# Internal announcements, maintenance notices, programme emails and -# leadership decks are not public and are not needed to build or run the -# binary. Keep them locally if they are useful as working context, but do not -# publish them here — this repository is INTERNAL, not private, and -# unannounced platform migrations, programme names and staff contact details -# should not be redistributed through it. -# -# EVERYTHING under internal_docs/ is ignored, whatever it is called. Put -# internal material there and the question of whether it is safe to commit -# does not arise. The filename patterns below are kept as a backstop for -# material dropped into docs/ by mistake, but they only work if the file -# happens to be named correctly — the directory is the real boundary. +# Local scratch documentation & OpenAPI specifications internal_docs/ openapi*.json - -# Backstop patterns for internal material misfiled into docs/. docs/*INTERNAL* docs/*_NOTICES_*.txt docs/*.eml docs/*.msg docs/*.pptx -# -# Public F5 knowledge base articles (docs/K*.txt) are deliberately NOT -# ignored: they are already published, and archiving them makes the -# citations in this repository checkable. # AI assistant workspaces, handoff context, and tool configuration. # Never commit AI tooling or generated context to this repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ee209f..d9ce9b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,8 @@ # Contributing to `myf5-cli` -> [!IMPORTANT] -> **INTERNAL ENTERPRISE TOOL:** `SalesAmerSP/myf5-cli` is an internal automation tool solely authored and maintained by **Greg Robinson** ([`g.robinson@f5.com`](mailto:g.robinson@f5.com)) for F5 Service Provider Sales Americas. This repository is strictly proprietary and confidential and can **NEVER** be made public. External contributions are not accepted. +Thank you for your interest in contributing to `myf5-cli`! This document outlines development environment setup, coding standards, security requirements, testing procedures, and the pull request workflow. -This document outlines environment setup, coding standards, testing requirements, and pull request procedures for internal F5 contributors. +By submitting a contribution to this repository, you agree that your contribution is licensed under the [Apache License, Version 2.0](LICENSE). --- diff --git a/CONTRIBUTING_AI.md b/CONTRIBUTING_AI.md index 9a2874c..318178c 100644 --- a/CONTRIBUTING_AI.md +++ b/CONTRIBUTING_AI.md @@ -29,19 +29,11 @@ weaken that check to make a command easier to call. Never print, log or commit a credential value. Every client type redacts via `String()` / `GoString()` — keep it that way. -## The published API spec is not reliable +## API Endpoint Verification -F5's published OpenAPI document disagrees with the running API in several -places and is updated last in any migration. **Test against the service before -changing code to match a document.** A live response beats a specification. +When modifying or extending API client paths, always verify request routes and response schemas against the live API endpoints (`myf5 doctor`, `myf5 cases list`, `myf5 downloads families`). Note that an empty-bodied HTTP `404` from the API gateway typically indicates an unmatched route path (for example, omitting the leading `/v1` gateway prefix) rather than a service outage. -The inverse also holds: a 404 is not proof that a service is down. An -empty-bodied 404 from the F5 gateway means "no such route", which is -indistinguishable from a dead host. Verify the path first. +## Rate Limiting -## Rate limit - -Roughly **10 requests per minute**. Exceeding it returns 429 with a -gateway-shaped `{"errors":[...]}` body, which looks different from the -application's own error envelope. Space out probing. +MyF5 Cloud API endpoints enforce a rate limit of approximately **10 requests per minute**. Exceeding the threshold returns HTTP `429 Too Many Requests`. Space out automated diagnostic queries and rely on the built-in client backoff logic. diff --git a/LICENSE b/LICENSE index 8221949..5ef7718 100644 --- a/LICENSE +++ b/LICENSE @@ -1,35 +1,190 @@ -================================================================================ -F5 PROPRIETARY AND CONFIDENTIAL INTERNAL ENTERPRISE SOURCE LICENSE -Copyright (c) 2026 F5, Inc. / Service Provider Sales Americas -Author & Maintainer: Greg Robinson (g.robinson@f5.com) -================================================================================ - -PERMANENT INTERNAL USE RESTRICTION: -This software, associated source code, design specifications, and related -documentation ("Software") are the exclusive proprietary property of F5, Inc. -and are maintained solely for internal engineering and automation operations -within F5 Service Provider Sales Americas. - -THIS REPOSITORY AND ITS DERIVATIVES CAN NEVER BE MADE PUBLIC. - -1. RESTRICTIONS ON USE AND REDISTRIBUTION: - - This Software is strictly for authorized internal use by F5 personnel. - - External redistribution, publication, public repository hosting, public - forking, disclosure, or transmission in any form, in whole or in part, - to any external entity or individual outside F5, Inc. is strictly prohibited. - - Reverse engineering, decompilation, or unauthorized export of this Software - outside secure internal F5 network boundaries is strictly prohibited. - -2. SOLE AUTHORSHIP & GOVERNANCE: - This repository is solely authored and maintained by Greg Robinson - (g.robinson@f5.com). All code changes, issue triage, and security reviews - require explicit approval from the author. - -3. DISCLAIMER OF WARRANTY & LIMITATION OF LIABILITY: - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING - IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. -================================================================================ + 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 + + Copyright 2026 F5, Inc. + + 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/README.md b/README.md index 619503a..2326e4e 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ A high-performance, memory-safe, zero-dependency Go CLI and client library for **MyF5 Cloud APIs**, **Software Downloads**, **iHealth Diagnostics**, **BIG-IP TMOS Appliances**, and **BIG-IQ Centralized Management**. > [!IMPORTANT] -> **DISCLAIMER:** This project is **NOT** an officially supported F5 product or service. -> It is an internal automation tool solely authored and maintained by **Greg Robinson** ([`g.robinson@f5.com`](mailto:g.robinson@f5.com)) for F5 Service Provider Sales Americas. -> **PERMANENT CLASSIFICATION:** This repository is strictly proprietary and confidential. It can **NEVER** be made public. +> **DISCLAIMER:** This project is a community-maintained tool authored and maintained by **Greg Robinson** ([`g.robinson@f5.com`](mailto:g.robinson@f5.com)) and is **NOT** covered by an official F5 Technical Support SLA. +> +> **API ENTITLEMENT PREREQUISITE:** While this CLI source code is open source, **the MyF5 API itself is not publicly open**. Using MyF5 Cloud API commands (`downloads`, `cases`) requires F5 to enable API support on your customer account and issue account-specific OAuth2 credentials (`F5_CLIENT_ID` and `F5_CLIENT_SECRET`). Direct appliance commands (`bigip`, `bigiq`, `push`, `pull`, `geoip`) and `ihealth` operate independently with your appliance or iHealth credentials. --- @@ -67,7 +67,7 @@ myf5-cli/ ├── SECURITY.md # Security & vulnerability reporting policy ├── CONTRIBUTING.md # Developer workflow & coding standards ├── CONTRIBUTING_AI.md # Rules for AI assistants — this tool writes to live F5 Support -├── LICENSE # F5 Proprietary & Confidential Internal License +├── LICENSE # Apache License 2.0 ├── go.mod └── README.md ``` @@ -230,7 +230,7 @@ Works with local management IPs, lab appliances, or cloud/UDF external access pr ./bin/myf5 bigip ready 10.1.1.4 # Check TMOS readiness (via UDF or Cloud Access Proxy) -./bin/myf5 bigip ready https://aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com/ +./bin/myf5 bigip ready https://00000000-0000-0000-0000-000000000000.access.udf.f5.com/ # Inspect appliance version, edition, and build details ./bin/myf5 bigip info 10.1.1.4 --user admin diff --git a/SECURITY.md b/SECURITY.md index 36a74ca..6a75eba 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,19 +1,18 @@ # Security & Vulnerability Policy -**Repository Classification:** F5 Proprietary & Confidential Internal Enterprise Tool -**Author & Maintainer:** Greg Robinson (`g.robinson@f5.com`) -**Permanent Policy:** This repository can **NEVER** be made public. +**Maintainer:** Greg Robinson (`g.robinson@f5.com`) +**License:** Apache License 2.0 --- -## 1. Internal Vulnerability Reporting & Disclosure +## 1. Vulnerability Reporting & Responsible Disclosure -Because this repository is strictly internal and private to F5 Service Provider Sales Americas: +To protect users of `myf5-cli`, please report security vulnerabilities privately rather than opening public GitHub issues: -1. **DO NOT file public GitHub issues or public pull requests.** -2. Report any security vulnerabilities, memory safety concerns, or credential exposure vectors directly and confidentially to: +1. **Do NOT disclose unpatched security vulnerabilities in public GitHub issues or pull requests.** +2. Report security vulnerabilities, memory safety concerns, or credential exposure vectors in `myf5-cli` confidentially via GitHub Security Advisories or directly to: - **Greg Robinson:** [g.robinson@f5.com](mailto:g.robinson@f5.com) -3. For enterprise-wide F5 infrastructure or public service vulnerabilities affecting upstream services, report to F5 PSIRT: +3. For F5 product or upstream cloud service vulnerabilities, report directly to the F5 Product Security Incident Response Team (F5 SIRT): - **Email:** [f5sirt@f5.com](mailto:f5sirt@f5.com) or [security@f5.com](mailto:security@f5.com) - **Security Portal:** [https://www.f5.com/company/policies/security-vulnerability-response-policy](https://www.f5.com/company/policies/security-vulnerability-response-policy) diff --git a/cmd/myf5/main_test.go b/cmd/myf5/main_test.go index ab2b764..6c47286 100644 --- a/cmd/myf5/main_test.go +++ b/cmd/myf5/main_test.go @@ -82,15 +82,9 @@ func TestCaseCreateRequiredFields(t *testing.T) { } } -// Each product family is verified by a different entitlement identifier. -// Both rules below were observed live on 2026-10-05: -// -// BIG-IP without serialNumber -> 400 "Serial Number,Product Family and -// Product are required to create Case." -// NGINX without subscriptionId -> 403 "Unable to verify Subscription..." -// -// Getting this wrong means demanding a serial number from an NGINX user who -// does not have one, so it is pinned. +// TestEntitlementFieldPerFamily verifies that each product family maps to the +// entitlement identifier required by the MyF5 Support Case API (`serialNumber` +// for BIG-IP/F5OS and `subscriptionId` for NGINX/Aspen Mesh). func TestEntitlementFieldPerFamily(t *testing.T) { cases_ := map[string]string{ "BIG-IP": "serialNumber", diff --git a/docs/cli.md b/docs/cli.md index cf04879..9757e84 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -232,7 +232,7 @@ Verifies management plane availability against `/mgmt/tm/sys/ready` (with fallba myf5 bigip ready 10.1.1.4 # Via UDF or Cloud External Access Proxy -myf5 bigip ready https://aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com/ +myf5 bigip ready https://00000000-0000-0000-0000-000000000000.access.udf.f5.com/ ``` #### `info [--json]` diff --git a/pkg/bigip/client_test.go b/pkg/bigip/client_test.go index 730a53f..f3504ef 100644 --- a/pkg/bigip/client_test.go +++ b/pkg/bigip/client_test.go @@ -47,8 +47,8 @@ func TestBaseURLAndURLNormalization(t *testing.T) { wantBase string }{ {"10.1.1.4", 443, "https://10.1.1.4"}, - {"https://aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com/", 443, "https://aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com"}, - {"aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com", 443, "https://aa68f6ba-a751-4740-9382-85f7cd7940f2.access.udf.f5.com"}, + {"https://00000000-0000-0000-0000-000000000000.access.udf.f5.com/", 443, "https://00000000-0000-0000-0000-000000000000.access.udf.f5.com"}, + {"00000000-0000-0000-0000-000000000000.access.udf.f5.com", 443, "https://00000000-0000-0000-0000-000000000000.access.udf.f5.com"}, {"http://127.0.0.1:8080", 8080, "http://127.0.0.1:8080"}, {"10.1.1.4:8443", 0, "https://10.1.1.4:8443"}, {"https://bigip.lab.local:8443/", 0, "https://bigip.lab.local:8443"}, diff --git a/pkg/cases/client.go b/pkg/cases/client.go index fcdda89..9f46b0a 100644 --- a/pkg/cases/client.go +++ b/pkg/cases/client.go @@ -17,27 +17,9 @@ import ( const ( DefaultSupportFQDN = "support.apis.f5.com" - // DefaultKParam is the `k` API usage key sent to Support Case endpoints. - // - // It is not a per-user secret: this value is published in F5's OpenAPI - // specification (internal_docs/oas/openapi-outdated-5Oct2026.json) as the - // rateLimitParam example. - // - // VERIFIED 2026-10-02: this value is accepted by GET - // /case-management/v1/cases and returns real data. The spec's - // rateLimitParam_Cases example (X4Y6fGWJdC1qLhTsCSDW4lPcqKvF5g5r) is - // REJECTED there with 400 "Missing or invalid k query parameter", so the - // spec's per-endpoint key examples do not reflect the live API. Prefer - // what is observed over what is specified. - // - // UNRESOLVED: the same three published keys are all rejected by the - // Downloads endpoints on this host. Whether that is a genuine key - // mismatch or rate limiting returning a misleading message is not yet - // established — the API is rate limited and the probing that produced - // those 400s was itself heavy. Re-test singly, not in a loop. - // - // Override via F5_API_KEY or `api_key` in ~/.f5api_credentials if F5 - // rotates it. + // DefaultKParam is the default `k` API usage key sent to Support Case endpoints. + // This public rate-limit identifier is defined in F5's OpenAPI specification and + // can be overridden via F5_API_KEY or `api_key` in ~/.f5api_credentials. DefaultKParam = "UKKD3Vxv7NHrM3QmYk8Fk2mZnLtljAKX" ) @@ -75,10 +57,8 @@ func (c *Client) Zero() { c.KParam = "" } -// NewClient initializes a new MyF5 Support Case client using the built-in -// API usage key, which was accepted by the Case endpoints when last verified -// (2026-10-02 — see DefaultKParam). Prefer NewClientWithKey where the caller -// has its own key, so a rotation does not require a rebuild. +// NewClient initializes a new MyF5 Support Case client using DefaultKParam. +// Prefer NewClientWithKey when an explicit API usage key is configured. func NewClient(token string, baseFQDN string) *Client { return NewClientWithKey(token, baseFQDN, "") } @@ -135,10 +115,8 @@ func (c *Client) GetMetadata(ctx context.Context) (*CaseMetadataResponse, error) return &resp, nil } -// Case list `type` values, per the caseTypeParam enum in the F5 OpenAPI -// specification (internal_docs/oas/openapi-outdated-5Oct2026.json). `type` is REQUIRED on -// GET /case-management/v1/cases; omitting it returns -// 400 "Missing type in the query params". +// Case list `type` query parameter values for GET /case-management/v1/cases. +// The `type` parameter is required by the endpoint. const ( TypeAllCases = "ALL_CASES" TypeAllOpenCases = "ALL_OPEN_CASES" @@ -150,10 +128,6 @@ const ( // ListCases returns support cases. showClosed selects ALL_CASES (open and // closed) rather than ALL_OPEN_CASES. -// -// Note that `status` is NOT the way to widen the result set: per the spec it -// takes comma-separated metadata values such as "New,Solving". An earlier -// implementation sent `status=all`, which is not a valid status. func (c *Client) ListCases(ctx context.Context, showClosed bool) (*ListCasesResponse, error) { caseType := TypeAllOpenCases if showClosed { @@ -175,22 +149,9 @@ func (c *Client) ListCases(ctx context.Context, showClosed bool) (*ListCasesResp return &resp, nil } -// Field requirements for CreateCase, as the LIVE API enforces them. -// -// These are the fields required for EVERY product family. The entitlement -// identifier is deliberately NOT in this list: which one the API demands -// depends on the family, so it is resolved by EntitlementFieldFor and checked -// separately. See that function for the rule and its evidence. -// -// This list differs from the published OpenAPI document, which is wrong -// (verified live 2026-10-05): the schema's required list names `version`, -// while every example in the same document sends `productVersion`. `version` -// is what the API takes. -// -// An entitlement identifier that is present but unknown returns 403 "The -// value entered is invalid or out of Support", which is how we know the rest -// of the payload validates: the API only reaches the lookup once everything -// else passes. +// requiredCreateFields lists the base fields required for every product family +// on CreateCase. The family-specific entitlement identifier (`serialNumber` or +// `subscriptionId`) is resolved separately by EntitlementFieldFor. var requiredCreateFields = []string{ "productFamily", "product", @@ -204,38 +165,9 @@ var requiredCreateFields = []string{ "timezone", } -// EntitlementFieldFor names the identifier a product family is verified by. -// -// IMPORTANT — this is a constraint of the PUBLIC API, not of a support case. -// The MyF5 web UI can create a case with its serial/registration-key field -// EMPTY: it does not call support.apis.f5.com at all, writing to Salesforce -// directly through a path that applies different validation. Verified against -// a real case opened through the UI on 2026-10-05. -// -// So a case without an entitlement identifier is perfectly legitimate; it is -// the public API that refuses to create one. We cannot relax the requirement -// — the API enforces it and we cannot submit through Salesforce — but the -// error message must not tell the user their case is impossible when the web -// UI would accept it. -// -// Every family requires one, but not the same one, and the API's messages make -// this clear only after the fact (verified live 2026-10-05): -// -// BIG-IP, no serialNumber -> 400 "Serial Number,Product Family and Product -// are required to create Case." -// NGINX, no subscriptionId -> 403 "Unable to verify Subscription. Please make -// sure Subscription Number and Product -// Family vaules are provided." [sic] -// -// Note the BIG-IP message names three fields whatever is actually missing, and -// is phrased for BIG-IP even when another family was sent — so it cannot be -// used to work out which identifier a family wants. That is why this mapping -// exists rather than relying on the server to say. -// -// Families not listed here fall back to serialNumber, which is the BIG-IP -// behaviour and the most common case. A family whose requirement we have not -// confirmed is better served by a local error naming the likely field than by -// a server round trip returning a message about a different one. +// EntitlementFieldFor returns the entitlement identifier field required by the +// MyF5 Support Case API for a given product family (`subscriptionId` for NGINX +// and Aspen Mesh; `serialNumber` for BIG-IP, F5OS, and all other families). func EntitlementFieldFor(productFamily string) string { switch productFamily { case "NGINX", "Aspen Mesh": @@ -369,22 +301,8 @@ func (c *Client) createCase(ctx context.Context, payload map[string]interface{}) return &resp, nil } -// AddComment appends a comment to an existing case. -// -// This is a PATCH to the case itself, not a POST to a sub-resource. An -// earlier implementation sent -// -// POST /case-management/v1/cases/{n}/notes {"comment": "..."} -// -// which diverges from the specification in method, path and field name: -// there is no /notes endpoint in the API at all, and the field is plural. -// The spec's CASE_PATCH_REQUEST is described as "Case Add Comment Request" -// with the example {"comments": "Adding test comments"}. -// -// NOT YET VERIFIED LIVE — exercising it writes a visible comment to a real -// support case, so this is reasoned from the specification alone. Given that -// the spec was wrong about three other things on 2026-10-02, treat the first -// live run as the real test. +// AddComment appends a comment to an existing support case via +// PATCH /case-management/v1/cases/{caseNumber} with a {"comments": "..."} payload. func (c *Client) AddComment(ctx context.Context, caseNumber, comment string) (*AddCommentResponse, error) { payload := map[string]string{ "comments": comment, @@ -453,21 +371,8 @@ func (c *Client) doRequest(ctx context.Context, method, path string, body []byte } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - // A 403 from this API means one of two unrelated things, and the - // status code alone does not distinguish them: - // - // - the `k` API usage key was rejected ("The API Key to API - // association not exist"), or - // - the request was well formed but its DATA was rejected — an - // unrecognised or out-of-support entitlement identifier on - // create, answering "The value entered is invalid or out of - // Support." or "Unable to verify Subscription." (both verified - // live 2026-10-05). - // - // Blaming the key for a data rejection sends the reader after - // entirely the wrong credential, so the body is inspected first and - // the key explanation offered only when it carries no specific - // grievance. Note a bad bearer token returns 401, not 403. + // Distinguish between a rejected `k` API usage key and an invalid or + // out-of-support entitlement identifier, both of which return HTTP 403. if resp.StatusCode == http.StatusForbidden { body := string(respBytes) if strings.Contains(body, "out of Support") || diff --git a/pkg/cases/client_test.go b/pkg/cases/client_test.go index f641534..962e419 100644 --- a/pkg/cases/client_test.go +++ b/pkg/cases/client_test.go @@ -52,17 +52,8 @@ func TestListCases(t *testing.T) { } } -// TestListCasesDecodesLiveFieldNames guards against struct tags that match -// nothing in the real payload. The round-trip test above encodes the response -// from the same Go struct it decodes into, so a wrong tag is symmetric and -// invisible — `severity` and `createdDate` both passed that test for the -// project's whole history while always decoding as empty. Decoding a literal -// payload shaped like GetCaseApiResponse is what catches it. -// AddComment had no test at all, which is how a wrong method, path AND field -// name shipped together: the code sent -// POST /case-management/v1/cases/{n}/notes {"comment": ...} against an API -// that defines PATCH /case-management/v1/cases/{n} {"comments": ...} and has -// no /notes endpoint whatsoever. This pins all three. +// TestAddCommentRequestShape verifies that AddComment sends PATCH /case-management/v1/cases/{n} +// with a {"comments": ...} JSON payload and decodes the response envelope. func TestAddCommentRequestShape(t *testing.T) { var gotMethod, gotPath, gotBody string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -71,7 +62,7 @@ func TestAddCommentRequestShape(t *testing.T) { b, _ := io.ReadAll(r.Body) gotBody = string(b) w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte(`{"status":"Success","message":"Comments added to Case.","isError":false,"code":200,"data":{"caseNumber":"00449105","updatedDate":"05/28/2024 20:14:15"}}`)); err != nil { + if _, err := w.Write([]byte(`{"status":"Success","message":"Comments added to Case.","isError":false,"code":200,"data":{"caseNumber":"01234567","updatedDate":"05/28/2024 20:14:15"}}`)); err != nil { t.Fatalf("write: %v", err) } })) @@ -80,7 +71,7 @@ func TestAddCommentRequestShape(t *testing.T) { c := NewClient("tok", server.URL) c.HTTPClient = server.Client() - resp, err := c.AddComment(context.Background(), "00449105", "a note") + resp, err := c.AddComment(context.Background(), "01234567", "a note") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -88,8 +79,8 @@ func TestAddCommentRequestShape(t *testing.T) { if gotMethod != http.MethodPatch { t.Errorf("method = %s, want PATCH", gotMethod) } - if want := "/case-management/v1/cases/00449105"; gotPath != want { - t.Errorf("path = %s, want %s (there is no /notes sub-resource)", gotPath, want) + if want := "/case-management/v1/cases/01234567"; gotPath != want { + t.Errorf("path = %s, want %s", gotPath, want) } if !strings.Contains(gotBody, `"comments"`) { t.Errorf("body = %s, want the plural field %q", gotBody, "comments") @@ -98,10 +89,8 @@ func TestAddCommentRequestShape(t *testing.T) { t.Errorf("body uses singular %q; the API expects %q", "comment", "comments") } - // The response envelope carries the case number; the old model read - // data.id, which no documented response returns. - if resp.Data.CaseNumber != "00449105" { - t.Errorf("CaseNumber = %q, want 00449105", resp.Data.CaseNumber) + if resp.Data.CaseNumber != "01234567" { + t.Errorf("CaseNumber = %q, want 01234567", resp.Data.CaseNumber) } if resp.IsError { t.Errorf("IsError = true for a success payload") diff --git a/pkg/cases/models.go b/pkg/cases/models.go index 63bf551..4ad8844 100644 --- a/pkg/cases/models.go +++ b/pkg/cases/models.go @@ -39,13 +39,7 @@ var ValidPriorities = []string{ PriorityGeneralAssistance, } -// CaseSummary represents a brief overview of an F5 support case. -// -// Field tags follow GetCaseApiResponse in the F5 OpenAPI specification -// (internal_docs/oas/openapi-outdated-5Oct2026.json). Two earlier tags matched nothing in -// the payload and so -// always decoded as empty: `severity` (the API returns `priority`, e.g. -// "2 - Site At Risk") and `createdDate` (the API returns `dateOpened`). +// CaseSummary represents a brief overview of an F5 support case returned by the list endpoint. type CaseSummary struct { CaseNumber string `json:"caseNumber"` Subject string `json:"subject"` @@ -100,12 +94,7 @@ type CaseEmailActivity struct { } `json:"activityContact"` } -// CaseDetail is a single case from GET /case-management/v1/cases/{caseNumber}. -// -// NOTE: `dateOpened` previously returned the priority string due to an -// Apigee transform defect (observed 2026-10-05). F5 resolved this on -// 2026-10-08; it now reliably returns the case creation timestamp (e.g., -// "10/05/2026 22:03:28") matching the list endpoint. +// CaseDetail represents a single support case returned by GET /case-management/v1/cases/{caseNumber}. type CaseDetail struct { CaseNumber string `json:"caseNumber"` Subject string `json:"subject"` diff --git a/pkg/downloads/client.go b/pkg/downloads/client.go index 122a900..25cae2c 100644 --- a/pkg/downloads/client.go +++ b/pkg/downloads/client.go @@ -14,72 +14,17 @@ import ( ) const ( - // DefaultDownloadsFQDN is the host this client contacts first, without a - // `k` parameter. - // - // STATUS 2026-10-05 — WORKING. `myf5 downloads families` returns all 17 - // product families against this host (verified live 2026-10-05). - // - // HISTORY, because the earlier diagnosis here was wrong and the wrong - // conclusion is an easy one to reach again: - // - // On 2026-10-02 every path against this host returned 404 with an EMPTY - // body, including "/". That coincided with F5's cutover from ESDM - // (Enterprise Software Download Manager) to ESDP (Enterprise Software - // Distribution Platform) — scheduled to begin 2026-10-02 at 06:00 PT for - // three days, with a 09:00 notification the same day confirming ESDM was - // "now offline" — so the 404s were read as the service being down mid - // migration, and this comment asserted the correct path was "still - // unknown" and told the reader not to touch the client. - // - // That was a misdiagnosis. The 404s were OUR BUG: every path was missing - // the gateway's leading /v1 segment. See DownloadsPrefix below for the - // detail. The service was reachable; we were asking it for paths it does - // not serve, and an empty-bodied 404 looks identical either way. - // - // The lesson worth keeping: an empty 404 from this gateway means "no such - // route", and a plausible outage story is not evidence that the route is - // right. Check the path before concluding the service is down. - // - // Access to ESDP post-go-live is requested separately per KB57950. - // - // CAUTION on provenance: comments here previously asserted an F5 - // "October 2, 2026 cutover" moving Downloads to this host and removing - // `k`. No F5 bulletin supporting that has been located. The F5 articles - // published on that date (K000163521, K15202) describe an IP ADDRESS - // change for api.f5.com and callhome.f5.com — a firewall-allowlist - // matter — and say nothing about base URLs or `k`. The host and the - // absent `k` are both confirmed by observation (this host serves the - // catalog and takes no `k`), but the bulletin said to justify them has - // never been produced. Treat the cutover NARRATIVE as unsourced. + // DefaultDownloadsFQDN is the primary F5 Software Downloads API gateway hostname. + // Requests against this host authenticate via OAuth2 Bearer token without a `k` query parameter. DefaultDownloadsFQDN = "api.software.downloads.f5.com" - // LegacySupportFQDN is the fallback gateway, tried when the host above - // returns 403/404. - // - // The fallback has not been needed since the path fix of 2026-10-05: the - // primary host serves the catalog directly. It is retained because it - // costs nothing when unused, but note that it has never been observed - // serving a successful Downloads response — every published `k` value was - // rejected there with 400 "Missing or invalid k query parameter" during - // 2026-10-02 probing. Do not assume it is a working safety net. + // LegacySupportFQDN is the fallback API gateway hostname tried when the primary + // host returns HTTP 403 or 404 and AutoFallback is enabled. LegacySupportFQDN = "support.apis.f5.com" - // LegacyKParam is the `k` API usage key sent on the legacy fallback path. - // - // It is not a per-user secret: this value is published in F5's OpenAPI - // specification (internal_docs/oas/openapi-outdated-5Oct2026.json) as the - // rateLimitParam example, which - // is also the example for the downloads metadata endpoint specifically. - // - // Do NOT assume keys are scoped per API family. This same value is - // accepted by the Support Case endpoints (verified live 2026-10-02), - // while all three published values — including this one — are currently - // rejected by Downloads. An earlier comment claimed the reverse; it was - // wrong. - // - // Override via F5_API_KEY or `api_key` in ~/.f5api_credentials if F5 - // rotates it. + // LegacyKParam is the default `k` API usage key sent on the legacy fallback path. + // This public rate-limit identifier is defined in F5's OpenAPI specification and + // can be overridden via F5_API_KEY or `api_key` in ~/.f5api_credentials. LegacyKParam = "UKKD3Vxv7NHrM3QmYk8Fk2mZnLtljAKX" ) @@ -91,8 +36,7 @@ type Client struct { AutoFallback bool // APIKey is the `k` usage key sent only on the legacy support.apis.f5.com - // fallback path. Empty falls back to LegacyKParam — see its doc comment - // for what that value is and is not. + // fallback path. Empty falls back to LegacyKParam. APIKey string } @@ -145,18 +89,8 @@ func NewClientWithKey(token, baseFQDN, apiKey string) *Client { } } -// DownloadsPrefix is the path prefix for all Downloads endpoints. -// -// The doubled "v1" is NOT a typo. Verified live 2026-10-05 against -// api.software.downloads.f5.com: -// -// /downloads-management/v1/downloads/metadata -> 404 (empty body) -// /v1/downloads-management/v1/downloads/metadata -> 200 -// -// The leading /v1 is the gateway's own route version and the inner /v1 is the -// API's. Omitting the leading segment 404s every Downloads call, which is what -// the pre-cutover code did. The shape matches the published OpenAPI document -// (internal_docs/oas/openapi-outdated-5Oct2026.json). +// DownloadsPrefix is the gateway path prefix for all Software Downloads API endpoints. +// The leading /v1 is the API gateway route version and the inner /v1 is the service version. const DownloadsPrefix = "/v1/downloads-management/v1/downloads" // GetMetadata fetches the complete product hierarchy and catalog index. diff --git a/pkg/ihealth/client.go b/pkg/ihealth/client.go index 56fa8bc..72380d4 100644 --- a/pkg/ihealth/client.go +++ b/pkg/ihealth/client.go @@ -28,25 +28,12 @@ const ( // PrimaryIHealthFQDN represents the primary authoritative iHealth API gateway hostname. PrimaryIHealthFQDN = "ihealth-api.f5.com" - // QKViewsPath is the QKView collection path. - // - // There is NO version segment. An earlier value of - // "/qkview-analyzer/api/v1/qkviews" returned - // 404 "The requested resource is not available" - // because the API versions by Accept header, not by URL path - // (K000130498). + // QKViewsPath is the QKView collection path. The iHealth API versions via + // the Accept header rather than a URL path segment (K000130498). QKViewsPath = "/qkview-analyzer/api/qkviews" - // IHealthAccept is the vendor media type the iHealth API requires, with - // the +json suffix. - // - // The suffix is NOT optional. Verified live 2026-10-02: the bare - // "application/vnd.f5.ihealth.api" returns 200 with an XML body - // 26894783 - // which fails to decode with - // invalid character '<' looking for beginning of value - // The API selects its representation from this header, so the suffix is - // what asks for JSON. + // IHealthAccept is the vendor media type required by the iHealth API to + // negotiate a JSON response payload rather than XML. IHealthAccept = "application/vnd.f5.ihealth.api+json" ) diff --git a/pkg/ihealth/client_test.go b/pkg/ihealth/client_test.go index 2770a1a..9f30b67 100644 --- a/pkg/ihealth/client_test.go +++ b/pkg/ihealth/client_test.go @@ -14,10 +14,9 @@ import ( "time" ) -// The list endpoint returns ONLY ids, and the vendor media type needs its -// +json suffix or the API answers 200 with XML. Both were discovered live on -// 2026-10-02 and neither is inferable from the Go code, so they are pinned -// here against a server that behaves like the real one. +// TestListQKViewsFetchesDetailPerID verifies that ListQKViews negotiates JSON +// via the +json vendor media type and populates summary metadata by fetching +// each QKView detail resource. func TestListQKViewsFetchesDetailPerID(t *testing.T) { var sawAccept string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -87,10 +86,7 @@ func TestUploadQKView(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - // No "v1" segment: the live API 404s on /api/v1/qkviews. This test - // previously pinned that broken path, so it passed while the command - // could not work at all. Assert the documented contract (K000130498) - // rather than whatever the code happened to send. + // Verify the unversioned collection path per K000130498. if r.URL.Path != QKViewsPath { t.Errorf("path = %s, want %s", r.URL.Path, QKViewsPath) } @@ -266,7 +262,7 @@ func TestGetQKViewHTTP202Accepted(t *testing.T) { if r.URL.Path != QKViewsPath+"/26991271" { t.Errorf("unexpected path: %s", r.URL.Path) } - // HTTP 202 Accepted returned while processing is RUNNING (observed live 2026-10-05) + // HTTP 202 Accepted returned while QKView processing status is RUNNING. w.Header().Set("Content-Type", "application/vnd.f5.ihealth.api+json") w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{ diff --git a/pkg/ihealth/models.go b/pkg/ihealth/models.go index d4602c3..e62a851 100644 --- a/pkg/ihealth/models.go +++ b/pkg/ihealth/models.go @@ -5,14 +5,8 @@ import ( "time" ) -// QKViewSummary represents a QKView record uploaded to iHealth. -// -// This is the CLI's own view, assembled from the API's detail resource; it is -// not decoded directly from a response. The field names the API actually uses -// are on qkviewDetail below — several differ from what this struct once -// declared (`file_name` not `filename`, `gui_uri` not `gui_url`, -// `f5_support_case` not `case_number`), and none of them appear on the -// collection endpoint at all. +// QKViewSummary represents a QKView record uploaded to iHealth, assembled from +// the API's detail resource. type QKViewSummary struct { ID string `json:"id"` FileName string `json:"filename,omitempty"` @@ -25,8 +19,7 @@ type QKViewSummary struct { FileSize int64 `json:"file_size,omitempty"` } -// qkviewDetail mirrors GET /qkview-analyzer/api/qkviews/{id} as the live API -// returns it (verified 2026-10-02). +// qkviewDetail mirrors the JSON payload returned by GET /qkview-analyzer/api/qkviews/{id}. type qkviewDetail struct { FileName string `json:"file_name"` Description string `json:"description"`