Files
egress-proxies-operator/config/crd/bases/crawl.example.com_proxies.yaml
Jan Novak f28766fce3 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>
2026-08-07 21:01:56 +02:00

352 lines
15 KiB
YAML

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.21.0
name: proxies.crawl.example.com
spec:
group: crawl.example.com
names:
kind: Proxy
listKind: ProxyList
plural: proxies
shortNames:
- px
singular: proxy
scope: Namespaced
versions:
- 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
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: spec defines the desired state of Proxy
properties:
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.
Standard types are Provisioned and Healthy.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
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
type: object
served: true
storage: true
subresources:
status: {}