Documentation
¶
Overview ¶
Package validators provides shared Terraform Plugin Framework validators for use across all resource packages.
Index ¶
- func DNSHostname() validator.String
- func DNSHostnameOrWildcard() validator.String
- func IANATimeZone() validator.String
- func IPv4Address() validator.String
- func IPv4CIDR() validator.String
- func IPv4CIDRAllowingHostBits() validator.String
- func NotEmptyString(detail string) validator.String
- func ParseIPv4CIDR(value string) (*net.IPNet, int, error)
- func PlistDocument() validator.String
- func TimeOfDayHHMMSS(allowEmpty bool) validator.String
- func UniqueStringFieldList(fieldName string) validator.List
- func UniqueStringFieldSet(fieldName string) validator.Set
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DNSHostname ¶
DNSHostname returns a validator.String enforcing DNS host name form.
Extracted at its second consumer per STYLE_GUIDE §Shared abstractions: the Jamf Security Cloud search domain and hostname mappings resources both need it.
Why it exists: the endpoints answer every malformed name with one opaque `400 [INVALID_FIELD] … Invalid field value.` carrying a null field, so nothing in the response says which attribute was wrong or what form was wanted. Only the empty and over-long cases name the field, and only on the search domain.
The accepted grammar was established by probing 20 inputs against both endpoints on 2026-08-29; every verdict matched, which is why one validator serves both:
- up to 253 characters overall, each label 1 to 63 — characters, not bytes
- a single trailing dot is allowed (the root label); no other empty label is
- labels hold letters, digits, hyphen, underscore and any non-ASCII rune — `xn--bcher-kva.example.com` and `ünïcode.example.com` are both accepted, and case is stored verbatim rather than normalised
- a label may not begin or end with a hyphen or an underscore
- the final label may not begin with a digit, which is what separates the accepted `123.foo` from the refused `foo.123`, `foo.1abc` and `1.2.3.4` — only the *final* label is held to it, so a leading numeric label is fine
- a single label is fine: `corp` is accepted
That is Guava's `InternetDomainName.isValid` without the public-suffix check, matching a Java service. Stated so a future divergence is diagnosable rather than mysterious: if the endpoints start accepting or refusing something this rejects, the suspect is a Guava version or a switch away from it, not a typo here.
Wildcards are refused by both endpoints, so they are refused here. A ZTNA app's hostnames are a different vocabulary that does take them — use DNSHostnameOrWildcard for that field.
Null and unknown values defer to the server, per STYLE_GUIDE §Config-time validators.
func DNSHostnameOrWildcard ¶
DNSHostnameOrWildcard returns a validator.String enforcing DNS host name form with wildcard prefixes allowed.
The ZTNA app `hostnames` field is a traffic-matching pattern rather than a name to resolve, so it takes two forms the DNS endpoints refuse:
- `*` on its own, which matches everything — the full-tunnel case
- a `*.` prefix on an otherwise valid name, e.g. `*.example.com`
The wildcard may only replace a whole leading label: `sub.*.example.com`, `*.*.example.com` and `*x.example.com` are all refused. Everything after the prefix is held to the grammar dnsHostnameProblem enforces, which was re-probed against this endpoint on 2026-08-30 across 12 inputs and matched the DNS endpoints' verdict on every non-wildcard one — underscores away from a label boundary, non-ASCII runes, a trailing root dot and a single bare label are all accepted, and a final all-numeric label is refused. That is why the two share a grammar function rather than each carrying their own copy.
The endpoint answers every refusal with one message, `400 [INVALID_FIELD] hostnames[]: Invalid hostname allowing wildcard prefix`, which names neither the offending entry nor what was wrong with it.
Null and unknown values defer to the server, per STYLE_GUIDE §Config-time validators.
func IANATimeZone ¶
IANATimeZone checks that a string attribute is a real IANA time-zone identifier, validating against Go's embedded IANA tz database (time.LoadLocation).
Why this source: the Jamf Pro endpoints that accept a timezone validate with java ZoneId semantics. Wire-probed on the mobile device prestage endpoints and again on `/v1/teacher-app` (2026-06-10): the server accepts "UTC" and "Etc/UTC" even though both are ABSENT from the `/api/pro/v1/time-zones` UI-dropdown list (a curated 476-entry, 8-region subset — also missing "GMT"), and rejects "PST", garbage, and null. Gating on that list would false-reject values the server accepts, so it cannot serve as the authoritative gate. Go's embedded tzdata accepts all 476/476 list entries (verified programmatically) and matches the server on the rejects ("PST" and bogus zones fail time.LoadLocation) — zero false-rejects in either direction on everything probed. That is why tzdata, not the API list, is the gate.
Caveat: Java accepts a handful of legacy aliases the IANA database omits (e.g. "PST"); those would be rejected here. The Jamf UI steers users to region IDs ("America/Los_Angeles"), so this is a narrow edge. LoadLocation also falsely accepts "" (→ UTC) and "Local" (→ host local zone); "Local" is rejected explicitly and empty is left to LengthAtLeast(1) for a clearer error. Null/unknown values are deferred per STYLE_GUIDE §Config-time validators.
func IPv4Address ¶
IPv4Address returns a validator.String enforcing dotted-quad IPv4 form.
Extracted at its second consumer per STYLE_GUIDE §Shared abstractions: the Jamf Security Cloud custom DNS zone and ZTNA gateway resources both need it, and two copies of an address check is two places for the next correction to be missed.
Why it exists at all: the Security Cloud endpoints collapse several distinct mistakes into one opaque `400 [INVALID_FIELD] … Invalid field value.` — a non-address string, an IPv6 literal, and a dotted quad with leading zeros were each wire-probed to that same message on 2026-08-27. Catching the shape here names the attribute and says what form is expected.
`net.ParseIP` carries most of the weight, including the leading-zero case, which it has rejected since Go 1.17. What it does not cover is the IPv4-mapped IPv6 literal: `::ffff:203.0.113.53` parses, and its `To4` is non-nil, so the parse alone accepts a form no Jamf endpoint takes. Counting dots does not rule it out either — it has exactly three. Rejecting any colon does.
Null and unknown values defer to the server, per STYLE_GUIDE §Config-time validators.
func IPv4CIDR ¶
IPv4CIDR returns a validator.String enforcing IPv4 CIDR notation.
Why it exists: the Jamf Security Cloud ZTNA app endpoint refuses a bare address and an IPv6 block with the same message — `400 [INVALID_FIELD] bareIps[]: IP address range must be in CIDR format.` — which is accurate for `10.11.12.13` and misleading for `2001:db8::/32`, a perfectly well-formed CIDR block that the endpoint simply does not accept (both wire-probed 2026-08-30). Catching the shape at plan time lets the second case say what is actually wrong.
A prefix length is required: `10.0.0.0` is refused, `10.0.0.0/32` accepted. Colons are rejected outright rather than relying on the parse, for the same reason IPv4Address rejects them — `::ffff:10.0.0.0/104` parses and its `To4` is non-nil, so the parse alone would let an IPv4-mapped IPv6 block through.
A range must also name its own network address rather than a host inside it. `10.99.43.7/24` parses, but Jamf Security Cloud canonicalises it: the write is accepted with `204` and the stored value reads back as `10.99.43.0/24`, and a `/0` prefix collapses the same way to `0.0.0.0/0` whatever address was written (both wire-probed against production EU 2026-08-30). Every attribute fed through this validator is Optional or Required rather than Optional+Computed — removing a range has to clear it, not leave the server's value in place — so Terraform holds the provider to the planned value and the server's rewrite surfaces as `Provider produced inconsistent result after apply` partway through the apply, with the object already created. Refusing the non-canonical form at plan time costs one clear error instead, and names the range actually described, which is worth saying on its own account: `10.1.2.3/0` reads in a plan diff as a single host while matching the entire IPv4 space.
Null and unknown values defer to the server, per STYLE_GUIDE §Config-time validators.
func IPv4CIDRAllowingHostBits ¶
IPv4CIDRAllowingHostBits returns a validator.String enforcing everything IPv4CIDR does except the network-address rule, so `10.10.0.1/16` is accepted.
The concession exists because the network-address rule is a wire fact about one endpoint, not a truth about CIDR notation. It was probed on the ZTNA app's `bareIps` and nowhere else. `jamfplatform_security_cloud_ztna_gateway` is already shipped and its IPsec endpoint has never been probed for canonicalisation, so imposing the rule there would refuse, at plan time, a configuration that may well apply cleanly today — a regression traded for a defect that may not exist. Probe that endpoint and this constructor should go away; until then the shipped behaviour stands.
What it does NOT concede is the IPv4-mapped IPv6 bypass: `::ffff:10.0.0.0/104` is refused here too, because that is a defect in every caller regardless of endpoint.
func NotEmptyString ¶ added in v1.2.2
NotEmptyString returns a validator.String that rejects "" at plan time.
It exists for attributes where Jamf Pro does not store an empty value as sent: it refuses the write, replaces "" with a default, or drops the object. Each one was wire-probed on 2026-10-01 (#445), and in every case the apply failed, either on the server's error or on the post-apply consistency check once the read disagreed with the plan. detail completes the diagnostic: say what Jamf Pro does with "" and what to write instead.
stringvalidator.LengthAtLeast(1) would catch the same input, but its message names a length and leaves the operator to guess why an empty value matters.
Null and unknown values defer to the server, per STYLE_GUIDE §Config-time validators.
func ParseIPv4CIDR ¶
ParseIPv4CIDR parses value against the rules IPv4CIDR enforces, returning the network it names and that network's prefix length.
Exported so one CIDR grammar can serve the callers that need the parsed network as well as the verdict: `internal/resources/security_cloud/ztna_gateway` has to check the Jamf-side subnet's prefix against the bounds of the private range it falls in, and the net.ParseCIDR it used to do that with was a second grammar free to drift from this one — as it had, accepting the IPv4-mapped IPv6 block this one rejects.
The error carries the same sentence the validator puts in a diagnostic, so a caller wrapping it in a diagnostic of its own still says what was wrong.
It applies the shape rules only, not the network-address rule — see IPv4CIDRAllowingHostBits for why that rule is scoped to the one endpoint it was probed on. A caller that wants it should compare the returned network against the input itself.
func PlistDocument ¶
PlistDocument checks that a string attribute holds a complete plist document — one whose root element is `<plist>` — rather than a bare fragment such as a lone `<dict>…</dict>`.
Why this exists: Jamf Pro's configuration-profile endpoints reject a fragment with HTTP 409 "Unable to update the database", which tells the user nothing about the actual mistake. Wire-probed against a live tenant 2026-08-24 (issue #326): the same payload is accepted the moment a `<plist>` root is added, and the root element alone is sufficient — the XML declaration and the DOCTYPE are both optional. So the root element is the gate, and neither of the other two is checked.
The validator only errors when it positively identifies a wrong root element (or no element at all). Anything it cannot parse is deferred to the server: false-rejecting a payload Jamf Pro would have accepted is worse than passing an unparseable one through to the existing error path. Null/unknown values are deferred per STYLE_GUIDE §Config-time validators.
Why plan time and not create time: a fragment used to fail on create yet succeed on update, which reads as a create/update difference in Jamf Pro but is not one — a raw PUT of the same fragment to an existing profile 409s identically (wire-probed 2026-08-24). The asymmetry was ours. payloadhelpers.InjectTopLevelIdentifierValues returns early on create (no identifiers to inject yet) and sends the authored bytes untouched — still the case for the macOS resource; the mobile device resource now strips the top-level identifiers instead, which parses too — but on update it parses the payload — plisthelpers.ParsePlist accepts a bare dict fragment — and re-encodes it, and the encoder always emits a whole document. So update silently repaired the payload and create could not. Gating in the schema closes that gap from both directions: the fragment never reaches either path, so neither can disagree about it again.
func TimeOfDayHHMMSS ¶
TimeOfDayHHMMSS checks that a string attribute is a canonical 24-hour HH:MM:SS time of day (e.g. "17:30:00").
allowEmpty controls whether "" passes: full-replace string fields use "" as the documented clear sentinel (STYLE_GUIDE §Full-replace endpoints, description convention), so clearable attributes pass true; fields where a present value must be a real time (e.g. required begin/end times) pass false. Null/unknown values are deferred per STYLE_GUIDE §Config-time validators.
func UniqueStringFieldList ¶
UniqueStringFieldList returns a validator.List enforcing that the named string attribute is unique across every element of a ListNestedAttribute. Duplicates are reported at the offending element's field path, citing the index of the first occurrence.
func UniqueStringFieldSet ¶
UniqueStringFieldSet returns a validator.Set enforcing that the named string attribute is unique across every element of a SetNestedAttribute. Set elements have no stable index, so duplicates are reported at the set path. Note the Set type alone cannot catch these: elements differing in any other attribute are distinct set members even when the named field collides.
Types ¶
This section is empty.