Add Proxy API types with CEL validation (Step 1)

Full ProxySpec/ProxyStatus/Proxy types per the plan: PlacementSpec,
CloudInitSpec, EndpointSpec, HealthCheckSpec, SecretKeySelector, all
defaults, and 7 CEL XValidation rules enforcing mode/provider
immutability, provider/endpoint required-iff-Managed/External, and
cloud-init exactly-one-of inline/secretRef.

Applies the four corrections identified during planning that would
otherwise be silent bugs: MaxLeases as *int32 (so an explicit 0 survives
Go round-trips instead of re-defaulting to 5), HealthCheck's
default={} marker (so nested defaults apply even when the field is
omitted entirely), MinLength=1 on Provider/CloudInit.Inline (so the CEL
has() checks stay simple), and listType=map on Conditions.

Adds pure helpers (EffectivePort, EffectiveHost, HealthCheckOrDefault,
MaxLeasesOrDefault) with table-driven tests, for use by the health
engine, discovery API, and spec-hash computation in later steps.

Patches the scaffolded placeholder controller test's resource literal to
a schema-valid spec so it survives the new CRD validation — the test
itself is rewritten wholesale in Step 4 alongside the real reconciler.

Regenerated deepcopy and the CRD; make test green (envtest confirmed all
7 CEL rules enforced by a real apiserver).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-07 21:01:56 +02:00
parent 26991fbe13
commit f28766fce3
8 changed files with 985 additions and 41 deletions

View File

