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>
352 lines
15 KiB
YAML
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: {}
|