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

87
api/v1alpha1/helpers.go Normal file
View File

@@ -0,0 +1,87 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
// EffectivePort returns the port a client should use to reach the proxy:
// spec.endpoint.port for External proxies, spec.port for Managed proxies.
// Falls back to DefaultPort if the relevant field is unset, so callers that
// bypass CRD structural defaulting (unit tests, fake clients) still get a
// sane value.
func (p *Proxy) EffectivePort() int32 {
if p.Spec.Mode == ModeExternal && p.Spec.Endpoint != nil {
if p.Spec.Endpoint.Port != 0 {
return p.Spec.Endpoint.Port
}
return DefaultPort
}
if p.Spec.Port != 0 {
return p.Spec.Port
}
return DefaultPort
}
// EffectiveHost returns the host a client should use to reach the proxy:
// spec.endpoint.host for External proxies, status.ip for Managed proxies
// (populated once the VM is running).
func (p *Proxy) EffectiveHost() string {
if p.Spec.Mode == ModeExternal && p.Spec.Endpoint != nil {
return p.Spec.Endpoint.Host
}
return p.Status.IP
}
// HealthCheckOrDefault returns spec.healthCheck with every unset field
// filled from its default. CRD structural defaulting (the default={} marker
// on ProxySpec.HealthCheck) already does this for objects that went through
// the API server; this is for callers that didn't (unit tests, fake
// clients, or a Proxy constructed directly in Go).
func (p *Proxy) HealthCheckOrDefault() HealthCheckSpec {
var hc HealthCheckSpec
if p.Spec.HealthCheck != nil {
hc = *p.Spec.HealthCheck
}
if hc.ProbeURL == "" {
hc.ProbeURL = DefaultProbeURL
}
if hc.IntervalSeconds == 0 {
hc.IntervalSeconds = DefaultHealthCheckIntervalSeconds
}
if hc.TimeoutSeconds == 0 {
hc.TimeoutSeconds = DefaultHealthCheckTimeoutSeconds
}
if hc.FailureThreshold == 0 {
hc.FailureThreshold = DefaultFailureThreshold
}
if hc.SuccessThreshold == 0 {
hc.SuccessThreshold = DefaultSuccessThreshold
}
if len(hc.ExpectedStatusCodes) == 0 {
hc.ExpectedStatusCodes = append([]int32(nil), DefaultExpectedStatusCodes...)
}
return hc
}
// MaxLeasesOrDefault returns spec.maxLeases, or DefaultMaxLeases if unset.
// spec.maxLeases is a pointer specifically so an explicit 0 (unleasable) is
// distinguishable from "unset" and survives Go round-trips; this helper
// preserves that distinction.
func (p *Proxy) MaxLeasesOrDefault() int32 {
if p.Spec.MaxLeases != nil {
return *p.Spec.MaxLeases
}
return DefaultMaxLeases
}