@@ -11,10 +11,31 @@ spec:
kind: Proxy
listKind: ProxyList
plural: proxies
shortNames:
- px
singular: proxy
scope: Namespaced
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .spec.mode
name: Mode
type: string
- jsonPath: .spec.provider
name: Provider
type: string
- jsonPath: .status.phase
name: Phase
type: string
- jsonPath: .status.ip
name: IP
type: string
- jsonPath: .status.conditions[?(@.type=="Healthy")].status
name: Healthy
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: Proxy is the Schema for the proxies API
@@ -39,25 +60,198 @@ spec:
spec:
description: spec defines the desired state of Proxy
properties:
foo:
description: foo is an example field of Proxy. Edit proxy_types.go
to remove/update
attributes:
additionalProperties:
type: string
description: |-
attributes are selection attributes exposed to the discovery API
(geo, asn, purpose, ...). Deliberately separate from Kubernetes object
labels, which stay an operator implementation concern.
maxProperties: 32
type: object
cloudInit:
description: |-
cloudInit supplies the VM's cloud-init user-data. Only meaningful for
Managed proxies. Changing the resolved content triggers replacement.
properties:
inline:
description: inline is the literal cloud-init user-data.
maxLength: 262144
minLength: 1
type: string
secretRef:
description: secretRef points at a Secret key holding the cloud-init
user-data.
properties:
key:
default: user-data
description: key is the data key holding the cloud-init user-data.
type: string
name:
description: name is the Secret's name.
minLength: 1
type: string
required:
- name
type: object
type: object
x-kubernetes-validations:
- message: exactly one of inline or secretRef must be set
rule: has(self.inline) != has(self.secretRef)
endpoint:
description: |-
endpoint identifies an External proxy's network location. Required
iff mode is External; must be unset iff mode is Managed.
properties:
host:
description: host is the proxy's hostname or IP address.
minLength: 1
type: string
port:
default: 3128
description: port is the port the proxy listens on.
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- host
type: object
healthCheck:
default: {}
description: |-
healthCheck configures the through-the-proxy healthcheck. All nested
fields are defaulted; the default={} marker ensures a proxy that omits
healthCheck entirely still gets every nested default.
properties:
expectedStatusCodes:
default:
- 200
- 204
description: |-
expectedStatusCodes are the HTTP status codes a probe response must
match to count as successful.
items:
format: int32
type: integer
maxItems: 8
type: array
failureThreshold:
default: 3
description: |-
failureThreshold is the number of consecutive failed probes required
to transition Healthy -> False.
format: int32
minimum: 1
type: integer
intervalSeconds:
default: 30
description: intervalSeconds is the time between probes for a
given proxy.
format: int32
minimum: 5
type: integer
probeURL:
default: https://www.gstatic.com/generate_204
description: probeURL is fetched through the proxy on every probe.
minLength: 1
type: string
successThreshold:
default: 1
description: |-
successThreshold is the number of consecutive successful probes
required to transition Healthy -> True.
format: int32
minimum: 1
type: integer
timeoutSeconds:
default: 5
description: |-
timeoutSeconds bounds a single probe, including the CONNECT tunnel
setup and the TLS handshake through it.
format: int32
minimum: 1
type: integer
type: object
maxLeases:
default: 5
description: |-
maxLeases is the maximum number of concurrent leases handed out for
this proxy. 0 means unleasable (list-only visibility). A pointer so an
explicit 0 survives Go round-trips instead of being re-defaulted to 5.
format: int32
maximum: 1000
minimum: 0
type: integer
mode:
description: |-
mode selects who owns the VM lifecycle: Managed (operator creates it)
or External (operator only tracks and healthchecks it). Immutable.
enum:
- Managed
- External
type: string
placement:
description: |-
placement is provider-opaque placement/size configuration. Only
meaningful for Managed proxies.
properties:
image:
description: image is the boot image reference.
type: string
machineType:
description: machineType is the provider's machine/instance type
(e.g. "e2-micro").
type: string
region:
description: region is the provider's region identifier (e.g.
"europe-west1").
type: string
zone:
description: zone is the provider's zone identifier (e.g. "europe-west1-b").
type: string
type: object
port:
default: 3128
description: |-
port is the port the proxy listens on once the VM is up. Only
meaningful for Managed proxies; External proxies use endpoint.port.
format: int32
maximum: 65535
minimum: 1
type: integer
provider:
description: |-
provider is the name of a configured provider ("mock", "gcp-eu", ...).
Required iff mode is Managed; must be unset iff mode is External.
Immutable.
maxLength: 63
minLength: 1
type: string
required:
- mode
type: object
x-kubernetes-validations:
- message: mode is immutable
rule: self.mode == oldSelf.mode
- message: provider is immutable
rule: has(self.provider) == has(oldSelf.provider) && (!has(self.provider)
|| self.provider == oldSelf.provider)
- message: provider is required when mode is Managed
rule: self.mode != 'Managed' || has(self.provider)
- message: provider must not be set when mode is External
rule: self.mode != 'External' || !has(self.provider)
- message: endpoint is required when mode is External
rule: self.mode != 'External' || has(self.endpoint)
- message: endpoint must not be set when mode is Managed
rule: self.mode != 'Managed' || !has(self.endpoint)
status:
description: status defines the observed state of Proxy
properties:
conditions:
description: |-
conditions represent the current state of the Proxy resource.
Each condition has a unique type and reflects the status of a specific aspect of the resource.
Standard condition types include:
- "Available": the resource is fully functional
- "Progressing": the resource is being created or updated
- "Degraded": the resource failed to reach or maintain its desired state
The status of each condition is one of True, False, or Unknown.
Standard types are Provisioned and Healthy.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
@@ -116,6 +310,37 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
ip:
description: |-
ip is the proxy's current IP address: the VM's address for Managed
proxies, or spec.endpoint.host for External proxies.
type: string
lastHealthCheckTime:
description: |-
lastHealthCheckTime is the time of the last status-affecting probe,
not the time of the most recent probe — probes that don't change the
Healthy condition or move latency materially don't write status.
format: date-time
type: string
latencyMillis:
description: latencyMillis is the latency of the last status-affecting
probe.
format: int64
type: integer
observedGeneration:
description: observedGeneration is the .metadata.generation last reconciled.
format: int64
type: integer
phase:
description: |-
phase is a high-level, human-readable summary derived from conditions.
One of Pending, Provisioning, Ready, Unhealthy, Deleting, Failed.
type: string
providerID:
description: |-
providerID is the opaque cloud resource ID returned by the provider.
Empty for External proxies.
type: string
type: object
required:
- spec