NodeClassifier
A NodeClassifier defines an External Node Classifier (ENC) endpoint for Puppet Server. It specifies how to query an external service for node classification data (classes, parameters, environment).
NodeClassifier is a standalone resource referenced by Config via nodeClassifierRef. This allows reusing the same classifier across multiple Configs.
Example
Foreman
Foreman is not directly compatible with the NodeClassifier CRD. See #26 and the External Node Classification guide for details.
Puppet Enterprise (POST, Token Auth)
apiVersion: openvox.voxpupuli.org/v1alpha1
kind: NodeClassifier
metadata:
name: pe-classifier
spec:
url: https://pe-console.example.com:4433
request:
method: POST
path: /classifier-api/v1/classified/nodes/{certname}
body: facts
response:
format: json
auth:
token:
header: X-Authentication
secretKeyRef:
name: pe-rbac-token
key: token
cache:
enabled: true
Generic HTTP (Bearer Token)
apiVersion: openvox.voxpupuli.org/v1alpha1
kind: NodeClassifier
metadata:
name: custom-enc
spec:
url: https://enc-service.internal:8443
request:
method: GET
path: /v1/classify/{certname}
response:
format: yaml
auth:
bearer:
secretKeyRef:
name: enc-api-token
key: token
Cluster-internal (no auth)
apiVersion: openvox.voxpupuli.org/v1alpha1
kind: NodeClassifier
metadata:
name: internal-enc
spec:
url: http://enc-service.enc-system.svc:8080
request:
method: GET
path: /enc/{certname}
response:
format: yaml
Config referencing a NodeClassifier
apiVersion: openvox.voxpupuli.org/v1alpha1
kind: Config
metadata:
name: production
spec:
authorityRef: production-ca
image:
repository: ghcr.io/slauger/openvox-server-9
tag: "latest"
nodeClassifierRef: foreman
Spec
NodeClassifierRequest
| Field |
Type |
Default |
Description |
method |
string |
GET |
HTTP method (GET or POST) |
path |
string |
/node/{certname} |
URL path template. {certname} is replaced with the node's certname |
body |
string |
- |
POST body type: facts (PE-compatible JSON with certname + facts), certname (minimal JSON), or empty. Only allowed with POST method |
NodeClassifierResponse
| Field |
Type |
Default |
Description |
format |
string |
yaml |
Expected response format: yaml or json |
NodeClassifierAuth
At most one authentication method may be configured.
| Field |
Type |
Description |
mtls |
bool |
Use Puppet SSL certificates for mutual TLS |
token |
TokenAuth |
Send token via custom HTTP header |
bearer |
SecretKeySelector |
Send Bearer token via Authorization header |
basic |
BasicAuth |
HTTP Basic Authentication |
TokenAuth
| Field |
Type |
Description |
header |
string |
HTTP header name (e.g. X-Authentication) |
secretKeyRef |
SecretKeyRef |
Reference to the Secret key holding the token value |
SecretKeySelector
| Field |
Type |
Description |
secretKeyRef |
SecretKeyRef |
Reference to the Secret key holding the bearer token |
SecretKeyRef
| Field |
Type |
Description |
name |
string |
Name of the Secret |
key |
string |
Key within the Secret |
BasicAuth
| Field |
Type |
Description |
secretRef |
BasicAuthSecretRef |
Reference to the Secret containing username and password |
BasicAuthSecretRef
| Field |
Type |
Default |
Description |
name |
string |
required |
Name of the Secret |
usernameKey |
string |
username |
Key within the Secret containing the username |
passwordKey |
string |
password |
Key within the Secret containing the password |
NodeClassifierCache
| Field |
Type |
Default |
Description |
enabled |
bool |
false |
Enable disk caching of classifier responses |
directory |
string |
/var/cache/openvox-enc |
Cache directory path inside the container |
Status
| Field |
Type |
Description |
observedGeneration |
int64 |
The .metadata.generation the status was last derived from. A value below .metadata.generation means the rest of this status has not caught up with the current spec yet |
phase |
string |
Current lifecycle phase |
conditions |
[]Condition |
Ready |
Phases
| Phase |
Description |
Active |
Classifier configuration is rendered and active |
Disabled |
Deliberately bypassed by an externalNodesCommand override -- a configuration choice, not a fault |
Error |
The classifier is not in effect -- see the Ready condition for which case |
The status is derived from the rendered ENC Secret, so it reports whether this
classifier actually reached a server rather than whether the resource itself is
well-formed. The Ready condition carries the reason:
| Reason |
Meaning |
Rendered |
The endpoint is present in the rendered Secret, at the classifier's current generation |
NoConfig |
No Config sets nodeClassifierRef to this NodeClassifier, so nothing renders it |
OverriddenByExternalNodesCommand |
Every Config referencing it sets spec.puppet.externalNodesCommand, which replaces the built-in binary and bypasses NodeClassifier resources |
NotRendered |
No Secret rendered from this NodeClassifier exists, or the one that exists was rendered from a different one |
RenderedConfigStale |
A Secret was rendered from this NodeClassifier, but from an earlier generation |
RenderedConfigSourceUnknown |
The Secret predates this mechanism and does not record what it was rendered from; it resolves once the Config controller re-renders |
Automation upgrading from an earlier operator version should note that these
reasons replace the previous two: ConfigRendered became Rendered, and a
single catch-all Error reason was split into the specific cases above.
enc.yaml carries no resource name, so the Secret's
openvox.voxpupuli.org/rendered-from annotation is what ties the rendered file
back to this NodeClassifier and to the generation it was rendered at. That also
catches a Secret left over from a previous nodeClassifierRef, which would
otherwise read as active. Where several Configs reference the same classifier,
any one of them holding a Secret that does not match the current generation --
an earlier generation, a different classifier, or one that records no source at
all -- holds the whole resource out of Ready: the current spec is not in
effect everywhere yet. A Config that has rendered no Secret at all is the
exception, since nothing is mounted there to contradict it.
Rendering failures -- an unresolvable auth Secret, for example -- are reported
on the Config that owns the Secret, as an ENCRenderFailed event, and the
failed render leaves the previous Secret untouched.
What the classifier reports then depends on whether its own spec changed. If a
spec edit triggered the failing render, the generation moved on and the
classifier reports RenderedConfigStale. If the spec did not change -- the
referenced auth Secret was rotated or deleted underneath it -- the generation
is unchanged, the previous Secret still matches it, and the classifier keeps
reporting Active while the servers classify against the old credential. Watch
the Config's ENCRenderFailed events for that case; the classifier's own
status cannot see it.
How It Works
- Create a NodeClassifier resource with your classifier endpoint configuration
- Set
nodeClassifierRef on your Config to reference the NodeClassifier
- The operator renders an
enc.yaml config into a Secret, mounted into Server pods
- puppet.conf gets
node_terminus = exec and external_nodes = /usr/local/bin/openvox-enc
- When Puppet Server needs to classify a node, it calls the
openvox-enc binary
- The binary reads
enc.yaml, queries the classifier service, and returns Puppet ENC YAML
flowchart TD
PS["Puppet Server"] -->|"calls openvox-enc certname"| ENC["openvox-enc binary"]
ENC -->|"reads"| Config["enc.yaml (from Secret)"]
ENC -->|"HTTP GET/POST"| Classifier["External Classifier<br/>(Foreman, PE, custom)"]
Classifier -->|"JSON/YAML response"| ENC
ENC -->|"Puppet ENC YAML"| PS
ENC -.->|"optional"| Cache["Disk Cache<br/>(emptyDir)"]
When the classifier is unreachable and caching is enabled, the binary falls back to the last cached response for that node.