Skip to content

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

Field Type Default Description
url string required Base URL of the classifier service
request NodeClassifierRequest required HTTP request configuration
response NodeClassifierResponse required Response interpretation settings
timeoutSeconds int32 10 HTTP request timeout
auth NodeClassifierAuth - Authentication method
cache NodeClassifierCache - Disk caching settings

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

  1. Create a NodeClassifier resource with your classifier endpoint configuration
  2. Set nodeClassifierRef on your Config to reference the NodeClassifier
  3. The operator renders an enc.yaml config into a Secret, mounted into Server pods
  4. puppet.conf gets node_terminus = exec and external_nodes = /usr/local/bin/openvox-enc
  5. When Puppet Server needs to classify a node, it calls the openvox-enc binary
  6. 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.