Help us shape the future of Go! Take the 2026 Go Developer Survey

jcdsupload

package
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package jcdsupload uploads a file to the Jamf Cloud Distribution Service by the route the service itself issues: POST /pro/v1/jcds/files hands back temporary AWS credentials and a bucket path, and the file goes to S3 directly. The gateway and CloudFront are not in the data path, so the 64 KiB per-stream HTTP/2 window that halves a gateway upload's throughput does not apply; a 2 GiB file went through in 3m15s.

It is handwritten and lives outside jamfplatform/ on purpose: that tree is generated and pruned. It is also the only part of the SDK that depends on the AWS SDK, so a consumer that never imports it never compiles it.

What Upload does and does not do

Upload moves bytes. It does not create the package record, refresh the inventory or wait for Jamf Pro to register the file; those are the caller's, in whatever order suits it. The sequence that works is create the record, Upload, then refresh and poll until the record reports READY — with the refresh being POST /jcds/refresh-inventory, or the cloud-distribution-point equivalent.

Treat a 504 from the refresh as "still processing" and keep polling. The first refresh after a large upload holds the request open while Jamf Pro processes the file, roughly 24 seconds per GiB, and so does every refresh sent before processing ends. The edge answers 504 at 30 seconds whether or not the refresh finished, so an 8 GiB file sees about three minutes of 504s; processing finishes regardless, and the first refresh after it returns normally. The SDK does not retry it, because the refresh is a POST and the transport never retries a POST on a 5xx.

Bound that poll with a deadline. On a tenant whose distribution point is not JCDS the record never reaches READY (see below), and nothing Upload returns says so; a caller that finds the distribution point is not JCDS (GetCloudDistributionPointV1's cdnType) should use UploadPackageV1 instead.

Wire behaviour

Probed 2026-10-27 against two environments: one with a JCDS distribution point (US) and one with an Amazon S3 distribution point (EU). Each rule below shapes the code.

  • The initiate response carries accessKeyID, secretAccessKey, sessionToken, region, bucketName, path (always "tmp/{uuid}/", trailing slash included) and uuid. The object key is path + fileName. The credentials are valid for 3600 seconds, and expiration is Unix seconds. The bucket is regional, whatever the tenant's distribution point is set to.
  • Renewal takes no parameters and returns new credentials with region, bucketName and path empty and a zero uuid. Only the key, secret, token and expiration are usable: bucket, region and path stay those of the initiate. A renewal made before any initiate answers the same shape. Renewed credentials write to the initiate path.
  • The credentials can put and complete a multipart upload under the issued path and cannot read: HeadObject is 403. An upload therefore cannot be verified from this side, and the caller's poll of the package record is the check.
  • Objects under tmp/ are removed daily by a bucket lifecycle rule, so a file that is never registered does not accumulate. Whether that rule also clears the parts of an unfinished multipart upload is not observable with these credentials, which is why Upload aborts one.
  • The file appears in GET /jcds/files and the package record fills in only after a refresh-inventory. A 1 MiB file registered within 3 seconds; a 2 GiB file took about two minutes after the last byte landed. A caller's poll has to be patient about large files.
  • On a tenant whose distribution point is Amazon S3 rather than JCDS, the same sequence uploads and registers the file in the JCDS inventory but the package record never links to it (no hashes, no status after a minute of refreshes). The route is a JCDS route.
  • The whole route is deprecated upstream (2025-08-28) and may be removed. The generated methods carry that marker; this package is the one place in the SDK that depends on them.

Proxies and the transport

The S3 traffic is not carried by jamfplatform's transport, so WithHeaders, the scope header, the rate-limit throttle and the tuned HTTP transport do not apply to it. The default HTTP client honours HTTPS_PROXY; WithHTTPClient replaces it for a caller that needs more.

Index

Constants

View Source
const (
	// DefaultPartSize is the multipart part size when the upload's size does not demand a larger
	// one. It matches the AWS transfer manager's own default.
	DefaultPartSize int64 = 8 << 20

	// MinPartSize is S3's floor for every part but the last.
	MinPartSize int64 = 5 << 20

	// DefaultConcurrency is the number of parts in flight at once.
	DefaultConcurrency = 5

	// DefaultRenewMargin is how long before the credentials expire that Upload replaces them.
	// The credentials live an hour, so five minutes is wide enough to cover a stalled part.
	DefaultRenewMargin = 5 * time.Minute
)

Variables

This section is empty.

Functions

func Upload

func Upload(ctx context.Context, api API, fileName string, body io.Reader, opts ...Option) error

Upload sends body to the Jamf Cloud Distribution Service as fileName. The name is what the package record's fileName must be, and what the inventory lists.

body is read once, from its current position. When its length is knowable (an *os.File, an io.Seeker, a *bytes.Buffer, or a reader wrapped with jamfplatform.SizedReader) the part size is chosen so the upload fits S3's 10,000-part limit; otherwise the default part size caps it at about 80 GiB. A length that is declared rather than measured (SizedReader, a *bytes.Buffer) is enforced: a body that ends early or runs past it fails the upload rather than completing an object of the wrong size.

A failed multipart upload is aborted, on a context of its own so that a cancelled or expired ctx does not stop the abort from being sent. The abort can itself fail, and then its error is part of the one returned.

The route is deprecated upstream (see the package documentation). Upload carries no Deprecated marker: no successor uploads to the bucket directly, and a marker would point every caller at the gateway route this package exists to avoid.

Upload initiates one upload per call, so a caller that retries after a failure calls it again and gets a new path.

Types

type API

type API interface {
	// InitiateJCDSUploadV1 returns temporary credentials and the bucket path for one upload.
	InitiateJCDSUploadV1(ctx context.Context) (*pro.Credentials, error)
	// RenewJCDSCredentialsV1 returns replacement credentials. Its region, bucket and path are
	// empty, so the upload keeps those of the initiate.
	RenewJCDSCredentialsV1(ctx context.Context) (*pro.Credentials, error)
}

API is the part of *pro.Client that Upload needs. It is an interface so a test can supply a fake, and so the deprecated methods are named in one place.

type Option

type Option func(*config)

Option configures Upload.

func WithConcurrency

func WithConcurrency(n int) Option

WithConcurrency sets how many parts are uploaded at once. Memory use is about concurrency times the part size when the body is not seekable.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient sets the HTTP client for the S3 requests. The default honours HTTPS_PROXY.

func WithPartSize

func WithPartSize(n int64) Option

WithPartSize sets the multipart part size in bytes. It must be at least MinPartSize. A size too small to keep the upload within S3's 10,000 parts is raised to the smallest that fits when the body's length is known.

func WithRenewMargin

func WithRenewMargin(d time.Duration) Option

WithRenewMargin sets how long before the credentials expire that they are replaced through RenewJCDSCredentialsV1. Zero waits for the expiry itself. A margin as long as the credentials' lifetime (an hour) would renew before every request, so a renewal that is already inside the margin fails the upload instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL