Skip to content

feat(classic): accept --from-file on create and update - #343

Merged
neilmartin83 merged 2 commits into
mainfrom
feat/classic-from-file
Aug 27, 2026
Merged

feat(classic): accept --from-file on create and update#343
neilmartin83 merged 2 commits into
mainfrom
feat/classic-from-file

Conversation

@neilmartin83

Copy link
Copy Markdown
Member

Problem

Classic create and update read their XML body from stdin only. That makes them unusable from AutoPkg's JamfCLIRunner processor, which supplies a body one of two ways:

  • from_file / body_file → a flag (--from-file), which classic create/update did not accept → cobra exits 2, unknown flag.
  • data → a dict json.dumps'd to stdin. That path reaches classic create, but the body is JSON while internal/client/client.go stamps Content-Type: application/xml on every /JSSResource write, so the server refuses it.

Resources with no name-resolution collection get no apply (which has always taken --from-file), so for those — classic-computer-invitations, classic-smtp-server, and friends — there was no file-based route at all.

Change

create and update now take --from-file <path>, falling back to stdin when the flag is absent. One helper does the reading:

func readClassicBody(fromFile string) ([]byte, error)

It deliberately tolerates an absent body and returns nil, nil, leaving each caller to decide whether that is fatal — the file-field flags (--mobileconfig-file, --script-file, --custom-payload-file) can supply a body on their own, so the config-profile and script creates must still accept an empty one. Where a body genuinely is required, the error now names both routes instead of only stdin.

The body can carry credentials — an SMTP or LDAP account password — which is why this is a file path and not a flag value, per the credential policy at the top of CLAUDE.md.

Template-side bookkeeping that came with it:

  • needsBytes widened to every create/update, since both now send bytes.NewReader over the body they read.
  • needsOS narrowed to the delete and apply paths — the only ones still touching os directly, now that body reading lives in the registry. Without this, classic-smtp-server and classic-gsx-connection (get+update singletons, no delete) failed to compile with "os" imported and not used.

Usage

jamf-cli pro classic-computer-invitations create --from-file invitation.xml
jamf-cli pro classic-smtp-server update 1 --from-file smtp.xml

Stdin still works exactly as before; --from-file wins when both are present, matching apply.

Tests

internal/commands/pro/generated/classic_from_file_test.goreadClassicBody unit coverage (file read, file-wins-over-stdin, unreadable file names the flag, absent body is not an error) plus four end-to-end runs through the real RunE against a recording client:

  • classic-computer-invitations create --from-file POSTs the file verbatim (the resource with no apply, i.e. the case that was unreachable).
  • create with neither body nor flag errors and sends nothing, with --from-file named in the message.
  • classic-smtp-server update 1 --from-file PUTs the file verbatim.
  • classic-macos-config-profiles create --from-file — the other create shape, where a file field is injected into the supplied XML.

Every one of these parks data on stdin without closing the writer, so a regression that wrongly reads stdin blocks and times out rather than passing quietly.

generator/classic/generator_test.go gains TestGenerate_CreateAndUpdateTakeFromFile, asserting the template emits exactly two body --from-file registrations (create and update — delete's --from-file is a list of IDs, not a body) and that the stdin-only error text is gone.

make test, make lint, make verify-generated all clean.

🤖 Generated with Claude Code

Classic create and update read their XML body from --from-file, falling
back to stdin when the flag is absent. Both routes go through one new
helper, readClassicBody, which tolerates an absent body so the file-field
flags (--mobileconfig-file, --script-file, --custom-payload-file) can
still supply one on their own.

Stdin alone left these commands unreachable from the callers that most
want them. AutoPkg's JamfCLIRunner supplies a body either as --from-file
or as a `data` dict it serialises to JSON; a JSON body on a /JSSResource
path is sent with Content-Type: application/xml and refused. So a classic
resource with no name-resolution collection — classic-computer-invitations
and its kin, which get no `apply` — had no file-based route at all.

The body can carry credentials (an SMTP or LDAP account password), which
is why this is a file path and not a flag value.

needsBytes now covers every create/update, since both send
bytes.NewReader over the body they read; needsOS narrows to the delete
and apply paths, which are all that still reach os directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@neilmartin83
neilmartin83 merged commit 1a368f8 into main Aug 27, 2026
1 check passed
@neilmartin83
neilmartin83 deleted the feat/classic-from-file branch August 27, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants