From 61a540d4af05274a8e3d5e955b9fc604615f2777 Mon Sep 17 00:00:00 2001 From: Neil Martin Date: Thu, 14 May 2026 13:52:28 +0100 Subject: [PATCH] fix(platform): apply commands fail to create new resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IsNotFound checked only the ErrNotFound sentinel; SDK Resolve* methods return *APIResponseError{404} on empty search results, which never matched. blueprints apply always errored on new names. device-groups apply had the inverse problem — no IsNotFound check at all, so any error (including 500s) fell through to create. Fix: extend IsNotFound to also match *APIResponseError with status 404. Add the missing guard to device-groups apply to match the blueprint pattern. Fixes #207. Co-Authored-By: Claude Sonnet 4.6 --- .../commands/pro_platform_device_groups.go | 3 +++ internal/platform/resolve.go | 18 +++++++++++--- internal/platform/resolve_test.go | 24 +++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/internal/commands/pro_platform_device_groups.go b/internal/commands/pro_platform_device_groups.go index 9d4f0299..49abb6cb 100644 --- a/internal/commands/pro_platform_device_groups.go +++ b/internal/commands/pro_platform_device_groups.go @@ -67,6 +67,9 @@ func newPDGApplyCmd(cliCtx *registry.CLIContext) *cobra.Command { dg := devicegroups.New(cliCtx.PlatformSDKClient) id, resolveErr := dg.ResolveDeviceGroupIDByName(ctx, createReq.Name) + if resolveErr != nil && !platform.IsNotFound(resolveErr) { + return resolveErr + } if resolveErr != nil { // Not found — create result, err := devicegroups.New(cliCtx.PlatformSDKClient).CreateDeviceGroup(ctx, &createReq) diff --git a/internal/platform/resolve.go b/internal/platform/resolve.go index 40f888b4..e86c122b 100644 --- a/internal/platform/resolve.go +++ b/internal/platform/resolve.go @@ -2,7 +2,11 @@ package platform -import "errors" +import ( + "errors" + + "github.com/Jamf-Concepts/jamfplatform-go-sdk/jamfplatform" +) // ErrNotFound is returned when a resource name cannot be resolved to an ID. // @@ -20,7 +24,15 @@ import "errors" // platform.IsNotFound assertions across the codebase. var ErrNotFound = errors.New("not found") -// IsNotFound reports whether err is or wraps ErrNotFound. +// IsNotFound reports whether err is or wraps ErrNotFound, or is a 404 +// *APIResponseError from the Platform SDK (returned by Resolve* methods when +// a name lookup yields zero results). func IsNotFound(err error) bool { - return errors.Is(err, ErrNotFound) + if errors.Is(err, ErrNotFound) { + return true + } + if apiErr := jamfplatform.AsAPIError(err); apiErr != nil && apiErr.HasStatus(404) { + return true + } + return false } diff --git a/internal/platform/resolve_test.go b/internal/platform/resolve_test.go index ffc757ca..a77fbcf7 100644 --- a/internal/platform/resolve_test.go +++ b/internal/platform/resolve_test.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "testing" + + "github.com/Jamf-Concepts/jamfplatform-go-sdk/jamfplatform" ) func TestIsNotFound(t *testing.T) { @@ -14,10 +16,28 @@ func TestIsNotFound(t *testing.T) { t.Error("expected IsNotFound(ErrNotFound) = true") } - // Wrapped sentinel (as produced by ResolveBlueprintID) + // Wrapped sentinel wrapped := fmt.Errorf("blueprint %q not found: %w", "test", ErrNotFound) if !IsNotFound(wrapped) { - t.Error("expected IsNotFound on wrapped error = true") + t.Error("expected IsNotFound on wrapped sentinel = true") + } + + // SDK *APIResponseError 404 (returned by ResolveBlueprintIDByName on empty results) + api404 := &jamfplatform.APIResponseError{StatusCode: 404} + if !IsNotFound(api404) { + t.Error("expected IsNotFound(*APIResponseError{404}) = true") + } + + // SDK *APIResponseError 404 wrapped in fmt.Errorf (as the SDK wraps it) + wrapped404 := fmt.Errorf("ResolveBlueprintIDByName(Brand New Blueprint): %w", api404) + if !IsNotFound(wrapped404) { + t.Error("expected IsNotFound on wrapped *APIResponseError{404} = true") + } + + // SDK *APIResponseError non-404 must not match + api500 := &jamfplatform.APIResponseError{StatusCode: 500} + if IsNotFound(api500) { + t.Error("expected IsNotFound(*APIResponseError{500}) = false") } // Non-matching error