Documentation
¶
Index ¶
- Variables
- func BuildRSQLExpression(clauses []RSQLClause) string
- func CacheKey(baseURL, clientID string) string
- func FormatArgument(value string) string
- func ListAllCursorPages[T any](ctx context.Context, pageSize int, ...) ([]T, error)
- func ListAllPages[T any](ctx context.Context, pageSize int, ...) ([]T, error)
- func PollUntil(ctx context.Context, interval time.Duration, ...) error
- func UnwrapResults[T any](ctx context.Context, t *Transport, method, endpoint, resultsField string) ([]T, error)
- type APIResponseError
- type AmbiguousMatchError
- type ApiError
- type Error
- type FileCookieJar
- type FileTokenCache
- type Logger
- type MultipartField
- type Option
- func WithAuthorizationHeaderName(name string) Option
- func WithCookieJar(jar http.CookieJar) Option
- func WithEnvironmentID(id string) Option
- func WithHTTPClient(httpClient *http.Client) Option
- func WithHeaders(h http.Header) Option
- func WithMinRequestInterval(d time.Duration) Option
- func WithRetryPolicy(waitMin, waitMax time.Duration, maxRetries int) Option
- func WithTenantID(id string) Option
- func WithTokenCache(cache TokenCache, cacheKey string) Option
- type PaginatedResponseRepresentation
- type RSQLClause
- type RequestOptions
- type ScopeKind
- type TokenCache
- type Transport
- func (c *Transport) APIPrefix(namespace, version string) string
- func (c *Transport) AccessToken(ctx context.Context) (*oauth2.Token, error)
- func (c *Transport) BaseURL() string
- func (c *Transport) Do(ctx context.Context, method, path string, body, result any) error
- func (c *Transport) DoExpect(ctx context.Context, method, path string, body any, expectedStatus int, ...) error
- func (c *Transport) DoMultipart(ctx context.Context, method, path string, fields []MultipartField, ...) error
- func (c *Transport) DoWithContentType(ctx context.Context, method, path string, body any, contentType string, ...) error
- func (c *Transport) DoWithContentTypeNoRetry(ctx context.Context, method, path string, body any, contentType string, ...) error
- func (c *Transport) DoWithOptions(ctx context.Context, method, path string, body any, opts RequestOptions, ...) error
- func (c *Transport) HTTPClient() *http.Client
- func (t *Transport) ResolveByNameClient(ctx context.Context, ...) (string, json.RawMessage, error)
- func (t *Transport) ResolveByNameClientPaged(ctx context.Context, ...) (string, json.RawMessage, error)
- func (t *Transport) ResolveByNameFiltered(ctx context.Context, ...) (string, json.RawMessage, error)
- func (c *Transport) Scope() (ScopeKind, string)
- func (c *Transport) SetHTTPClient(httpClient *http.Client)
- func (c *Transport) SetLogger(logger Logger)
- func (c *Transport) SetUserAgent(ua string)
- func (c *Transport) TenantID() string
- func (c *Transport) ValidateCredentials(ctx context.Context) error
Constants ¶
This section is empty.
Variables ¶
var ErrUnexpectedResponse = errors.New("jamfplatform: unexpected non-JSON response")
ErrUnexpectedResponse indicates the server returned a non-JSON body where a JSON response was expected — typically an HTML error page from an edge proxy or WAF — and is distinct from a genuine JSON syntax error from the API.
The one sentinel the SDK exposes, deliberately. Everything else is *APIResponseError, because a status code and structured details already say what a caller needs. This case is different: the condition is inferred from the body shape rather than reported by Jamf, and acting on it means doing something extra — terraform-provider-jamfprotect looks up the host's public egress IP and prints a support block. That is a branch, so it needs a matchable error and not a string to grep.
Named to match jamfprotect-go-sdk. The Protect provider's resources move into terraform-provider-jamfplatform once Protect has Platform API support, and a shared error surface is one less thing to rewrite when they do.
Functions ¶
func BuildRSQLExpression ¶
func BuildRSQLExpression(clauses []RSQLClause) string
BuildRSQLExpression concatenates filter clauses into an RSQL query string.
func FormatArgument ¶
FormatArgument prepares an RSQL argument value, adding quotes/escapes when needed.
func ListAllCursorPages ¶
func ListAllCursorPages[T any](ctx context.Context, pageSize int, fetchPage func(ctx context.Context, cursor string, pageSize int) ([]T, string, error)) ([]T, error)
ListAllCursorPages fetches all pages from a cursor-paginated REST endpoint, requesting pageSize items per page. fetchPage receives the cursor for the page to fetch — empty on the first call — and returns that page's items plus the cursor for the next page, empty when the page just returned was the last.
Unlike ListAllPages this cannot skip data when the server clamps pageSize: the next page's position comes from the server's own cursor rather than from an offset the client computes, so a clamped page size costs extra round trips and nothing else. That is the whole reason cursor endpoints get their own walker instead of being forced into one of the offset styles — doing that would reintroduce exactly the silent-truncation risk documented above.
An empty page is not a termination condition. Cursor endpoints may return one legitimately — a page whose every row was filtered out server-side still carries a cursor to the rows beyond it — so only an absent cursor ends the walk. That leaves a misbehaving server able to hand back the same cursor forever, which would hang the caller with no error, so a repeated cursor is treated as a protocol failure rather than trusted.
func ListAllPages ¶
func ListAllPages[T any](ctx context.Context, pageSize int, fetchPage func(ctx context.Context, page, pageSize int) ([]T, bool, error)) ([]T, error)
ListAllPages fetches all pages from a paginated REST endpoint, requesting pageSize items per page. Callers must pass a pageSize the endpoint is known to honor: some Jamf endpoints silently clamp an oversized page-size to a lower server-enforced maximum instead of rejecting it, and this function has no way to detect that — it assumes the server returned exactly pageSize items on every non-final page when computing the next page's offset. Requesting more than the true maximum causes those endpoints to silently skip the untransferred tail of one page on the next request. See ListGroupsV1/V2 in jamfplatform/pro for a verified example (server caps at 2000 regardless of the requested page-size).
func PollUntil ¶
func PollUntil(ctx context.Context, interval time.Duration, checker func(context.Context) (bool, error)) error
PollUntil repeatedly invokes checker until it reports completion or returns an error. Between attempts the function waits for the provided interval while respecting context cancellation.
func UnwrapResults ¶
func UnwrapResults[T any](ctx context.Context, t *Transport, method, endpoint, resultsField string) ([]T, error)
UnwrapResults performs the request and returns the element slice of an unpaginated list response, accepting either shape such an endpoint sends:
{"totalCount": 2, "results": [...]} the envelope the specs declare
[...] a bare JSON array
Both have been served in production by the same operation. The four account list endpoints answered with a bare array until 2026-09-01 and with the envelope after, and a method decoding into one shape fails on the other with `json: cannot unmarshal …` on every call — a break that no generated httptest handler can see, because the stub serves whatever the SDK assumed. Deciding on the byte the server actually sent costs one branch and makes the generated methods indifferent to which shape arrives, in either direction.
resultsField names the envelope key; empty means "results".
Types ¶
type APIResponseError ¶
type APIResponseError struct {
StatusCode int
Method string
URL string
Body string
TraceID string
Errors []Error
}
APIResponseError represents an unexpected HTTP status returned by the Jamf Platform API. Implements error; consumers access structured details via Details/FieldErrors/Summary.
func AsAPIError ¶
func AsAPIError(err error) *APIResponseError
AsAPIError unwraps err and returns the underlying *APIResponseError if present, otherwise nil. Consumers use this instead of calling errors.As directly so they don't need to import the concrete error type or manage the target pointer themselves.
func (*APIResponseError) Details ¶
func (e *APIResponseError) Details() []Error
Details returns the structured error details parsed from the API response body. Returns nil when the response had no structured error body (e.g. a 5xx with an HTML or empty body).
func (*APIResponseError) Error ¶
func (e *APIResponseError) Error() string
Error formats the API response error as a human-readable string. Kept verbose on purpose — this is the fallback when a consumer has not plugged in structured handling via Details/FieldErrors/Summary.
func (*APIResponseError) FieldErrors ¶
func (e *APIResponseError) FieldErrors() map[string][]string
FieldErrors buckets structured error details by their Field property. Details with no associated field are bucketed under the empty-string key. Returns an empty map when no structured details are present, so callers can range over the result unconditionally.
func (*APIResponseError) HasStatus ¶
func (e *APIResponseError) HasStatus(code int) bool
HasStatus reports whether the error carries the given HTTP status code.
func (*APIResponseError) Summary ¶
func (e *APIResponseError) Summary() string
Summary returns a concise single-line description of the error suitable for CLI output, log lines, or generic diagnostic messages. Format prefers parsed details when present and falls back to HTTP status text otherwise.
type AmbiguousMatchError ¶
type AmbiguousMatchError struct {
Name string
Matches []string // IDs of the colliding resources, in the order returned by the API
}
AmbiguousMatchError indicates a name-based lookup matched more than one resource. Consumers inspect Matches to surface disambiguation options.
func (*AmbiguousMatchError) Error ¶
func (e *AmbiguousMatchError) Error() string
Error satisfies the error interface.
type ApiError ¶
type ApiError struct {
HTTPStatus int `json:"httpStatus"`
TraceID string `json:"traceId"`
Errors []Error `json:"errors"`
}
ApiError is the on-the-wire shape of an API error response body. Not re-exported from the public jamfplatform package — consumers reach structured details via APIResponseError accessors, not via this intermediate shape.
type Error ¶
type Error struct {
ID string `json:"id,omitempty"`
Code string `json:"code"`
Field string `json:"field"`
Description string `json:"description"`
}
Error represents an individual structured error detail returned by the API. Re-exported publicly as jamfplatform.ErrorDetail.
type FileCookieJar ¶
type FileCookieJar struct {
// contains filtered or unexported fields
}
FileCookieJar is a cookie jar backed by a JSON file so cookies persist across process invocations. Layered on top of the stdlib in-memory cookiejar.Jar; every SetCookies call mutates the jar and flushes to disk.
Used primarily for CLI-style consumers that make each API call in a separate process — long-running callers don't need persistence since the in-memory jar already handles sticky-session cookies within the run.
func NewFileCookieJar ¶
func NewFileCookieJar(path string) (*FileCookieJar, error)
NewFileCookieJar opens (or creates) a persistent jar backed by path. A missing or unreadable file is treated as empty — the caller starts with a fresh jar rather than failing.
func (*FileCookieJar) Cookies ¶
func (j *FileCookieJar) Cookies(u *url.URL) []*http.Cookie
Cookies implements http.CookieJar. The inner jar is goroutine-safe on its own, but we take the same lock SetCookies holds so reads are consistent with in-flight persists (no observing a half-updated jar state).
func (*FileCookieJar) SetCookies ¶
func (j *FileCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie)
SetCookies implements http.CookieJar. After forwarding to the inner jar it rewrites the on-disk state. Errors writing to disk are swallowed — the in-memory jar is authoritative for the current process regardless.
type FileTokenCache ¶
type FileTokenCache struct {
// contains filtered or unexported fields
}
FileTokenCache persists tokens to disk as JSON files.
func NewFileTokenCache ¶
func NewFileTokenCache(dir string) *FileTokenCache
NewFileTokenCache creates a FileTokenCache that stores tokens in the given directory.
type Logger ¶
type Logger interface {
LogRequest(ctx context.Context, method, url string, body []byte)
LogResponse(ctx context.Context, statusCode int, headers http.Header, body []byte)
}
Logger is an interface for logging HTTP requests and responses.
type MultipartField ¶
type MultipartField struct {
Name string // form field name
Filename string // if non-empty, part is a file upload with this filename
Content io.Reader // file content; read to EOF
Value string // text value when Filename is empty
}
MultipartField represents one part of a multipart/form-data request body. Exactly one of Filename (file upload) or Value (text field) must be set.
For file parts, Content is consumed once and streamed directly to the network — no in-memory buffering of the whole body. If Content is a *os.File or an io.Seeker whose size can be determined, the transport precomputes an exact Content-Length header (avoiding chunked transfer encoding, which some proxies handle poorly). Otherwise the body is sent chunked.
type Option ¶
type Option func(*Transport)
Option configures a Client.
func WithAuthorizationHeaderName ¶
WithAuthorizationHeaderName moves the OAuth2 bearer credential out of the Authorization header and into the named header on every API request.
For callers behind a reverse proxy that consumes Authorization for its own credential and expects Jamf's bearer elsewhere. The relocation runs before the WithHeaders values are applied, so pairing the two — Authorization supplied by WithHeaders, the bearer relocated by this option — sends both credentials on the same request.
Only a Bearer credential is moved, so the token exchange is unaffected: x/oauth2 writes the client credential there as Basic, and relocating it would leave the token endpoint with nothing to read while appearing to have worked.
Two names are refused, both because accepting them breaks the request in a way no error message points at. "Authorization" is the header the bearer is already in, and relocating a header onto itself deletes it — RoundTrip sets the target and then deletes the source, which for one name are the same key, so every API call would go out with no credential at all and answer 401 exactly as a wrong client secret does. The scope headers are refused for the reason WithHeaders refuses them, reached the other way round: the bearer would overwrite the scope setScopeHeader stamped, and the gateway answers 403 OWNERSHIP_FORBIDDEN. Both rejections are logged.
func WithCookieJar ¶
WithCookieJar overrides the default in-memory cookie jar. Typically used to install a persistent jar (e.g. FileCookieJar) so sticky-session cookies survive across process invocations.
func WithEnvironmentID ¶
WithEnvironmentID sets the platform environment this client is scoped to. It is sent as the X-Environment-Id request header on every API call.
An environment groups a customer's tenants. A credential is minted against one scope or the other, and the header must match the credential: an environment-scoped integration sending X-Tenant-Id — or a tenant-scoped one sending X-Environment-Id — is refused with 403 OWNERSHIP_FORBIDDEN, even when both IDs belong to the same customer (wire-verified against securitycloud in prod, 2026-08-25). So this is not an alternative spelling of WithTenantID; pick the one your integration was created for.
func WithHTTPClient ¶
WithHTTPClient overrides the HTTP client used by the API client.
func WithHeaders ¶
WithHeaders sets additional HTTP headers sent on every request this client makes, including the OAuth2 token exchange. Existing values for the same header name are replaced.
Intended for callers whose traffic is fronted by a reverse proxy that requires headers of its own — a vaulted service-account credential, a routing tag — which the SDK cannot know about. Prefer this over WithHTTPClient for the purpose: supplying a client replaces the SDK's tuned *http.Transport and silently drops proxy-from-environment support, the per-phase timeouts, the connection-pool ceiling matched to Terraform's default parallelism, and the large write buffer that package upload depends on. This option layers onto that transport instead of replacing it, and composes with WithHTTPClient when a caller genuinely needs both.
The scope headers (X-Tenant-Id, X-Environment-Id) are rejected: the gateway resolves the request context from them, and an override is refused with the same 403 OWNERSHIP_FORBIDDEN a mismatched credential gives. Set the scope with WithTenantID or WithEnvironmentID. Rejections are logged rather than silently dropped.
Values replace rather than merge, which is worth knowing for one header: passing Cookie here replaces the sticky-session cookie Jamf Cloud uses to pin a client to a single app node, so a write may not be visible on the next read. It is allowed anyway, because a proxy may require a cookie of its own.
Supplying an Authorization header here moves the OAuth2 client credential from the Authorization header into the request body on the token exchange (RFC 6749 §2.3.1 permits both forms). Without that, the caller's header would overwrite the client credential and every token fetch would fail — or, under x/oauth2's auto-detection, succeed only after a wasted 401 round trip per fetch. This is what makes "proxy takes Authorization, Jamf credential rides in the body" work, which is the arrangement such proxies expect.
func WithMinRequestInterval ¶
WithMinRequestInterval sets a client-level minimum elapsed wall-clock time between the start of consecutive outbound HTTP requests. It paces all traffic through the shared transport (which Terraform fans out across parallel goroutines), giving the server breathing room and reducing 429s. A value <= 0 disables the gate. The default is 100ms.
func WithRetryPolicy ¶
WithRetryPolicy overrides the transport's automatic-retry timing for transient failures (see retry.go's isRetryableWriteStatus for what gets retried, and jamfBackoff for the curve). waitMin seeds an exponential backoff and waitMax caps it; maxRetries follows retryablehttp's own semantics, so total attempts = maxRetries+1 and 0 disables retrying entirely.
Two distinct uses, both legitimate:
- A test harness mocking a persistently-failing transient status (e.g. an always-500 GET) wants the loop to run in milliseconds rather than the production window. Pass a few milliseconds and a low maxRetries.
- An interactive caller (a CLI) wants a bound far tighter than the production default, so a transient failure surfaces promptly instead of appearing to hang.
An overall time bound is better expressed as a context deadline, which bounds the whole call including every retry and needs no policy change.
func WithTenantID ¶
WithTenantID sets the tenant this client is scoped to. It is sent as the X-Tenant-Id request header on every API call; see ScopeHeader.
Tenant is the legacy scope; prefer WithEnvironmentID for new integrations.
func WithTokenCache ¶
func WithTokenCache(cache TokenCache, cacheKey string) Option
WithTokenCache sets a persistent token cache and its lookup key.
type PaginatedResponseRepresentation ¶
type PaginatedResponseRepresentation struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalCount int64 `json:"totalCount"`
TotalPages int `json:"totalPages"`
HasNext bool `json:"hasNext"`
HasPrevious bool `json:"hasPrevious"`
}
PaginatedResponseRepresentation captures pagination metadata shared by multiple endpoints.
type RSQLClause ¶
type RSQLClause struct {
Selector string
Operator string // defaults to "==" if empty
Argument string
JoinWith string // "and" or "or", defaults to "and"
HasOpeningParenthesis bool
HasClosingParenthesis bool
}
RSQLClause represents a single RSQL filter clause.
type RequestOptions ¶
type RequestOptions struct {
// ExpectedStatus is the success status; zero means 200, which keeps a
// plain read's options literal empty rather than restating the default.
ExpectedStatus int
// ContentType overrides the Content-Type the codec would pick. Only
// consulted when the request carries a body.
ContentType string
// Headers are set on the request, replacing rather than appending.
Headers http.Header
// NoRetry opts out of the automatic 5xx retry — see
// DoWithContentTypeNoRetry for the case it exists for.
NoRetry bool
}
RequestOptions carries the per-request dimensions of a call. It exists because those dimensions are independent — expected status, Content-Type, extra headers, and whether the write may be retried — so naming a wrapper per combination doubles the surface every time one is added. The Do* methods above are the shorthands for the combinations the generated code reaches most; DoWithOptions is the general form and the only one that can carry headers.
Headers are stamped after the scope header, so a caller could in principle overwrite X-Tenant-Id / X-Environment-Id with a wrong value and get an undiagnosable 403 OWNERSHIP_FORBIDDEN. That is why tools/generate refuses to emit a scope header as a method argument (generatorReservedHeaders) — the restriction lives at the generator rather than here so a legitimate reverse-proxy caller reaching Client.Transport() is not blocked.
type ScopeKind ¶
type ScopeKind int
ScopeKind identifies which kind of Jamf scope a client is bound to. The gateway calls this the request context, and each kind travels in its own request header.
const ( // ScopeOrganization scopes requests to a Jamf Account organization. It is // the zero value, and it sends no header at all: the gateway resolves the // organization from the access token // (request-context-allowed-sources is `token` for the account // api-products, in every environment). // // The constant exists so the generated Privileges registry can *name* this // scope rather than leaving an empty slice, which would be // indistinguishable from "the spec declared nothing". Client code has no // use for it: an unset scope already means organization, so there is no // WithOrganizationID option and never will be. ScopeOrganization ScopeKind = 0 // ScopeTenant scopes requests to a single product tenant, sent as // X-Tenant-Id. This is the legacy scope: every spec still declares this // header, but Jamf intends new integrations to be environment-scoped. ScopeTenant ScopeKind = 1 // ScopeEnvironment scopes requests to a platform environment — a grouping // of tenants — sent as X-Environment-Id, set by WithEnvironmentID. This is // the scope to prefer, and as of GitOps v2082 the specs declare it: the // six Platform APIs declare it as their only scope, and jpapi, capi and // the Security Cloud specs declare it alongside tenant. Wire-verified // against blueprints, compliance-benchmarks, pro, proclassic, devices and // securitycloud. ScopeEnvironment ScopeKind = 2 )
Every value is written out rather than run off iota, and that is not a style preference. iota counts the ConstSpec's position in the block, not the constants declared before it, so naming the zero value first and then opening an `iota + 1` run underneath it numbered ScopeTenant 2 and ScopeEnvironment 3 and left 1 unreachable — the three printed `0 2 3`. Nothing observable broke, because every comparison in the SDK is symbolic and a ScopeKind is never serialised or persisted, but the block read as though ScopeTenant were 1, these constants are exported, and the next named zero value someone inserts at the top would have renumbered them again just as silently. TestScopeKindValues pins all three.
func (ScopeKind) ScopeHeader ¶
ScopeHeader returns the request header that carries this scope kind, or "" when the kind is unset.
Organization scope deliberately has no entry: the gateway resolves it from the access token alone (request-context-allowed-sources is `token` for the account api-products, in every environment), so there is no header to send.
func (ScopeKind) String ¶
String names the scope kind, for logs and diagnostics.
The zero value reports "organization", not "none". There is no unset scope in this model: absence of a scope header *is* organization scope, because the gateway resolves the organization from the access token, so a client that sends no header is organization-scoped whether or not its author meant it to be. Reporting "none" would name a fourth state that does not exist, and a caller reading it could not tell that the client was in fact addressing an organization-scoped API correctly.
The consequence worth stating: a client whose scope options never took effect also logs "organization", so this string cannot diagnose a missing WithEnvironmentID or WithTenantID. What diagnoses that is the gateway, which answers 400 REQUEST_CONTEXT_NOT_PROVIDED when a scoped API is called with no scope header. Read the refusal, not the log line.
A value outside the three kinds reports "unknown" rather than falling back to a real scope name — that is a programming error, not a scope, and naming it as one would be the same conflation this method exists to avoid.
type TokenCache ¶
type TokenCache interface {
Load(key string) (token string, expiresAt time.Time, ok bool)
Store(key string, token string, expiresAt time.Time) error
}
TokenCache persists OAuth2 tokens across process restarts.
type Transport ¶
type Transport struct {
// contains filtered or unexported fields
}
Transport represents the HTTP transport layer for the Jamf Platform API. Sub-packages in jamfplatform/ construct service clients that wrap a Transport.
func NewTransport ¶
NewTransport creates a new Jamf Platform API transport.
func NewTransportWithUserAgent ¶
func NewTransportWithUserAgent(baseURL, clientID, clientSecret, userAgent string, opts ...Option) *Transport
NewTransportWithUserAgent creates a new Jamf Platform API transport with a custom user agent string.
func (*Transport) APIPrefix ¶
APIPrefix returns the /{namespace}/{version} URL prefix for a namespace. An empty version collapses that segment, for the APIs that carry no version in the URL (proclassic, Pro preview paths).
There is NO /api segment. The GA gateway at {region}.api.jamfcloud.com mounts each namespace at the root, and answers 404 "page not found" — the unknown-namespace tell — for anything under /api. The retired {region}.apigw.jamf.com gateway required that segment; it is gone at GA (2026-09-01), so callers must set a base URL of https://{region}.api.jamfcloud.com. Wire-verified 2026-08-28 against EU with both a tenant- and an environment-scoped credential: every namespace this SDK generates returns byte-identical statuses on the new host once the segment is dropped, and tokens minted at either host work on both.
Dropped outright rather than selected per host, the same call as the scope migration below: a second code path nothing exercises is how an earlier URL-shape bug went unnoticed for weeks.
The scope is NOT in the path either. Until 2026-08-25 every Jamf URL embedded it — /api/{namespace}/{version}/tenant/{tenantID} — and the gateway's Tyk config resolved the request context from `path`. `header` was added as an allowed source in prod on that date by a gateway API-definition change enabling header context support, and the published specs dropped the path segment in GitOps build v1495 in favour of a required X-Tenant-Id header.
func (*Transport) AccessToken ¶
AccessToken returns a valid OAuth2 token from the client's credentials configuration.
func (*Transport) Do ¶
Do performs an authenticated API request and decodes the response. It expects HTTP 200 OK as the success status.
func (*Transport) DoExpect ¶
func (c *Transport) DoExpect(ctx context.Context, method, path string, body any, expectedStatus int, result any) error
DoExpect performs an authenticated API request expecting the given HTTP status.
func (*Transport) DoMultipart ¶
func (c *Transport) DoMultipart(ctx context.Context, method, path string, fields []MultipartField, expectedStatus int, result any) error
DoMultipart performs an authenticated API request with a multipart/form-data body. The body is streamed via io.Pipe — memory usage is O(buffer), not O(file). result follows the same rules as Do — either a JSON-unmarshal target or *[]byte for raw responses.
Retries a transient failure (429, 503, or 500/502/504 on an idempotent method — see isRetryableWriteStatus) up to retryMax times, with the same backoff policy as the JSON/XML transport (jamfBackoff), but ONLY when every file part's Content is an io.Seeker (rewindable): sendMultipart streams the body through an io.Pipe consumed exactly once, so a retry can only resend it by seeking each part back to the start and re-streaming. Otherwise the failure surfaces as an APIResponseError/transport error immediately and the caller is expected to re-invoke with fresh Content readers.
This is a separate, manual retry loop rather than a ride on c.httpClient's automatic one deliberately: retryablehttp makes a request body replayable by buffering it wholesale (see FromRequest/getBodyReaderAndContentLength), which for a multi-GB package upload would defeat the entire point of streaming it — see sendMultipart's use of c.uploadClient instead of c.httpClient.
func (*Transport) DoWithContentType ¶
func (c *Transport) DoWithContentType(ctx context.Context, method, path string, body any, contentType string, expectedStatus int, result any) error
DoWithContentType performs an authenticated API request with a custom Content-Type header. It expects HTTP 200 OK as the success status.
func (*Transport) DoWithContentTypeNoRetry ¶
func (c *Transport) DoWithContentTypeNoRetry(ctx context.Context, method, path string, body any, contentType string, expectedStatus int, result any) error
DoWithContentTypeNoRetry is DoWithContentType without the transport's automatic 5xx retry (see isRetryableWriteStatus). It exists for the small, enumerable set of PUT/PATCH endpoints that carry a side-channel precondition — an optimistic-lock field sourced from a GET taken before the write — where a successful-but-500ing write, if blindly retried, replays a now-stale precondition and turns into a genuine conflict the caller's own 500-specific compensation never expects. Route through this method instead of DoWithContentType for any such endpoint; see isRetryableWriteStatus's doc for the mechanism and the current callers (computer/mobile-device prestage enrollment). 429/503 retry still applies — those are gateway-level rejections that never reached the precondition check in the first place, so they carry none of this risk.
This is a workaround for an upstream response-serializer bug, not a permanent architectural split: once Jamf fixes it, this opt-out and its callers' own GET-diff compensation (e.g. isPutSerializerBug in terraform-provider-jamfplatform) should both be removed together.
func (*Transport) DoWithOptions ¶
func (c *Transport) DoWithOptions(ctx context.Context, method, path string, body any, opts RequestOptions, result any) error
DoWithOptions performs an authenticated API request with any combination of the per-request options. It is the general form of the Do* family.
func (*Transport) HTTPClient ¶
HTTPClient returns the underlying OAuth2-managed, retry-wrapped HTTP client for raw authenticated requests.
func (*Transport) ResolveByNameClient ¶
func (t *Transport) ResolveByNameClient(ctx context.Context, listPath, searchParam, resultsField, nameField, idField, name string) (string, json.RawMessage, error)
ResolveByNameClient looks up a resource by name via client-side exact matching. Use when the List endpoint supports no RSQL filter, or only a coarse search=<term> parameter (e.g. blueprints — search matches against name and description, no equality semantics). When searchParam is non-empty the request appends <searchParam>=<name> to narrow the server-side result set; matching is always re-applied client-side against nameField so full-text hits that are not exact equals are dropped.
resultsField names the envelope key holding the array of elements; empty defaults to "results". See ResolveByNameFiltered for the full envelope-handling contract.
Error semantics match ResolveByNameFiltered: not-found surfaces as *APIResponseError(404); ambiguity as *AmbiguousMatchError.
func (*Transport) ResolveByNameClientPaged ¶
func (t *Transport) ResolveByNameClientPaged(ctx context.Context, listPath, searchParam, resultsField, nameField, idField, name string) (string, json.RawMessage, error)
ResolveByNameClientPaged looks up a resource by name via client-side exact matching across all pages of a paginated list endpoint. Use when the List endpoint returns paginated results but supports no RSQL filter for server-side equality matching. Fetches pages sequentially until all results are examined, accumulating exact-match hits on nameField.
Error semantics match ResolveByNameFiltered: not-found surfaces as *APIResponseError(404); ambiguity as *AmbiguousMatchError. Early exit once two or more matches are found (ambiguity is certain).
func (*Transport) ResolveByNameFiltered ¶
func (t *Transport) ResolveByNameFiltered(ctx context.Context, listPath, resultsField, nameField, matchField, idField, name string) (string, json.RawMessage, error)
ResolveByNameFiltered looks up a resource by name via server-side RSQL equality filtering. Use when the List endpoint supports filter=<nameField>=="<value>". Returns the IDField value of the matched element and the raw JSON bytes of that element so typed wrappers can decode into concrete types without a second round-trip.
resultsField names the envelope key holding the array of elements ("results" for standard paginated responses, "benchmarks" for compliance benchmarks, etc.). Empty defaults to "results".
Not-found surfaces as *APIResponseError with StatusCode 404 — matches the shape Classic's native /name/{name} endpoints produce naturally, so consumers can check apiErr.HasStatus(404) uniformly across all three resolver modes. Multiple matches surface as *AmbiguousMatchError.
func (*Transport) Scope ¶
Scope reports which kind of scope this client carries and the ID it sends.
ScopeOrganization with an empty ID means no scope header is sent at all, which is how organization-scoped credentials work: the gateway resolves the context from the access token, so there is nothing for the client to state. It is the zero value, so it is also what a client whose scope options never took effect reports — the two are the same state on the wire and this method cannot separate them. Callers switching on the kind must handle ScopeOrganization rather than assuming a header-bearing scope is always present.
func (*Transport) SetHTTPClient ¶
SetHTTPClient sets a custom base HTTP client (useful for testing).
installHeaderTransport runs afterwards for the same reason it does in SetUserAgent: this replaces baseClient outright, so any headerTransport layered onto the previous one is gone and the caller's headers and bearer relocation would silently stop being applied.
func (*Transport) SetUserAgent ¶
SetUserAgent sets the User-Agent header value used for token and API requests.
func (*Transport) TenantID ¶
TenantID returns the tenant ID configured on the transport, or "" when this client is scoped to something other than a tenant.
This is a partial view of a three-valued property and cannot distinguish an environment-scoped client from an unscoped, organization-style one — both report "". Prefer Scope, which reports the kind alongside the ID.