Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1bfa0c0
feat: per-policy SAN and authorization-extension allowlists for autosign
slauger Aug 6, 2026
866335b
refactor: rename SigningPolicy 'pattern' field to 'certnames'
slauger Aug 6, 2026
bce0e1f
Merge remote-tracking branch 'origin/develop' into feat/506-per-polic…
slauger Sep 3, 2026
e70d8c8
fix(autosign): reserve the operator certname and bind the CSR subject
slauger Sep 3, 2026
49ecfa3
docs: align the SigningPolicy documentation with the renamed field an…
slauger Sep 3, 2026
c1dd4c1
chore(deps): update dependency conforma/cli to v0.10.3 (#584)
renovate[bot] Sep 3, 2026
3e1eeb5
docs: correct manifest examples that the API server rejects
slauger Sep 3, 2026
7397a42
docs: correct behaviour the controller does not have
slauger Sep 3, 2026
aab8ce7
docs: explain that no SigningPolicy means deny-all
slauger Sep 3, 2026
ab6c28e
docs: close the gaps between the reference pages and the CRDs
slauger Sep 3, 2026
a61c135
docs: add a guide for connecting agents
slauger Sep 3, 2026
93d4757
docs: fix the remaining broken example and two claims about behaviour
slauger Sep 3, 2026
baa6b55
docs: stop advertising JVM auto-tuning that does not happen
slauger Sep 3, 2026
51bd89a
Merge pull request #591 from slauger/docs/review-corrections
slauger Sep 3, 2026
f330b55
Merge pull request #516 from slauger/feat/506-per-policy-san-extensio…
slauger Sep 3, 2026
a57cf74
fix(api): let javaArgs be empty so the heap can follow the memory limit
slauger Sep 3, 2026
8d82785
fix(ca): apply signing policy changes without restarting the CA
slauger Sep 3, 2026
dce4c95
fix(deps): update module sigs.k8s.io/gateway-api to v1.6.2 (#585)
renovate[bot] Sep 4, 2026
c3b7f4b
Merge pull request #593 from slauger/fix/javaargs-and-policy-mount
slauger Sep 4, 2026
e5703ae
feat(metrics): add authenticated HTTPS serving behind an opt-in
slauger Sep 4, 2026
d59fedf
docs: make updating the documentation part of the change
slauger Sep 4, 2026
fc124f2
Merge pull request #594 from slauger/fix/metrics-authn-authz
slauger Sep 4, 2026
1f3159c
Merge pull request #596 from slauger/docs/agent-doc-rule
slauger Sep 4, 2026
b375418
fix(image): drop obsolete openvoxserver-ca rootless patches
slauger Sep 4, 2026
0dafb38
Merge pull request #597 from slauger/fix/remove-obsolete-ca-gem-patch
slauger Sep 4, 2026
c548647
ci: add golangci-lint configuration
slauger Sep 5, 2026
0e2c7d9
fix: resolve gosec, errorlint and unconvert findings
slauger Sep 5, 2026
b2fbcd0
fix: clean remaining file reads and uncap lint findings
slauger Sep 5, 2026
ae42cba
ci: expand golangci-lint to the common operator linter set
slauger Sep 5, 2026
779153f
refactor: apply the expanded golangci-lint findings
slauger Sep 5, 2026
4b967c0
Merge pull request #601 from slauger/ci/golangci-config
slauger Sep 5, 2026
3f5e4fb
chore(deps): update dependency puppetlabs-stdlib to v10.1.0
renovate[bot] Sep 7, 2026
9b1ca3f
fix(deps): update openvox 8 versions
renovate[bot] Sep 8, 2026
5b7d6d5
chore(deps): update dependency conforma/cli to v0.10.5 (#604)
renovate[bot] Sep 8, 2026
e60fc84
fix(deps): update module golang.org/x/vuln to v1.8.0
renovate[bot] Sep 9, 2026
5daa15e
fix(deps): update container base images
renovate[bot] Sep 9, 2026
886eaf7
Merge pull request #602 from slauger/renovate/puppetlabs-stdlib-10.x
slauger Sep 9, 2026
ca83785
Merge pull request #606 from slauger/renovate/container-base-images
slauger Sep 9, 2026
3eb80d7
Merge pull request #605 from slauger/renovate/go-dependencies
slauger Sep 9, 2026
09dfd45
Merge pull request #600 from slauger/renovate/openvox-8-versions
slauger Sep 9, 2026
7eb5c2f
chore(deps): update dependency conforma/cli to v0.10.7 (#607)
renovate[bot] Sep 10, 2026
c6c833a
chore(deps): update dependency conforma/cli to v0.10.10 (#608)
renovate[bot] Sep 11, 2026
ffeda52
chore(deps): update dependency conforma/cli to v0.10.11 (#609)
renovate[bot] Sep 11, 2026
107ea76
fix(deps): update module sigs.k8s.io/controller-runtime to v0.25.1 (#…
renovate[bot] Sep 14, 2026
d82ae91
fix(deps): update container base images (#610)
renovate[bot] Sep 14, 2026
ea25771
fix(deps): update container base images
slauger Sep 17, 2026
734498c
chore(deps): update dependency conforma/cli to v0.10.17
slauger Sep 17, 2026
3c255ac
fix(deps): update google.golang.org/grpc to v1.83.1
slauger Sep 17, 2026
0a5e119
Merge pull request #614 from slauger/chore/dependency-updates
slauger Sep 17, 2026
feba260
fix(deps): update registry.access.redhat.com/ubi9/ubi docker tag to v…
renovate[bot] Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_conforma-validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
packages: read
env:
# renovate: datasource=github-releases depName=conforma/cli
EC_VERSION: "0.10.2"
EC_VERSION: "0.10.17"
steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down
80 changes: 80 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
version: "2"

run:
timeout: 5m

linters:
default: standard
enable:
- asciicheck
- bidichk
- bodyclose
- copyloopvar
- dogsled
- durationcheck
- errorlint
- gocritic
- gosec
- importas
- intrange
- loggercheck
- misspell
- modernize
- nakedret
- nilerr
- nolintlint
- prealloc
- revive
- unconvert
- unparam
settings:
importas:
no-unaliased: true
alias:
- pkg: k8s.io/api/core/v1
alias: corev1
- pkg: k8s.io/api/apps/v1
alias: appsv1
- pkg: k8s.io/api/autoscaling/v2
alias: autoscalingv2
- pkg: k8s.io/api/batch/v1
alias: batchv1
- pkg: k8s.io/api/discovery/v1
alias: discoveryv1
- pkg: k8s.io/api/networking/v1
alias: networkingv1
- pkg: k8s.io/api/policy/v1
alias: policyv1
- pkg: k8s.io/api/rbac/v1
alias: rbacv1
- pkg: k8s.io/apimachinery/pkg/api/errors
alias: apierrors
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
alias: metav1
- pkg: sigs.k8s.io/gateway-api/apis/v1
alias: gwapiv1
nolintlint:
require-specific: true
revive:
rules:
- name: exported
disabled: true
exclusions:
generated: lax
rules:
# Test helpers construct fixtures with hardcoded values and do not need
# the same hardening as production code.
- path: _test\.go
linters:
- gosec

formatters:
enable:
- gofmt
- goimports

issues:
# Report everything instead of capping repeated findings (the defaults hide
# issues beyond 3 of the same kind, which makes CI runs non-exhaustive).
max-issues-per-linter: 0
max-same-issues: 0
51 changes: 49 additions & 2 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,58 @@ docs/ Documentation

## Documentation

- `README.md` is the primary user-facing doc - update when adding user-visible features
`docs/` is user-facing reference, not design notes: 35 pages covering every CRD
field, phase, condition and metric. Treat it as part of the API.

- `README.md` is the entry point - update when adding user-visible features
- `CONTRIBUTING.md` covers developer setup and workflow
- `docs/` contains design docs and architecture diagrams
- No CHANGELOG - release notes are auto-generated from commit messages by semantic-release

### Documentation is part of the change, not a follow-up

**Every change that alters observable behaviour updates the documentation in
the same commit.** Not in a later pass: drift found afterwards is drift that
shipped, and a reader cannot tell a stale sentence from a correct one.

What to check, by kind of change:

| Change | Update |
|---|---|
| CRD field added, removed or renamed | the field table in `docs/reference/<kind>.md` |
| A `+kubebuilder:default` added or removed | the table **and** the prose around it - these have contradicted each other |
| New or changed condition, phase, reason | the conditions table; do not document a phase the controller never sets |
| New or removed metric | `docs/guides/monitoring.md`, including an alert rule where one makes sense |
| Behaviour a reader would predict wrongly | the relevant `docs/concepts/` page |
| New chart value | `values.yaml` comments, then regenerate `README.md` and `values.schema.json` |
| Resource name, label or selector | anything in `docs/` that names it - troubleshooting commands go stale silently |

### Verify rather than assume

The documentation had never been checked against the code before 2026-09-03,
and the review that followed found examples the API server rejects, three label
selectors that match nothing, a phase no controller sets and image tags that
cannot exist. Each was plausible on the page.

Cheap checks worth running when touching docs:

```bash
# Manifests in the docs must be accepted by a real API server
kubectl apply --dry-run=server -f <extracted-block>.yaml

# Selectors must exist in internal/controller/labels.go
grep -rn "kubectl.*-l " docs/

# Fields documented for a kind must exist in its CRD, and vice versa
ruby -ryaml -e 'd=YAML.load_file(ARGV[0]);
acc=[]; w=lambda{|n| next unless n.is_a?(Hash);
(n["properties"]||{}).each{|k,v| acc<<k; w.call(v)}; w.call(n["items"]) if n["items"]};
d["spec"]["versions"].each{|v| w.call(v["schema"]["openAPIV3Schema"])};
puts acc.uniq.sort' config/crd/bases/openvox.voxpupuli.org_servers.yaml
```

When a documented claim cannot be verified against the code, say so in the
text rather than leaving it to look confirmed.

### Feature List Synchronization

The canonical feature list lives in `docs/_snippets/features.md`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A Kubernetes Operator that maps [OpenVox Server](https://github.com/OpenVoxProje
- 🔄 **Multi-Version Deployments** - Run different server versions side by side - canary deployments, rolling upgrades
- 🔒 **Rootless & OpenShift Ready** - Random UID compatible, no root, no ezbake, no privilege escalation
- 🪶 **Minimal Image** - UBI9-based, no agent Ruby, no ezbake packaging - smaller footprint, fewer updates
- 🧠 **Auto-tuned JVM** - Heap size calculated from memory limits (90%) - no manual `-Xmx` tuning needed
- 🧠 **Auto-tuned JVM** - Heap derived from the pod memory limit (90%) unless `javaArgs` is set
- 📦 **OCI Image Volumes** - Package Puppet code as OCI images, deploy immutably with automatic rollout (K8s 1.35+)
- 🌐 **Gateway API** - SNI-based TLSRoute support - share a single LoadBalancer across environments via TLS passthrough
- 🗄️ **Managed OpenVox DB** - Deploy OpenVox DB (PuppetDB) with external PostgreSQL - TLS, config, and credentials managed by the operator
Expand Down
15 changes: 7 additions & 8 deletions api/v1alpha1/bool_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)

// TestDefaultedBooleansRoundTrip pins the behaviour of the optional boolean
Expand All @@ -27,7 +26,7 @@ func TestDefaultedBooleansRoundTrip(t *testing.T) {
ConfigRef: "production",
CertificateRef: "production-cert",
CA: true,
Server: ptr.To(false),
Server: new(false),
},
}
if err := k8sClient.Create(ctx, s); err != nil {
Expand Down Expand Up @@ -63,8 +62,8 @@ func TestDefaultedBooleansRoundTrip(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{GenerateName: "test-config-", Namespace: "default"},
Spec: ConfigSpec{
Image: ImageSpec{Repository: "example.invalid/openvox-server", Tag: "latest"},
ReadOnlyRootFilesystem: ptr.To(false),
Puppet: PuppetSpec{Storeconfigs: ptr.To(false)},
ReadOnlyRootFilesystem: new(false),
Puppet: PuppetSpec{Storeconfigs: new(false)},
},
}
if err := k8sClient.Create(ctx, c); err != nil {
Expand All @@ -84,10 +83,10 @@ func TestDefaultedBooleansRoundTrip(t *testing.T) {
ca := &CertificateAuthority{
ObjectMeta: metav1.ObjectMeta{GenerateName: "test-ca-", Namespace: "default"},
Spec: CertificateAuthoritySpec{
AllowSubjectAltNames: ptr.To(false),
AllowAuthorizationExtensions: ptr.To(false),
EnableInfraCRL: ptr.To(false),
AllowAutoRenewal: ptr.To(false),
AllowSubjectAltNames: new(false),
AllowAuthorizationExtensions: new(false),
EnableInfraCRL: new(false),
AllowAutoRenewal: new(false),
},
}
if err := k8sClient.Create(ctx, ca); err != nil {
Expand Down
17 changes: 8 additions & 9 deletions api/v1alpha1/certificateauthority_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/ptr"
)

func validCA() *CertificateAuthority {
Expand Down Expand Up @@ -44,7 +43,7 @@ func TestCertificateAuthorityStorageExclusivity(t *testing.T) {
{
name: "storage without external accepted",
mutate: func(ca *CertificateAuthority) {
ca.Spec.Storage = &StorageSpec{Size: ptr.To(resource.MustParse("10Gi"))}
ca.Spec.Storage = &StorageSpec{Size: new(resource.MustParse("10Gi"))}
},
},
{
Expand All @@ -57,15 +56,15 @@ func TestCertificateAuthorityStorageExclusivity(t *testing.T) {
name: "external with custom storage rejected",
mutate: func(ca *CertificateAuthority) {
ca.Spec.External = external
ca.Spec.Storage = &StorageSpec{Size: ptr.To(resource.MustParse("10Gi"))}
ca.Spec.Storage = &StorageSpec{Size: new(resource.MustParse("10Gi"))}
},
wantErr: "external and storage are mutually exclusive",
},
{
name: "external with storage at the default size rejected",
mutate: func(ca *CertificateAuthority) {
ca.Spec.External = external
ca.Spec.Storage = &StorageSpec{Size: ptr.To(resource.MustParse("1Gi"))}
ca.Spec.Storage = &StorageSpec{Size: new(resource.MustParse("1Gi"))}
},
wantErr: "external and storage are mutually exclusive",
},
Expand Down Expand Up @@ -136,15 +135,15 @@ func TestCertificateAuthorityStorageSizeValidation(t *testing.T) {
ctx := context.Background()

t.Run("invalid quantity rejected", func(t *testing.T) {
raw := &unstructured.Unstructured{Object: map[string]interface{}{
raw := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": GroupVersion.String(),
"kind": "CertificateAuthority",
"metadata": map[string]interface{}{
"metadata": map[string]any{
"generateName": "test-ca-",
"namespace": "default",
},
"spec": map[string]interface{}{
"storage": map[string]interface{}{"size": "1Gib"},
"spec": map[string]any{
"storage": map[string]any{"size": "1Gib"},
},
}}
err := k8sClient.Create(ctx, raw)
Expand All @@ -159,7 +158,7 @@ func TestCertificateAuthorityStorageSizeValidation(t *testing.T) {

t.Run("valid quantity accepted", func(t *testing.T) {
ca := validCA()
ca.Spec.Storage = &StorageSpec{Size: ptr.To(resource.MustParse("500Mi"))}
ca.Spec.Storage = &StorageSpec{Size: new(resource.MustParse("500Mi"))}
if err := k8sClient.Create(ctx, ca); err != nil {
t.Fatalf("a valid quantity must be accepted, got: %v", err)
}
Expand Down
36 changes: 36 additions & 0 deletions api/v1alpha1/javaargs_default_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package v1alpha1

import (
"context"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TestServerJavaArgsHasNoDefault is the test that was missing while the bug
// existed. The unit tests around resolveJavaArgs all call the function
// directly and therefore pass whether or not the field can ever be empty; only
// a round-trip through the API server shows that.
//
// A default here is not cosmetic: the controller derives the heap from the
// pod's memory limit exactly when javaArgs is empty, so a default silently
// pins every Server to the same heap.
func TestServerJavaArgsHasNoDefault(t *testing.T) {
ctx := context.Background()

server := &Server{
ObjectMeta: metav1.ObjectMeta{GenerateName: "test-server-", Namespace: "default"},
Spec: ServerSpec{
ConfigRef: "production",
CertificateRef: "production-cert",
},
}
if err := k8sClient.Create(ctx, server); err != nil {
t.Fatalf("creating Server: %v", err)
}
t.Cleanup(func() { _ = k8sClient.Delete(ctx, server) })

if server.Spec.JavaArgs != "" {
t.Errorf("javaArgs must come back empty so the heap can be derived, got %q", server.Spec.JavaArgs)
}
}
6 changes: 5 additions & 1 deletion api/v1alpha1/server_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ type ServerSpec struct {
Resources corev1.ResourceRequirements `json:"resources,omitempty"`

// JavaArgs defines the JVM arguments.
// +kubebuilder:default="-Xms512m -Xmx1024m"
//
// There is deliberately no default. A defaulted field is never empty, and
// the controller uses emptiness to decide whether to derive the heap from
// the pod's memory limit. With a default in place that derivation is dead
// code and every Server runs on the same heap regardless of its limit.
// +optional
JavaArgs string `json:"javaArgs,omitempty"`

Expand Down
35 changes: 31 additions & 4 deletions api/v1alpha1/signingpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,42 @@ type SigningPolicySpec struct {
// +optional
Any bool `json:"any,omitempty"`

// Pattern defines certname glob matching rules.
// Certnames defines allowed certname glob patterns. The certname must match
// at least one.
// +optional
Pattern *PatternSpec `json:"pattern,omitempty"`
Certnames *PatternSpec `json:"certnames,omitempty"`

// DNSAltNames defines allowed DNS subject alternative name patterns.
// If not set and Any is false, CSRs with SANs are denied by the autosign binary.
// DNSAltNames defines allowed DNS subject alternative name patterns (glob).
// If a CSR carries DNS SANs and this is not set, the CSR is denied.
// +optional
DNSAltNames *PatternSpec `json:"dnsAltNames,omitempty"`

// IPAltNames defines allowed IP subject alternative names as CIDR ranges
// (e.g. "10.0.0.0/16", "::1/128"). If a CSR carries IP SANs and this is not
// set, the CSR is denied.
// +optional
IPAltNames *PatternSpec `json:"ipAltNames,omitempty"`

// URIAltNames defines allowed URI subject alternative name patterns. A "*"
// wildcard matches any run of characters, including "/". If a CSR carries URI
// SANs and this is not set, the CSR is denied.
// +optional
URIAltNames *PatternSpec `json:"uriAltNames,omitempty"`

// EmailAltNames defines allowed email subject alternative name patterns. A "*"
// wildcard matches any run of characters, including "@". If a CSR carries email
// SANs and this is not set, the CSR is denied.
// +optional
EmailAltNames *PatternSpec `json:"emailAltNames,omitempty"`

// Extensions lists Puppet CSR extension names (e.g. pp_cli_auth) that a CSR
// matched by this policy is permitted to carry. Privileged authorization
// extensions (the 1.3.6.1.4.1.34380.1.3 arc: pp_cli_auth, pp_authorization,
// pp_auth_token) are denied unless listed here; this gate applies to every
// policy, including one with any=true. Trusted-fact extensions are unaffected.
// +optional
Extensions *PatternSpec `json:"extensions,omitempty"`

// CSRAttributes defines CSR extension attributes that must all match (AND logic).
// Each entry specifies an attribute name and the expected value (inline or from a Secret).
// +optional
Expand Down
Loading
Loading