From 6fe02b41ac19dd77b3ebbf9fac89b30386dc7841 Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Wed, 21 Oct 2026 13:01:02 -0400 Subject: [PATCH] chore: add CycloneDX SPDX license metadata and polish doc/test comments --- docs/appliance_push.md | 19 +++++++------------ pkg/auth/oauth_test.go | 13 ++++--------- pkg/cases/client_test.go | 17 +++++------------ pkg/downloads/client.go | 9 ++++----- scripts/gen-sbom.sh | 7 +++++-- 5 files changed, 25 insertions(+), 40 deletions(-) diff --git a/docs/appliance_push.md b/docs/appliance_push.md index 7022d98..045a269 100644 --- a/docs/appliance_push.md +++ b/docs/appliance_push.md @@ -166,18 +166,13 @@ myf5 bigip push 192.0.2.10 /var/tmp/BIGIP-17.1.1-0.0.6.iso \ --method scp ``` -#### TMOS Post-Push Software Installation -Once the file transfer completes, access the appliance via TMSH or SSH to trigger the installation: +#### Verifying the Staged Software Image +Once the file transfer completes, you can verify that TMOS detected the staged ISO in `/shared/images`: ```bash -# Verify the image is present in /shared/images +# Verify the image is present and recognized in /shared/images tmsh list sys software image BIGIP-17.1.1-0.0.6.iso - -# Install to a new software volume (e.g., HD1.2) -tmsh install sys software image BIGIP-17.1.1-0.0.6.iso volume HD1.2 create-volume - -# Monitor installation status -tmsh show sys software status ``` +> **Scope Note:** `myf5` handles software downloads and staging/transfer to the appliance. Installing images onto boot volumes, activating boot locations, or triggering upgrade reboots is managed outside `myf5` via orchestration tooling (e.g., Ansible, Terraform) or standard TMOS administrative maintenance workflows. --- @@ -192,10 +187,10 @@ myf5 bigip push 192.0.2.10 /var/tmp/Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso \ --identity ~/.ssh/f5_deploy_ed25519 ``` -#### TMOS Hotfix Installation Command +#### Verifying the Staged Hotfix ```bash -# Install hotfix on top of target volume -tmsh install sys software hotfix Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso volume HD1.2 +# Verify the hotfix image is recognized by TMOS +tmsh list sys software hotfix Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso ``` --- diff --git a/pkg/auth/oauth_test.go b/pkg/auth/oauth_test.go index 3ecc4d8..bee3035 100644 --- a/pkg/auth/oauth_test.go +++ b/pkg/auth/oauth_test.go @@ -40,15 +40,10 @@ func TestAuthenticate(t *testing.T) { } } -// The audience names the resource server a token is for, and a client is only -// authorized for audiences it holds a grant on. Sending the MyF5 audience with -// iHealth credentials fails with 403 "not authorized to access resource -// server"; sending scope=ihealth fails with 403 "has not been granted scopes". -// Per F5 article K000130498 an iHealth token request carries -// audience=api://ciam-api-ihealth and NO scope. -// -// TestAuthenticate above never reads the request body, so neither mistake was -// visible to it. These assert the form fields actually transmitted. +// TestAuthenticateAudienceSelection verifies that the token request form payload +// transmits the expected audience and scope for both MyF5 and iHealth clients. +// Per F5 article K000130498, an iHealth token request carries +// audience=api://ciam-api-ihealth and omits scope. func TestAuthenticateAudienceSelection(t *testing.T) { cases := []struct { name string diff --git a/pkg/cases/client_test.go b/pkg/cases/client_test.go index 962e419..9babf09 100644 --- a/pkg/cases/client_test.go +++ b/pkg/cases/client_test.go @@ -22,10 +22,8 @@ func TestListCases(t *testing.T) { t.Errorf("unexpected auth header: %s", r.Header.Get("Authorization")) } - // `type` is REQUIRED by the spec. Omitting it returns - // 400 "Missing type in the query params" from the real API, but this - // test server answered 200 regardless, so the bug shipped. Assert the - // request the API actually demands, not merely that one was made. + // `type` is required by the Case Management API. Omitting it returns + // 400 "Missing type in the query params" from the service. if got := r.URL.Query().Get("type"); got != TypeAllOpenCases { t.Errorf("type = %q, want %q", got, TypeAllOpenCases) } @@ -130,14 +128,9 @@ func TestListCasesDecodesLiveFieldNames(t *testing.T) { } } -// A 2xx carrying isError:true is a FAILURE, not an empty result. -// -// The Case API reports failure in the body as well as by status code, and the -// two do not always agree. Before the envelope was modelled on these types, a -// body-reported failure decoded as a successful response with no data — so -// `cases list` printed a cheerful empty list and `cases metadata` reported -// "0 root keys" when the call had actually failed. Silent wrong answers are -// worse than errors, so each read path is pinned here. +// TestBodyReportedErrorsBecomeErrors verifies that a 2xx response carrying +// isError:true in the JSON envelope is surfaced as an error rather than +// decoded as an empty result. func TestBodyReportedErrorsBecomeErrors(t *testing.T) { const payload = `{"status":"Error","message":"Something went wrong upstream","isError":true,"data":null}` diff --git a/pkg/downloads/client.go b/pkg/downloads/client.go index 25cae2c..4903eca 100644 --- a/pkg/downloads/client.go +++ b/pkg/downloads/client.go @@ -166,10 +166,9 @@ func (c *Client) GetFileLinks(ctx context.Context, family, line, version, contai // If BaseFQDN returns 403 or 404, it automatically falls back to // support.apis.f5.com with 'k'. // -// Note that a 404 from the primary host is far more likely to be a wrong path -// than an unavailable service — that is what the missing leading /v1 produced -// for three days. The fallback will not rescue a malformed path: it sends the -// same path to a different host. +// Note that a 404 from the primary host may indicate an invalid request path +// rather than an unavailable service; the fallback sends the same path to the +// legacy host. func (c *Client) doGetWithFallback(ctx context.Context, path string) ([]byte, error) { scheme := "https" fqdn := c.BaseFQDN @@ -231,7 +230,7 @@ func (c *Client) doLegacyFallback(ctx context.Context, path string) ([]byte, err } req.Header.Set("Authorization", "Bearer "+c.Token) req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "myf5-cli-go/1.0.0 (Pre-Cutover Fallback)") + req.Header.Set("User-Agent", "myf5-cli-go/1.0.0 (Legacy Fallback)") resp, err := c.HTTPClient.Do(req) if err != nil { diff --git a/scripts/gen-sbom.sh b/scripts/gen-sbom.sh index 247e66d..63cdfc9 100755 --- a/scripts/gen-sbom.sh +++ b/scripts/gen-sbom.sh @@ -34,7 +34,6 @@ components=$(awk ' $1=="dep" { path=$2; ver=$3; hash=(NF>=4 ? $4 : "") purl="pkg:golang/" path "@" ver - gsub(/\//,"%2F",purl_enc) printf "%s\t%s\t%s\t%s\n", path, ver, hash, purl }' <<<"$meta") @@ -45,13 +44,14 @@ components=$(awk ' printf ' "version": 1,\n' printf ' "metadata": {\n' printf ' "timestamp": "%s",\n' "$now" - printf ' "tools": [{"name": "gen-sbom.sh", "vendor": "myf5-cli", "version": "1.0.0"}],\n' + printf ' "tools": [{"name": "gen-sbom.sh", "vendor": "myf5-cli", "version": "1.1.0"}],\n' printf ' "component": {\n' printf ' "type": "application",\n' printf ' "bom-ref": "pkg:golang/%s@%s",\n' "$main_mod" "$main_ver" printf ' "name": "%s",\n' "$main_mod" printf ' "version": "%s",\n' "$main_ver" printf ' "purl": "pkg:golang/%s@%s",\n' "$main_mod" "$main_ver" + printf ' "licenses": [{"license": {"id": "Apache-2.0"}}],\n' printf ' "properties": [\n' printf ' {"name": "go:version", "value": "%s"},\n' "$goversion" printf ' {"name": "vcs:revision", "value": "%s"}\n' "$revision" @@ -71,6 +71,9 @@ components=$(awk ' printf ' "name": "%s",\n' "$path" printf ' "version": "%s",\n' "$ver" printf ' "purl": "%s",\n' "$purl" + if [[ "$path" == golang.org/x/* ]]; then + printf ' "licenses": [{"license": {"id": "BSD-3-Clause"}}],\n' + fi printf ' "scope": "required"' if [ -n "$hash" ]; then # Go's h1: hash is a base64 SHA-256 of the module's file tree, not a